فهرست منبع

Adding Elastica & Elasticsearch libraries

Paolo Libertini 6 سال پیش
کامیت
043097ff85
100فایلهای تغییر یافته به همراه9395 افزوده شده و 0 حذف شده
  1. 10 0
      .idea/composerJson.xml
  2. 6 0
      .idea/encodings.xml
  3. 12 0
      .idea/misc.xml
  4. 8 0
      .idea/modules.xml
  5. 8 0
      .idea/repo_elasticAdapter.iml
  6. 135 0
      .idea/workspace.xml
  7. 21 0
      composer.json
  8. 485 0
      lib/Elastica/AbstractUpdateAction.php
  9. 100 0
      lib/Elastica/Aggregation/AbstractAggregation.php
  10. 57 0
      lib/Elastica/Aggregation/AbstractSimpleAggregation.php
  11. 98 0
      lib/Elastica/Aggregation/AbstractTermsAggregation.php
  12. 12 0
      lib/Elastica/Aggregation/Avg.php
  13. 76 0
      lib/Elastica/Aggregation/AvgBucket.php
  14. 95 0
      lib/Elastica/Aggregation/BucketScript.php
  15. 53 0
      lib/Elastica/Aggregation/BucketSelector.php
  16. 39 0
      lib/Elastica/Aggregation/Cardinality.php
  17. 23 0
      lib/Elastica/Aggregation/Children.php
  18. 85 0
      lib/Elastica/Aggregation/DateHistogram.php
  19. 23 0
      lib/Elastica/Aggregation/DateRange.php
  20. 76 0
      lib/Elastica/Aggregation/Derivative.php
  21. 12 0
      lib/Elastica/Aggregation/ExtendedStats.php
  22. 61 0
      lib/Elastica/Aggregation/Filter.php
  23. 117 0
      lib/Elastica/Aggregation/Filters.php
  24. 33 0
      lib/Elastica/Aggregation/GeoBounds.php
  25. 33 0
      lib/Elastica/Aggregation/GeoCentroid.php
  26. 104 0
      lib/Elastica/Aggregation/GeoDistance.php
  27. 69 0
      lib/Elastica/Aggregation/GeohashGrid.php
  28. 12 0
      lib/Elastica/Aggregation/GlobalAggregation.php
  29. 60 0
      lib/Elastica/Aggregation/Histogram.php
  30. 73 0
      lib/Elastica/Aggregation/IpRange.php
  31. 12 0
      lib/Elastica/Aggregation/Max.php
  32. 12 0
      lib/Elastica/Aggregation/Min.php
  33. 33 0
      lib/Elastica/Aggregation/Missing.php
  34. 33 0
      lib/Elastica/Aggregation/Nested.php
  35. 104 0
      lib/Elastica/Aggregation/Percentiles.php
  36. 59 0
      lib/Elastica/Aggregation/Range.php
  37. 50 0
      lib/Elastica/Aggregation/ReverseNested.php
  38. 94 0
      lib/Elastica/Aggregation/ScriptedMetric.php
  39. 88 0
      lib/Elastica/Aggregation/SerialDiff.php
  40. 28 0
      lib/Elastica/Aggregation/SignificantTerms.php
  41. 12 0
      lib/Elastica/Aggregation/Stats.php
  42. 76 0
      lib/Elastica/Aggregation/StatsBucket.php
  43. 12 0
      lib/Elastica/Aggregation/Sum.php
  44. 76 0
      lib/Elastica/Aggregation/SumBucket.php
  45. 36 0
      lib/Elastica/Aggregation/Terms.php
  46. 161 0
      lib/Elastica/Aggregation/TopHits.php
  47. 33 0
      lib/Elastica/Aggregation/ValueCount.php
  48. 19 0
      lib/Elastica/ArrayableInterface.php
  49. 416 0
      lib/Elastica/Bulk.php
  50. 233 0
      lib/Elastica/Bulk/Action.php
  51. 171 0
      lib/Elastica/Bulk/Action/AbstractDocument.php
  52. 11 0
      lib/Elastica/Bulk/Action/CreateDocument.php
  53. 34 0
      lib/Elastica/Bulk/Action/DeleteDocument.php
  54. 51 0
      lib/Elastica/Bulk/Action/IndexDocument.php
  55. 67 0
      lib/Elastica/Bulk/Action/UpdateDocument.php
  56. 47 0
      lib/Elastica/Bulk/Response.php
  57. 139 0
      lib/Elastica/Bulk/ResponseSet.php
  58. 834 0
      lib/Elastica/Client.php
  59. 170 0
      lib/Elastica/Cluster.php
  60. 229 0
      lib/Elastica/Cluster/Health.php
  61. 138 0
      lib/Elastica/Cluster/Health/Index.php
  62. 103 0
      lib/Elastica/Cluster/Health/Shard.php
  63. 199 0
      lib/Elastica/Cluster/Settings.php
  64. 360 0
      lib/Elastica/Connection.php
  65. 123 0
      lib/Elastica/Connection/ConnectionPool.php
  66. 52 0
      lib/Elastica/Connection/Strategy/CallbackStrategy.php
  67. 25 0
      lib/Elastica/Connection/Strategy/RoundRobin.php
  68. 31 0
      lib/Elastica/Connection/Strategy/Simple.php
  69. 46 0
      lib/Elastica/Connection/Strategy/StrategyFactory.php
  70. 18 0
      lib/Elastica/Connection/Strategy/StrategyInterface.php
  71. 339 0
      lib/Elastica/Document.php
  72. 66 0
      lib/Elastica/Exception/Bulk/Response/ActionException.php
  73. 99 0
      lib/Elastica/Exception/Bulk/ResponseException.php
  74. 7 0
      lib/Elastica/Exception/BulkException.php
  75. 12 0
      lib/Elastica/Exception/ClientException.php
  76. 51 0
      lib/Elastica/Exception/Connection/GuzzleException.php
  77. 77 0
      lib/Elastica/Exception/Connection/HttpException.php
  78. 59 0
      lib/Elastica/Exception/ConnectionException.php
  79. 14 0
      lib/Elastica/Exception/DeprecatedException.php
  80. 107 0
      lib/Elastica/Exception/ElasticsearchException.php
  81. 12 0
      lib/Elastica/Exception/ExceptionInterface.php
  82. 12 0
      lib/Elastica/Exception/InvalidException.php
  83. 10 0
      lib/Elastica/Exception/JSONParseException.php
  84. 12 0
      lib/Elastica/Exception/NotFoundException.php
  85. 14 0
      lib/Elastica/Exception/NotImplementedException.php
  86. 29 0
      lib/Elastica/Exception/PartialShardFailureException.php
  87. 12 0
      lib/Elastica/Exception/QueryBuilderException.php
  88. 69 0
      lib/Elastica/Exception/ResponseException.php
  89. 12 0
      lib/Elastica/Exception/RuntimeException.php
  90. 617 0
      lib/Elastica/Index.php
  91. 101 0
      lib/Elastica/Index/Recovery.php
  92. 385 0
      lib/Elastica/Index/Settings.php
  93. 107 0
      lib/Elastica/Index/Stats.php
  94. 120 0
      lib/Elastica/IndexTemplate.php
  95. 88 0
      lib/Elastica/JSON.php
  96. 82 0
      lib/Elastica/Log.php
  97. 60 0
      lib/Elastica/Multi/MultiBuilder.php
  98. 17 0
      lib/Elastica/Multi/MultiBuilderInterface.php
  99. 164 0
      lib/Elastica/Multi/ResultSet.php
  100. 210 0
      lib/Elastica/Multi/Search.php

+ 10 - 0
.idea/composerJson.xml

@@ -0,0 +1,10 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project version="4">
+  <component name="ComposerJsonPluginSettings">
+    <unboundedVersionInspectionSettings>
+      <excludedPackages />
+    </unboundedVersionInspectionSettings>
+    <customRepositories />
+    <composerUpdateOptions />
+  </component>
+</project>

+ 6 - 0
.idea/encodings.xml

@@ -0,0 +1,6 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project version="4">
+  <component name="Encoding" addBOMForNewFiles="with NO BOM">
+    <file url="PROJECT" charset="UTF-8" />
+  </component>
+</project>

+ 12 - 0
.idea/misc.xml

@@ -0,0 +1,12 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project version="4">
+  <component name="JavaScriptSettings">
+    <option name="languageLevel" value="ES6" />
+  </component>
+  <component name="NodePackageJsonFileManager">
+    <packageJsonPaths />
+  </component>
+  <component name="SvnConfiguration">
+    <configuration>$USER_HOME$/.subversion</configuration>
+  </component>
+</project>

+ 8 - 0
.idea/modules.xml

@@ -0,0 +1,8 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project version="4">
+  <component name="ProjectModuleManager">
+    <modules>
+      <module fileurl="file://$PROJECT_DIR$/.idea/repo_elasticAdapter.iml" filepath="$PROJECT_DIR$/.idea/repo_elasticAdapter.iml" />
+    </modules>
+  </component>
+</project>

+ 8 - 0
.idea/repo_elasticAdapter.iml

@@ -0,0 +1,8 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<module type="WEB_MODULE" version="4">
+  <component name="NewModuleRootManager">
+    <content url="file://$MODULE_DIR$" />
+    <orderEntry type="inheritedJdk" />
+    <orderEntry type="sourceFolder" forTests="false" />
+  </component>
+</module>

+ 135 - 0
.idea/workspace.xml

@@ -0,0 +1,135 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project version="4">
+  <component name="ChangeListManager">
+    <list default="true" id="4469eac4-ddd3-46ab-8494-8108d0a9d821" name="Default Changelist" comment="" />
+    <option name="EXCLUDED_CONVERTED_TO_IGNORED" value="true" />
+    <option name="SHOW_DIALOG" value="false" />
+    <option name="HIGHLIGHT_CONFLICTS" value="true" />
+    <option name="HIGHLIGHT_NON_ACTIVE_CHANGELIST" value="false" />
+    <option name="LAST_RESOLUTION" value="IGNORE" />
+  </component>
+  <component name="FileEditorManager">
+    <leaf>
+      <file pinned="false" current-in-tab="true">
+        <entry file="file://$PROJECT_DIR$/composer.json">
+          <provider selected="true" editor-type-id="text-editor">
+            <state relative-caret-position="300">
+              <caret line="20" column="1" selection-start-line="20" selection-start-column="1" selection-end-line="20" selection-end-column="1" />
+            </state>
+          </provider>
+        </entry>
+      </file>
+    </leaf>
+  </component>
+  <component name="IdeDocumentHistory">
+    <option name="CHANGED_PATHS">
+      <list>
+        <option value="$PROJECT_DIR$/composer.json" />
+      </list>
+    </option>
+  </component>
+  <component name="ProjectFrameBounds" extendedState="6">
+    <option name="y" value="23" />
+    <option name="width" value="1920" />
+    <option name="height" value="1177" />
+  </component>
+  <component name="ProjectView">
+    <navigator proportions="" version="1">
+      <foldersAlwaysOnTop value="true" />
+    </navigator>
+    <panes>
+      <pane id="ProjectPane">
+        <subPane>
+          <expand>
+            <path>
+              <item name="repo_elasticAdapter" type="b2602c69:ProjectViewProjectNode" />
+              <item name="repo_elasticAdapter" type="462c0819:PsiDirectoryNode" />
+            </path>
+            <path>
+              <item name="repo_elasticAdapter" type="b2602c69:ProjectViewProjectNode" />
+              <item name="repo_elasticAdapter" type="462c0819:PsiDirectoryNode" />
+              <item name="lib" type="462c0819:PsiDirectoryNode" />
+            </path>
+          </expand>
+          <select />
+        </subPane>
+      </pane>
+      <pane id="Scope" />
+    </panes>
+  </component>
+  <component name="PropertiesComponent">
+    <property name="WebServerToolWindowFactoryState" value="false" />
+    <property name="nodejs_interpreter_path.stuck_in_default_project" value="undefined stuck path" />
+    <property name="nodejs_npm_path_reset_for_default_project" value="true" />
+    <property name="settings.editor.selected.configurable" value="Settings.JavaScript" />
+    <property name="settings.editor.splitter.proportion" value="0.2" />
+  </component>
+  <component name="RunDashboard">
+    <option name="ruleStates">
+      <list>
+        <RuleState>
+          <option name="name" value="ConfigurationTypeDashboardGroupingRule" />
+        </RuleState>
+        <RuleState>
+          <option name="name" value="StatusDashboardGroupingRule" />
+        </RuleState>
+      </list>
+    </option>
+  </component>
+  <component name="TaskManager">
+    <task active="true" id="Default" summary="Default task">
+      <changelist id="4469eac4-ddd3-46ab-8494-8108d0a9d821" name="Default Changelist" comment="" />
+      <created>1553268852371</created>
+      <option name="number" value="Default" />
+      <option name="presentableId" value="Default" />
+      <updated>1553268852371</updated>
+      <workItem from="1553268854069" duration="293000" />
+    </task>
+    <servers />
+  </component>
+  <component name="TimeTrackingManager">
+    <option name="totallyTimeSpent" value="293000" />
+  </component>
+  <component name="ToolWindowManager">
+    <frame x="0" y="23" width="1920" height="1177" extended-state="6" />
+    <editor active="true" />
+    <layout>
+      <window_info active="true" content_ui="combo" x="347" y="283" width="247" height="908" id="Project" order="0" sideWeight="0.55668205" visible="true" weight="0.26996806" />
+      <window_info id="Structure" order="1" sideWeight="0.44331798" side_tool="true" visible="true" weight="0.26996806" />
+      <window_info id="Favorites" order="2" side_tool="true" />
+      <window_info anchor="bottom" id="Database Changes" />
+      <window_info anchor="bottom" id="Event Log" order="0" side_tool="true" />
+      <window_info anchor="bottom" id="Find" order="1" weight="0.32760474" />
+      <window_info anchor="bottom" id="Application Servers" order="2" />
+      <window_info anchor="bottom" id="Version Control" order="3" weight="0.27712137" />
+      <window_info anchor="bottom" id="Terminal" order="4" />
+      <window_info anchor="bottom" id="Message" order="5" />
+      <window_info anchor="bottom" id="TODO" order="6" weight="0.32971016" />
+      <window_info anchor="bottom" id="Run" order="7" />
+      <window_info anchor="bottom" id="Debug" order="8" weight="0.39957035" />
+      <window_info anchor="bottom" id="Cvs" order="9" weight="0.25" />
+      <window_info anchor="bottom" id="Inspection" order="10" weight="0.4" />
+      <window_info anchor="bottom" id="Messages" order="11" />
+      <window_info anchor="bottom" id="Database Console" order="12" weight="0.32975295" />
+      <window_info anchor="right" id="Database" order="0" weight="0.32960597" />
+      <window_info anchor="right" id="Commander" internal_type="SLIDING" order="1" type="SLIDING" weight="0.4" />
+      <window_info anchor="right" id="Ant Build" order="2" weight="0.25" />
+      <window_info anchor="right" content_ui="combo" id="Hierarchy" order="3" weight="0.25" />
+    </layout>
+  </component>
+  <component name="TypeScriptGeneratedFilesManager">
+    <option name="version" value="1" />
+  </component>
+  <component name="editorHistoryManager">
+    <entry file="file://$PROJECT_DIR$/lib/Elastica/Bulk/Action/IndexDocument.php">
+      <provider selected="true" editor-type-id="text-editor" />
+    </entry>
+    <entry file="file://$PROJECT_DIR$/composer.json">
+      <provider selected="true" editor-type-id="text-editor">
+        <state relative-caret-position="300">
+          <caret line="20" column="1" selection-start-line="20" selection-start-column="1" selection-end-line="20" selection-end-column="1" />
+        </state>
+      </provider>
+    </entry>
+  </component>
+</project>

+ 21 - 0
composer.json

@@ -0,0 +1,21 @@
+{
+  "name": "mooses/elastic-php-adapter",
+  "type": "library",
+  "description": "Adapter to provide elasticsearch interface",
+  "keywords": ["elasticsearch", "search"],
+  "license": "MIT",
+  "authors": [
+    { "name": "Paolo Libertini", "email": "paolo.libertini@mooses.it" }
+  ],
+  "require": {
+    "php": "^7.1",
+    "ext-hash": "*",
+    "ext-elasticsearch": "*"
+  },
+  "autoload": {
+    "psr-0": {
+      "Elastica": "lib/Elastica",
+      "Elasticsearch": "lib/Elasticsearch"
+    }
+  }
+}

+ 485 - 0
lib/Elastica/AbstractUpdateAction.php

@@ -0,0 +1,485 @@
+<?php
+
+namespace Elastica;
+
+/**
+ * Base class for things that can be sent to the update api (Document and
+ * Script).
+ *
+ * @author   Nik Everett <nik9000@gmail.com>
+ */
+class AbstractUpdateAction extends Param
+{
+    /**
+     * @var \Elastica\Document
+     */
+    protected $_upsert;
+
+    /**
+     * Sets the id of the document.
+     *
+     * @param string $id
+     *
+     * @return $this
+     */
+    public function setId($id)
+    {
+        return $this->setParam('_id', $id);
+    }
+
+    /**
+     * Returns document id.
+     *
+     * @return string|int Document id
+     */
+    public function getId()
+    {
+        return ($this->hasParam('_id')) ? $this->getParam('_id') : null;
+    }
+
+    /**
+     * @return bool
+     */
+    public function hasId()
+    {
+        return '' !== (string) $this->getId();
+    }
+
+    /**
+     * Sets the document type name.
+     *
+     * @param Type|string $type Type name
+     *
+     * @return $this
+     */
+    public function setType($type)
+    {
+        if ($type instanceof Type) {
+            $this->setIndex($type->getIndex());
+            $type = $type->getName();
+        }
+
+        return $this->setParam('_type', $type);
+    }
+
+    /**
+     * Return document type name.
+     *
+     * @throws \Elastica\Exception\InvalidException
+     *
+     * @return string Document type name
+     */
+    public function getType()
+    {
+        return $this->getParam('_type');
+    }
+
+    /**
+     * Sets the document index name.
+     *
+     * @param Index|string $index Index name
+     *
+     * @return $this
+     */
+    public function setIndex($index)
+    {
+        if ($index instanceof Index) {
+            $index = $index->getName();
+        }
+
+        return $this->setParam('_index', $index);
+    }
+
+    /**
+     * Get the document index name.
+     *
+     * @throws \Elastica\Exception\InvalidException
+     *
+     * @return string Index name
+     */
+    public function getIndex()
+    {
+        return $this->getParam('_index');
+    }
+
+    /**
+     * Sets the version of a document for use with optimistic concurrency control.
+     *
+     * @param int $version Document version
+     *
+     * @return $this
+     *
+     * @see https://www.elastic.co/blog/versioning
+     */
+    public function setVersion($version)
+    {
+        return $this->setParam('_version', (int) $version);
+    }
+
+    /**
+     * Returns document version.
+     *
+     * @return string|int Document version
+     */
+    public function getVersion()
+    {
+        return $this->getParam('_version');
+    }
+
+    /**
+     * @return bool
+     */
+    public function hasVersion()
+    {
+        return $this->hasParam('_version');
+    }
+
+    /**
+     * Sets the version_type of a document
+     * Default in ES is internal, but you can set to external to use custom versioning.
+     *
+     * @param string $versionType Document version type
+     *
+     * @return $this
+     */
+    public function setVersionType($versionType)
+    {
+        return $this->setParam('_version_type', $versionType);
+    }
+
+    /**
+     * Returns document version type.
+     *
+     * @return string|int Document version type
+     */
+    public function getVersionType()
+    {
+        return $this->getParam('_version_type');
+    }
+
+    /**
+     * @return bool
+     */
+    public function hasVersionType()
+    {
+        return $this->hasParam('_version_type');
+    }
+
+    /**
+     * Sets parent document id.
+     *
+     * @param string|int $parent Parent document id
+     *
+     * @return $this
+     *
+     * @see https://www.elastic.co/guide/en/elasticsearch/reference/current/mapping-parent-field.html
+     */
+    public function setParent($parent)
+    {
+        return $this->setParam('_parent', $parent);
+    }
+
+    /**
+     * Returns the parent document id.
+     *
+     * @return string|int Parent document id
+     */
+    public function getParent()
+    {
+        return $this->getParam('_parent');
+    }
+
+    /**
+     * @return bool
+     */
+    public function hasParent()
+    {
+        return $this->hasParam('_parent');
+    }
+
+    /**
+     * Set operation type.
+     *
+     * @param string $opType Only accept create
+     *
+     * @return $this
+     */
+    public function setOpType($opType)
+    {
+        return $this->setParam('_op_type', $opType);
+    }
+
+    /**
+     * Get operation type.
+     *
+     * @return string
+     */
+    public function getOpType()
+    {
+        return $this->getParam('_op_type');
+    }
+
+    /**
+     * @return bool
+     */
+    public function hasOpType()
+    {
+        return $this->hasParam('_op_type');
+    }
+
+    /**
+     * Set routing query param.
+     *
+     * @param string $value routing
+     *
+     * @return $this
+     */
+    public function setRouting($value)
+    {
+        return $this->setParam('_routing', $value);
+    }
+
+    /**
+     * Get routing parameter.
+     *
+     * @return string
+     */
+    public function getRouting()
+    {
+        return $this->getParam('_routing');
+    }
+
+    /**
+     * @return bool
+     */
+    public function hasRouting()
+    {
+        return $this->hasParam('_routing');
+    }
+
+    /**
+     * @param array|string $fields
+     *
+     * @return $this
+     */
+    public function setFields($fields)
+    {
+        if (is_array($fields)) {
+            $fields = implode(',', $fields);
+        }
+
+        return $this->setParam('_fields', (string) $fields);
+    }
+
+    /**
+     * @return $this
+     */
+    public function setFieldsSource()
+    {
+        return $this->setFields('_source');
+    }
+
+    /**
+     * @return string
+     */
+    public function getFields()
+    {
+        return $this->getParam('_fields');
+    }
+
+    /**
+     * @return bool
+     */
+    public function hasFields()
+    {
+        return $this->hasParam('_fields');
+    }
+
+    /**
+     * @param int $num
+     *
+     * @return $this
+     */
+    public function setRetryOnConflict($num)
+    {
+        return $this->setParam('_retry_on_conflict', (int) $num);
+    }
+
+    /**
+     * @return int
+     */
+    public function getRetryOnConflict()
+    {
+        return $this->getParam('_retry_on_conflict');
+    }
+
+    /**
+     * @return bool
+     */
+    public function hasRetryOnConflict()
+    {
+        return $this->hasParam('_retry_on_conflict');
+    }
+
+    /**
+     * @param bool $refresh
+     *
+     * @return $this
+     */
+    public function setRefresh($refresh = true)
+    {
+        return $this->setParam('_refresh', (bool) $refresh ? 'true' : 'false');
+    }
+
+    /**
+     * @return bool
+     */
+    public function getRefresh()
+    {
+        return 'true' === $this->getParam('_refresh');
+    }
+
+    /**
+     * @return bool
+     */
+    public function hasRefresh()
+    {
+        return $this->hasParam('_refresh');
+    }
+
+    /**
+     * @param string $timeout
+     *
+     * @return $this
+     */
+    public function setTimeout($timeout)
+    {
+        return $this->setParam('_timeout', $timeout);
+    }
+
+    /**
+     * @return bool
+     */
+    public function getTimeout()
+    {
+        return $this->getParam('_timeout');
+    }
+
+    /**
+     * @return bool
+     */
+    public function hasTimeout()
+    {
+        return $this->hasParam('_timeout');
+    }
+
+    /**
+     * @param string $timeout
+     *
+     * @return $this
+     */
+    public function setConsistency($timeout)
+    {
+        return $this->setParam('_consistency', $timeout);
+    }
+
+    /**
+     * @return string
+     */
+    public function getConsistency()
+    {
+        return $this->getParam('_consistency');
+    }
+
+    /**
+     * @return bool
+     */
+    public function hasConsistency()
+    {
+        return $this->hasParam('_consistency');
+    }
+
+    /**
+     * @param string $timeout
+     *
+     * @return $this
+     */
+    public function setReplication($timeout)
+    {
+        return $this->setParam('_replication', $timeout);
+    }
+
+    /**
+     * @return string
+     */
+    public function getReplication()
+    {
+        return $this->getParam('_replication');
+    }
+
+    /**
+     * @return bool
+     */
+    public function hasReplication()
+    {
+        return $this->hasParam('_replication');
+    }
+
+    /**
+     * @param \Elastica\Document|array $data
+     *
+     * @return $this
+     */
+    public function setUpsert($data)
+    {
+        $document = Document::create($data);
+        $this->_upsert = $document;
+
+        return $this;
+    }
+
+    /**
+     * @return \Elastica\Document
+     */
+    public function getUpsert()
+    {
+        return $this->_upsert;
+    }
+
+    /**
+     * @return bool
+     */
+    public function hasUpsert()
+    {
+        return null !== $this->_upsert;
+    }
+
+    /**
+     * @param array $fields         if empty array all options will be returned, field names can be either with underscored either without, i.e. _percolate, routing
+     * @param bool  $withUnderscore should option keys contain underscore prefix
+     *
+     * @return array
+     */
+    public function getOptions(array $fields = [], $withUnderscore = false)
+    {
+        if (!empty($fields)) {
+            $data = [];
+            foreach ($fields as $field) {
+                $key = '_'.ltrim($field, '_');
+                if ($this->hasParam($key) && '' !== (string) $this->getParam($key)) {
+                    $data[$key] = $this->getParam($key);
+                }
+            }
+        } else {
+            $data = $this->getParams();
+        }
+        if (!$withUnderscore) {
+            foreach ($data as $key => $value) {
+                $data[ltrim($key, '_')] = $value;
+                unset($data[$key]);
+            }
+        }
+
+        return $data;
+    }
+}

+ 100 - 0
lib/Elastica/Aggregation/AbstractAggregation.php

@@ -0,0 +1,100 @@
+<?php
+
+namespace Elastica\Aggregation;
+
+use Elastica\Exception\InvalidException;
+use Elastica\NameableInterface;
+use Elastica\Param;
+
+abstract class AbstractAggregation extends Param implements NameableInterface
+{
+    /**
+     * @var string The name of this aggregation
+     */
+    protected $_name;
+
+    /**
+     * @var array Subaggregations belonging to this aggregation
+     */
+    protected $_aggs = [];
+
+    /**
+     * @param string $name the name of this aggregation
+     */
+    public function __construct($name)
+    {
+        $this->setName($name);
+    }
+
+    /**
+     * Set the name of this aggregation.
+     *
+     * @param string $name
+     *
+     * @return $this
+     */
+    public function setName($name)
+    {
+        $this->_name = $name;
+
+        return $this;
+    }
+
+    /**
+     * Retrieve the name of this aggregation.
+     *
+     * @return string
+     */
+    public function getName()
+    {
+        return $this->_name;
+    }
+
+    /**
+     * Retrieve all subaggregations belonging to this aggregation.
+     *
+     * @return array
+     */
+    public function getAggs()
+    {
+        return $this->_aggs;
+    }
+
+    /**
+     * Add a sub-aggregation.
+     *
+     * @param AbstractAggregation $aggregation
+     *
+     * @throws \Elastica\Exception\InvalidException
+     *
+     * @return $this
+     */
+    public function addAggregation(AbstractAggregation $aggregation)
+    {
+        if ($aggregation instanceof GlobalAggregation) {
+            throw new InvalidException('Global aggregators can only be placed as top level aggregators');
+        }
+
+        $this->_aggs[] = $aggregation;
+
+        return $this;
+    }
+
+    /**
+     * @return array
+     */
+    public function toArray()
+    {
+        $array = parent::toArray();
+
+        if (array_key_exists('global_aggregation', $array)) {
+            // compensate for class name GlobalAggregation
+            $array = ['global' => new \stdClass()];
+        }
+        if (sizeof($this->_aggs)) {
+            $array['aggs'] = $this->_convertArrayable($this->_aggs);
+        }
+
+        return $array;
+    }
+}

+ 57 - 0
lib/Elastica/Aggregation/AbstractSimpleAggregation.php

@@ -0,0 +1,57 @@
+<?php
+
+namespace Elastica\Aggregation;
+
+use Elastica\Exception\InvalidException;
+
+abstract class AbstractSimpleAggregation extends AbstractAggregation
+{
+    /**
+     * Set the field for this aggregation.
+     *
+     * @param string $field the name of the document field on which to perform this aggregation
+     *
+     * @return $this
+     */
+    public function setField($field)
+    {
+        return $this->setParam('field', $field);
+    }
+
+    /**
+     * Set a script for this aggregation.
+     *
+     * @param string|\Elastica\Script\AbstractScript $script
+     *
+     * @return $this
+     */
+    public function setScript($script)
+    {
+        return $this->setParam('script', $script);
+    }
+
+    /**
+     * {@inheritdoc}
+     */
+    public function toArray()
+    {
+        if (!$this->hasParam('field') && !$this->hasParam('script')) {
+            throw new InvalidException(
+                'Either the field param or the script param should be set'
+            );
+        }
+        $array = parent::toArray();
+
+        $baseName = $this->_getBaseName();
+
+        if (isset($array[$baseName]['script']) && is_array($array[$baseName]['script'])) {
+            $script = $array[$baseName]['script'];
+
+            unset($array[$baseName]['script']);
+
+            $array[$baseName] = array_merge($array[$baseName], $script);
+        }
+
+        return $array;
+    }
+}

+ 98 - 0
lib/Elastica/Aggregation/AbstractTermsAggregation.php

@@ -0,0 +1,98 @@
+<?php
+
+namespace Elastica\Aggregation;
+
+/**
+ * Class AbstractTermsAggregation.
+ */
+abstract class AbstractTermsAggregation extends AbstractSimpleAggregation
+{
+    /**
+     * Set the minimum number of documents in which a term must appear in order to be returned in a bucket.
+     *
+     * @param int $count
+     *
+     * @return $this
+     */
+    public function setMinimumDocumentCount($count)
+    {
+        return $this->setParam('min_doc_count', $count);
+    }
+
+    /**
+     * Filter documents to include based on a regular expression.
+     *
+     * @param string $pattern a regular expression
+     * @param string $flags   Java Pattern flags
+     *
+     * @return $this
+     */
+    public function setInclude($pattern, $flags = null)
+    {
+        if (is_null($flags)) {
+            return $this->setParam('include', $pattern);
+        }
+
+        return $this->setParam('include', [
+            'pattern' => $pattern,
+            'flags' => $flags,
+        ]);
+    }
+
+    /**
+     * Filter documents to exclude based on a regular expression.
+     *
+     * @param string $pattern a regular expression
+     * @param string $flags   Java Pattern flags
+     *
+     * @return $this
+     */
+    public function setExclude($pattern, $flags = null)
+    {
+        if (is_null($flags)) {
+            return $this->setParam('exclude', $pattern);
+        }
+
+        return $this->setParam('exclude', [
+            'pattern' => $pattern,
+            'flags' => $flags,
+        ]);
+    }
+
+    /**
+     * Sets the amount of terms to be returned.
+     *
+     * @param int $size the amount of terms to be returned
+     *
+     * @return $this
+     */
+    public function setSize($size)
+    {
+        return $this->setParam('size', $size);
+    }
+
+    /**
+     * Sets how many terms the coordinating node will request from each shard.
+     *
+     * @param int $shard_size the amount of terms to be returned
+     *
+     * @return $this
+     */
+    public function setShardSize($shard_size)
+    {
+        return $this->setParam('shard_size', $shard_size);
+    }
+
+    /**
+     * Instruct Elasticsearch to use direct field data or ordinals of the field values to execute this aggregation.
+     * The execution hint will be ignored if it is not applicable.
+     *
+     * @param string $hint map or ordinals
+     *
+     * @return $this
+     */
+    public function setExecutionHint($hint)
+    {
+        return $this->setParam('execution_hint', $hint);
+    }
+}

+ 12 - 0
lib/Elastica/Aggregation/Avg.php

@@ -0,0 +1,12 @@
+<?php
+
+namespace Elastica\Aggregation;
+
+/**
+ * Class Avg.
+ *
+ * @see https://www.elastic.co/guide/en/elasticsearch/reference/current/search-aggregations-metrics-avg-aggregation.html
+ */
+class Avg extends AbstractSimpleAggregation
+{
+}

+ 76 - 0
lib/Elastica/Aggregation/AvgBucket.php

@@ -0,0 +1,76 @@
+<?php
+
+namespace Elastica\Aggregation;
+
+use Elastica\Exception\InvalidException;
+
+/**
+ * Class AvgBucket.
+ *
+ * @see https://www.elastic.co/guide/en/elasticsearch/reference/current/search-aggregations-pipeline-avg-bucket-aggregation.html
+ */
+class AvgBucket extends AbstractAggregation
+{
+    /**
+     * @param string      $name
+     * @param string|null $bucketsPath
+     */
+    public function __construct($name, $bucketsPath = null)
+    {
+        parent::__construct($name);
+
+        if (null !== $bucketsPath) {
+            $this->setBucketsPath($bucketsPath);
+        }
+    }
+
+    /**
+     * Set the buckets_path for this aggregation.
+     *
+     * @param string $bucketsPath
+     *
+     * @return $this
+     */
+    public function setBucketsPath($bucketsPath)
+    {
+        return $this->setParam('buckets_path', $bucketsPath);
+    }
+
+    /**
+     * Set the gap policy for this aggregation.
+     *
+     * @param string $gapPolicy
+     *
+     * @return $this
+     */
+    public function setGapPolicy($gapPolicy)
+    {
+        return $this->setParam('gap_policy', $gapPolicy);
+    }
+
+    /**
+     * Set the format for this aggregation.
+     *
+     * @param string $format
+     *
+     * @return $this
+     */
+    public function setFormat($format)
+    {
+        return $this->setParam('format', $format);
+    }
+
+    /**
+     * @throws InvalidException If buckets path or script is not set
+     *
+     * @return array
+     */
+    public function toArray()
+    {
+        if (!$this->hasParam('buckets_path')) {
+            throw new InvalidException('Buckets path is required');
+        }
+
+        return parent::toArray();
+    }
+}

+ 95 - 0
lib/Elastica/Aggregation/BucketScript.php

@@ -0,0 +1,95 @@
+<?php
+
+namespace Elastica\Aggregation;
+
+use Elastica\Exception\InvalidException;
+
+/**
+ * Class BucketScript.
+ *
+ * @see https://www.elastic.co/guide/en/elasticsearch/reference/current/search-aggregations-pipeline-bucket-script-aggregation.html
+ */
+class BucketScript extends AbstractAggregation
+{
+    /**
+     * @param string      $name
+     * @param array|null  $bucketsPath
+     * @param string|null $script
+     */
+    public function __construct($name, $bucketsPath = null, $script = null)
+    {
+        parent::__construct($name);
+
+        if (null !== $bucketsPath) {
+            $this->setBucketsPath($bucketsPath);
+        }
+
+        if (null !== $script) {
+            $this->setScript($script);
+        }
+    }
+
+    /**
+     * Set the buckets_path for this aggregation.
+     *
+     * @param array $bucketsPath
+     *
+     * @return $this
+     */
+    public function setBucketsPath($bucketsPath)
+    {
+        return $this->setParam('buckets_path', $bucketsPath);
+    }
+
+    /**
+     * Set the script for this aggregation.
+     *
+     * @param string $script
+     *
+     * @return $this
+     */
+    public function setScript($script)
+    {
+        return $this->setParam('script', $script);
+    }
+
+    /**
+     * Set the gap policy for this aggregation.
+     *
+     * @param string $gapPolicy
+     *
+     * @return $this
+     */
+    public function setGapPolicy($gapPolicy)
+    {
+        return $this->setParam('gap_policy', $gapPolicy);
+    }
+
+    /**
+     * Set the format for this aggregation.
+     *
+     * @param string $format
+     *
+     * @return $this
+     */
+    public function setFormat($format)
+    {
+        return $this->setParam('format', $format);
+    }
+
+    /**
+     * @throws InvalidException If buckets path or script is not set
+     *
+     * @return array
+     */
+    public function toArray()
+    {
+        if (!$this->hasParam('buckets_path')) {
+            throw new InvalidException('Buckets path is required');
+        } elseif (!$this->hasParam('script')) {
+            throw new InvalidException('Script parameter is required');
+        }
+
+        return parent::toArray();
+    }
+}

+ 53 - 0
lib/Elastica/Aggregation/BucketSelector.php

@@ -0,0 +1,53 @@
+<?php
+
+namespace Elastica\Aggregation;
+
+/**
+ * Class BucketSelector.
+ *
+ * @see https://www.elastic.co/guide/en/elasticsearch/reference/current/search-aggregations-pipeline-bucket-selector-aggregation.html
+ */
+class BucketSelector extends AbstractSimpleAggregation
+{
+    /**
+     * @param string      $name
+     * @param array|null  $bucketsPath
+     * @param string|null $script
+     */
+    public function __construct(string $name, array $bucketsPath = null, string $script = null)
+    {
+        parent::__construct($name);
+
+        if (null !== $bucketsPath) {
+            $this->setBucketsPath($bucketsPath);
+        }
+
+        if (null !== $script) {
+            $this->setScript($script);
+        }
+    }
+
+    /**
+     * Set the buckets_path for this aggregation.
+     *
+     * @param array $bucketsPath
+     *
+     * @return $this
+     */
+    public function setBucketsPath($bucketsPath)
+    {
+        return $this->setParam('buckets_path', $bucketsPath);
+    }
+
+    /**
+     * Set the gap policy for this aggregation.
+     *
+     * @param string $gapPolicy
+     *
+     * @return $this
+     */
+    public function setGapPolicy(string $gapPolicy = 'skip')
+    {
+        return $this->setParam('gap_policy', $gapPolicy);
+    }
+}

+ 39 - 0
lib/Elastica/Aggregation/Cardinality.php

@@ -0,0 +1,39 @@
+<?php
+
+namespace Elastica\Aggregation;
+
+/**
+ * Class Cardinality.
+ *
+ * @see https://www.elastic.co/guide/en/elasticsearch/reference/current/search-aggregations-metrics-cardinality-aggregation.html
+ */
+class Cardinality extends AbstractSimpleAggregation
+{
+    /**
+     * @param int $precisionThreshold
+     *
+     * @return $this
+     */
+    public function setPrecisionThreshold($precisionThreshold)
+    {
+        if (!is_int($precisionThreshold)) {
+            throw new \InvalidArgumentException('precision_threshold only supports integer values');
+        }
+
+        return $this->setParam('precision_threshold', $precisionThreshold);
+    }
+
+    /**
+     * @param bool $rehash
+     *
+     * @return $this
+     */
+    public function setRehash($rehash)
+    {
+        if (!is_bool($rehash)) {
+            throw new \InvalidArgumentException('rehash only supports boolean values');
+        }
+
+        return $this->setParam('rehash', $rehash);
+    }
+}

+ 23 - 0
lib/Elastica/Aggregation/Children.php

@@ -0,0 +1,23 @@
+<?php
+
+namespace Elastica\Aggregation;
+
+/**
+ * Class Children.
+ *
+ * @see https://www.elastic.co/guide/en/elasticsearch/reference/1.7/search-aggregations-bucket-children-aggregation.html
+ */
+class Children extends AbstractAggregation
+{
+    /**
+     * Set the type for this aggregation.
+     *
+     * @param string $field the child type the buckets in the parent space should be mapped to
+     *
+     * @return $this
+     */
+    public function setType($type)
+    {
+        return $this->setParam('type', $type);
+    }
+}

+ 85 - 0
lib/Elastica/Aggregation/DateHistogram.php

@@ -0,0 +1,85 @@
+<?php
+
+namespace Elastica\Aggregation;
+
+/**
+ * Class DateHistogram.
+ *
+ * @see https://www.elastic.co/guide/en/elasticsearch/reference/current/search-aggregations-bucket-datehistogram-aggregation.html
+ */
+class DateHistogram extends Histogram
+{
+    /**
+     * Set time_zone option.
+     *
+     * @param  string
+     *
+     * @return $this
+     */
+    public function setTimezone($timezone)
+    {
+        return $this->setParam('time_zone', $timezone);
+    }
+
+    /**
+     * Adjust for granularity of date data.
+     *
+     * @param int $factor set to 1000 if date is stored in seconds rather than milliseconds
+     *
+     * @return $this
+     */
+    public function setFactor($factor)
+    {
+        return $this->setParam('factor', $factor);
+    }
+
+    /**
+     * Set offset option.
+     *
+     * @param string
+     *
+     * @return $this
+     */
+    public function setOffset($offset)
+    {
+        return $this->setParam('offset', $offset);
+    }
+
+    /**
+     * Set the format for returned bucket key_as_string values.
+     *
+     * @see https://www.elastic.co/guide/en/elasticsearch/reference/master/search-aggregations-bucket-daterange-aggregation.html#date-format-pattern
+     *
+     * @param string $format see link for formatting options
+     *
+     * @return $this
+     */
+    public function setFormat($format)
+    {
+        return $this->setParam('format', $format);
+    }
+
+    /**
+     * Set extended bounds option.
+     *
+     * @see https://www.elastic.co/guide/en/elasticsearch/reference/current/search-aggregations-bucket-histogram-aggregation.html#search-aggregations-bucket-histogram-aggregation-extended-bounds
+     *
+     * @param string $min see link for formatting options
+     * @param string $max see link for formatting options
+     *
+     * @return $this
+     */
+    public function setExtendedBounds($min = '', $max = '')
+    {
+        $bounds = [];
+        $bounds['min'] = $min;
+        $bounds['max'] = $max;
+        // switch if min is higher then max
+        if (strtotime($min) > strtotime($max)) {
+            $bounds['min'] = $max;
+            $bounds['max'] = $min;
+        }
+
+        return $this->setParam('extended_bounds', $bounds);
+    }
+}

+ 23 - 0
lib/Elastica/Aggregation/DateRange.php

@@ -0,0 +1,23 @@
+<?php
+
+namespace Elastica\Aggregation;
+
+/**
+ * Class DateRange.
+ *
+ * @see https://www.elastic.co/guide/en/elasticsearch/reference/current/search-aggregations-bucket-daterange-aggregation.html
+ */
+class DateRange extends Range
+{
+    /**
+     * Set the formatting for the returned date values.
+     *
+     * @param string $format see documentation for formatting options
+     *
+     * @return $this
+     */
+    public function setFormat($format)
+    {
+        return $this->setParam('format', $format);
+    }
+}

+ 76 - 0
lib/Elastica/Aggregation/Derivative.php

@@ -0,0 +1,76 @@
+<?php
+
+namespace Elastica\Aggregation;
+
+use Elastica\Exception\InvalidException;
+
+/**
+ * Class Derivative.
+ *
+ * @see https://www.elastic.co/guide/en/elasticsearch/reference/current/search-aggregations-pipeline-derivative-aggregation.html
+ */
+class Derivative extends AbstractAggregation
+{
+    /**
+     * @param string      $name
+     * @param string|null $bucketsPath
+     */
+    public function __construct(string $name, string $bucketsPath = null)
+    {
+        parent::__construct($name);
+
+        if (null !== $bucketsPath) {
+            $this->setBucketsPath($bucketsPath);
+        }
+    }
+
+    /**
+     * Set the buckets_path for this aggregation.
+     *
+     * @param string $bucketsPath
+     *
+     * @return $this
+     */
+    public function setBucketsPath(string $bucketsPath)
+    {
+        return $this->setParam('buckets_path', $bucketsPath);
+    }
+
+    /**
+     * Set the gap policy for this aggregation.
+     *
+     * @param string $gapPolicy
+     *
+     * @return $this
+     */
+    public function setGapPolicy(string $gapPolicy = 'skip')
+    {
+        return $this->setParam('gap_policy', $gapPolicy);
+    }
+
+    /**
+     * Set the format for this aggregation.
+     *
+     * @param string $format
+     *
+     * @return $this
+     */
+    public function setFormat(string $format)
+    {
+        return $this->setParam('format', $format);
+    }
+
+    /**
+     * @throws InvalidException If buckets path or script is not set
+     *
+     * @return array
+     */
+    public function toArray(): array
+    {
+        if (!$this->hasParam('buckets_path')) {
+            throw new InvalidException('Buckets path is required');
+        }
+
+        return parent::toArray();
+    }
+}

+ 12 - 0
lib/Elastica/Aggregation/ExtendedStats.php

@@ -0,0 +1,12 @@
+<?php
+
+namespace Elastica\Aggregation;
+
+/**
+ * Class ExtendedStats.
+ *
+ * @see https://www.elastic.co/guide/en/elasticsearch/reference/current/search-aggregations-metrics-extendedstats-aggregation.html
+ */
+class ExtendedStats extends AbstractSimpleAggregation
+{
+}

+ 61 - 0
lib/Elastica/Aggregation/Filter.php

@@ -0,0 +1,61 @@
+<?php
+
+namespace Elastica\Aggregation;
+
+use Elastica\Exception\InvalidException;
+use Elastica\Query\AbstractQuery;
+
+/**
+ * Class Filter.
+ *
+ * @see https://www.elastic.co/guide/en/elasticsearch/reference/current/search-aggregations-bucket-filter-aggregation.html
+ */
+class Filter extends AbstractAggregation
+{
+    /**
+     * @param string        $name
+     * @param AbstractQuery $filter
+     */
+    public function __construct($name, AbstractQuery $filter = null)
+    {
+        parent::__construct($name);
+
+        if (null !== $filter) {
+            $this->setFilter($filter);
+        }
+    }
+
+    /**
+     * Set the filter for this aggregation.
+     *
+     * @param AbstractQuery $filter
+     *
+     * @return $this
+     */
+    public function setFilter(AbstractQuery $filter)
+    {
+        return $this->setParam('filter', $filter);
+    }
+
+    /**
+     * @throws \Elastica\Exception\InvalidException If filter is not set
+     *
+     * @return array
+     */
+    public function toArray()
+    {
+        if (!$this->hasParam('filter')) {
+            throw new InvalidException('Filter is required');
+        }
+
+        $array = [
+            'filter' => $this->getParam('filter')->toArray(),
+        ];
+
+        if ($this->_aggs) {
+            $array['aggs'] = $this->_convertArrayable($this->_aggs);
+        }
+
+        return $array;
+    }
+}

+ 117 - 0
lib/Elastica/Aggregation/Filters.php

@@ -0,0 +1,117 @@
+<?php
+
+namespace Elastica\Aggregation;
+
+use Elastica\Exception\InvalidException;
+use Elastica\Query\AbstractQuery;
+
+/**
+ * Class Filters.
+ *
+ * @see https://www.elastic.co/guide/en/elasticsearch/reference/current/search-aggregations-bucket-filters-aggregation.html
+ */
+class Filters extends AbstractAggregation
+{
+    const NAMED_TYPE = 1;
+    const ANONYMOUS_TYPE = 2;
+
+    /**
+     * @var int Type of bucket keys - named, or anonymous
+     */
+    private $_type;
+
+    /**
+     * Add a filter.
+     *
+     * If a name is given, it will be added as a key, otherwise considered as an anonymous filter
+     *
+     * @param AbstractQuery $filter
+     * @param string        $name
+     *
+     * @return $this
+     */
+    public function addFilter(AbstractQuery $filter, $name = null)
+    {
+        if (null !== $name && !is_string($name)) {
+            throw new InvalidException('Name must be a string');
+        }
+
+        $filterArray = [];
+
+        $type = self::NAMED_TYPE;
+
+        if (null === $name) {
+            $filterArray[] = $filter;
+            $type = self::ANONYMOUS_TYPE;
+        } else {
+            $filterArray[$name] = $filter;
+        }
+
+        if ($this->hasParam('filters')
+            && count($this->getParam('filters'))
+            && $this->_type !== $type
+        ) {
+            throw new InvalidException('Mix named and anonymous keys are not allowed');
+        }
+
+        $this->_type = $type;
+
+        return $this->addParam('filters', $filterArray);
+    }
+
+    /**
+     * @param bool $otherBucket
+     *
+     * @return $this
+     */
+    public function setOtherBucket($otherBucket)
+    {
+        if (!is_bool($otherBucket)) {
+            throw new \InvalidArgumentException('other_bucket only supports boolean values');
+        }
+
+        return $this->setParam('other_bucket', $otherBucket);
+    }
+
+    /**
+     * @param string $otherBucketKey
+     *
+     * @return $this
+     */
+    public function setOtherBucketKey($otherBucketKey)
+    {
+        return $this->setParam('other_bucket_key', $otherBucketKey);
+    }
+
+    /**
+     * @return array
+     */
+    public function toArray()
+    {
+        $array = [];
+        $filters = $this->getParam('filters');
+
+        foreach ($filters as $filter) {
+            if (self::NAMED_TYPE === $this->_type) {
+                $key = key($filter);
+                $array['filters']['filters'][$key] = current($filter)->toArray();
+            } else {
+                $array['filters']['filters'][] = current($filter)->toArray();
+            }
+        }
+
+        if ($this->hasParam('other_bucket')) {
+            $array['filters']['other_bucket'] = $this->getParam('other_bucket');
+        }
+
+        if ($this->hasParam('other_bucket_key')) {
+            $array['filters']['other_bucket_key'] = $this->getParam('other_bucket_key');
+        }
+
+        if ($this->_aggs) {
+            $array['aggs'] = $this->_convertArrayable($this->_aggs);
+        }
+
+        return $array;
+    }
+}

+ 33 - 0
lib/Elastica/Aggregation/GeoBounds.php

@@ -0,0 +1,33 @@
+<?php
+
+namespace Elastica\Aggregation;
+
+/**
+ * A metric aggregation that computes the bounding box containing all geo_point values for a field.
+ *
+ * @see https://www.elastic.co/guide/en/elasticsearch/reference/current/search-aggregations-metrics-geobounds-aggregation.html
+ */
+class GeoBounds extends AbstractAggregation
+{
+    /**
+     * @param string $name  the name of this aggregation
+     * @param string $field the field on which to perform this aggregation
+     */
+    public function __construct($name, $field)
+    {
+        parent::__construct($name);
+        $this->setField($field);
+    }
+
+    /**
+     * Set the field for this aggregation.
+     *
+     * @param string $field the name of the document field on which to perform this aggregation
+     *
+     * @return $this
+     */
+    public function setField($field)
+    {
+        return $this->setParam('field', $field);
+    }
+}

+ 33 - 0
lib/Elastica/Aggregation/GeoCentroid.php

@@ -0,0 +1,33 @@
+<?php
+
+namespace Elastica\Aggregation;
+
+/**
+ * Class GeoCentroid.
+ *
+ * @see https://www.elastic.co/guide/en/elasticsearch/reference/current/search-aggregations-metrics-geocentroid-aggregation.html
+ */
+class GeoCentroid extends AbstractAggregation
+{
+    /**
+     * @param string $name  the name of this aggregation
+     * @param string $field the field on which to perform this aggregation
+     */
+    public function __construct($name, $field)
+    {
+        parent::__construct($name);
+        $this->setField($field);
+    }
+
+    /**
+     * Set the field for this aggregation.
+     *
+     * @param string $field the name of the document field on which to perform this aggregation
+     *
+     * @return $this
+     */
+    public function setField($field)
+    {
+        return $this->setParam('field', $field);
+    }
+}

+ 104 - 0
lib/Elastica/Aggregation/GeoDistance.php

@@ -0,0 +1,104 @@
+<?php
+
+namespace Elastica\Aggregation;
+
+use Elastica\Exception\InvalidException;
+
+/**
+ * Class GeoDistance.
+ *
+ * @see https://www.elastic.co/guide/en/elasticsearch/reference/current/search-aggregations-bucket-geodistance-aggregation.html
+ */
+class GeoDistance extends AbstractAggregation
+{
+    const DISTANCE_TYPE_ARC = 'arc';
+    const DISTANCE_TYPE_PLANE = 'plane';
+
+    /**
+     * @param string       $name   the name if this aggregation
+     * @param string       $field  the field on which to perform this aggregation
+     * @param string|array $origin the point from which distances will be calculated
+     */
+    public function __construct($name, $field, $origin)
+    {
+        parent::__construct($name);
+        $this->setField($field)->setOrigin($origin);
+    }
+
+    /**
+     * Set the field for this aggregation.
+     *
+     * @param string $field the name of the document field on which to perform this aggregation
+     *
+     * @return $this
+     */
+    public function setField($field)
+    {
+        return $this->setParam('field', $field);
+    }
+
+    /**
+     * Set the origin point from which distances will be calculated.
+     *
+     * @param string|array $origin valid formats are array("lat" => 52.3760, "lon" => 4.894), "52.3760, 4.894", and array(4.894, 52.3760)
+     *
+     * @return $this
+     */
+    public function setOrigin($origin)
+    {
+        return $this->setParam('origin', $origin);
+    }
+
+    /**
+     * Add a distance range to this aggregation.
+     *
+     * @param int $fromValue a distance
+     * @param int $toValue   a distance
+     *
+     * @throws \Elastica\Exception\InvalidException
+     *
+     * @return $this
+     */
+    public function addRange($fromValue = null, $toValue = null)
+    {
+        if (is_null($fromValue) && is_null($toValue)) {
+            throw new InvalidException('Either fromValue or toValue must be set. Both cannot be null.');
+        }
+
+        $range = [];
+
+        if (!is_null($fromValue)) {
+            $range['from'] = $fromValue;
+        }
+
+        if (!is_null($toValue)) {
+            $range['to'] = $toValue;
+        }
+
+        return $this->addParam('ranges', $range);
+    }
+
+    /**
+     * Set the unit of distance measure for  this aggregation.
+     *
+     * @param string $unit defaults to km
+     *
+     * @return $this
+     */
+    public function setUnit($unit)
+    {
+        return $this->setParam('unit', $unit);
+    }
+
+    /**
+     * Set the method by which distances will be calculated.
+     *
+     * @param string $distanceType see DISTANCE_TYPE_* constants for options. Defaults to arc.
+     *
+     * @return $this
+     */
+    public function setDistanceType($distanceType)
+    {
+        return $this->setParam('distance_type', $distanceType);
+    }
+}

+ 69 - 0
lib/Elastica/Aggregation/GeohashGrid.php

@@ -0,0 +1,69 @@
+<?php
+
+namespace Elastica\Aggregation;
+
+/**
+ * Class GeohashGrid.
+ *
+ * @see https://www.elastic.co/guide/en/elasticsearch/reference/current/search-aggregations-bucket-geohashgrid-aggregation.html
+ */
+class GeohashGrid extends AbstractAggregation
+{
+    /**
+     * @param string $name  the name of this aggregation
+     * @param string $field the field on which to perform this aggregation
+     */
+    public function __construct($name, $field)
+    {
+        parent::__construct($name);
+        $this->setField($field);
+    }
+
+    /**
+     * Set the field for this aggregation.
+     *
+     * @param string $field the name of the document field on which to perform this aggregation
+     *
+     * @return $this
+     */
+    public function setField($field)
+    {
+        return $this->setParam('field', $field);
+    }
+
+    /**
+     * Set the precision for this aggregation.
+     *
+     * @param int $precision an integer between 1 and 12, inclusive. Defaults to 5.
+     *
+     * @return $this
+     */
+    public function setPrecision($precision)
+    {
+        return $this->setParam('precision', $precision);
+    }
+
+    /**
+     * Set the maximum number of buckets to return.
+     *
+     * @param int $size defaults to 10,000
+     *
+     * @return $this
+     */
+    public function setSize($size)
+    {
+        return $this->setParam('size', $size);
+    }
+
+    /**
+     * Set the number of results returned from each shard.
+     *
+     * @param int $shardSize
+     *
+     * @return $this
+     */
+    public function setShardSize($shardSize)
+    {
+        return $this->setParam('shard_size', $shardSize);
+    }
+}

+ 12 - 0
lib/Elastica/Aggregation/GlobalAggregation.php

@@ -0,0 +1,12 @@
+<?php
+
+namespace Elastica\Aggregation;
+
+/**
+ * Class GlobalAggregation.
+ *
+ * @see https://www.elastic.co/guide/en/elasticsearch/reference/current/search-aggregations-bucket-global-aggregation.html
+ */
+class GlobalAggregation extends AbstractAggregation
+{
+}

+ 60 - 0
lib/Elastica/Aggregation/Histogram.php

@@ -0,0 +1,60 @@
+<?php
+
+namespace Elastica\Aggregation;
+
+/**
+ * Class Histogram.
+ *
+ * @see https://www.elastic.co/guide/en/elasticsearch/reference/current/search-aggregations-bucket-histogram-aggregation.html
+ */
+class Histogram extends AbstractSimpleAggregation
+{
+    /**
+     * @param string     $name     the name of this aggregation
+     * @param string     $field    the name of the field on which to perform the aggregation
+     * @param int|string $interval the interval by which documents will be bucketed
+     */
+    public function __construct($name, $field, $interval)
+    {
+        parent::__construct($name);
+        $this->setField($field);
+        $this->setInterval($interval);
+    }
+
+    /**
+     * Set the interval by which documents will be bucketed.
+     *
+     * @param int $interval
+     *
+     * @return $this
+     */
+    public function setInterval($interval)
+    {
+        return $this->setParam('interval', $interval);
+    }
+
+    /**
+     * Set the bucket sort order.
+     *
+     * @param string $order     "_count", "_term", or the name of a sub-aggregation or sub-aggregation response field
+     * @param string $direction "asc" or "desc"
+     *
+     * @return $this
+     */
+    public function setOrder($order, $direction)
+    {
+        return $this->setParam('order', [$order => $direction]);
+    }
+
+    /**
+     * Set the minimum number of documents which must fall into a bucket in order for the bucket to be returned.
+     *
+     * @param int $count set to 0 to include empty buckets
+     *
+     * @return $this
+     */
+    public function setMinimumDocumentCount($count)
+    {
+        return $this->setParam('min_doc_count', $count);
+    }
+}

+ 73 - 0
lib/Elastica/Aggregation/IpRange.php

@@ -0,0 +1,73 @@
+<?php
+
+namespace Elastica\Aggregation;
+
+use Elastica\Exception\InvalidException;
+
+/**
+ * Class IpRange.
+ *
+ * @see https://www.elastic.co/guide/en/elasticsearch/reference/current/search-aggregations-bucket-iprange-aggregation.html
+ */
+class IpRange extends AbstractAggregation
+{
+    /**
+     * @param string $name  the name of this aggregation
+     * @param string $field the field on which to perform this aggregation
+     */
+    public function __construct($name, $field)
+    {
+        parent::__construct($name);
+        $this->setField($field);
+    }
+
+    /**
+     * Set the field for this aggregation.
+     *
+     * @param string $field the name of the document field on which to perform this aggregation
+     *
+     * @return $this
+     */
+    public function setField($field)
+    {
+        return $this->setParam('field', $field);
+    }
+
+    /**
+     * Add an ip range to this aggregation.
+     *
+     * @param string $fromValue a valid ipv4 address. Low end of this range, exclusive (greater than)
+     * @param string $toValue   a valid ipv4 address. High end of this range, exclusive (less than)
+     *
+     * @throws \Elastica\Exception\InvalidException
+     *
+     * @return $this
+     */
+    public function addRange($fromValue = null, $toValue = null)
+    {
+        if (is_null($fromValue) && is_null($toValue)) {
+            throw new InvalidException('Either fromValue or toValue must be set. Both cannot be null.');
+        }
+        $range = [];
+        if (!is_null($fromValue)) {
+            $range['from'] = $fromValue;
+        }
+        if (!is_null($toValue)) {
+            $range['to'] = $toValue;
+        }
+
+        return $this->addParam('ranges', $range);
+    }
+
+    /**
+     * Add an ip range in the form of a CIDR mask.
+     *
+     * @param string $mask a valid CIDR mask
+     *
+     * @return $this
+     */
+    public function addMaskRange($mask)
+    {
+        return $this->addParam('ranges', ['mask' => $mask]);
+    }
+}

+ 12 - 0
lib/Elastica/Aggregation/Max.php

@@ -0,0 +1,12 @@
+<?php
+
+namespace Elastica\Aggregation;
+
+/**
+ * Class Max.
+ *
+ * @see https://www.elastic.co/guide/en/elasticsearch/reference/current/search-aggregations-metrics-max-aggregation.html
+ */
+class Max extends AbstractSimpleAggregation
+{
+}

+ 12 - 0
lib/Elastica/Aggregation/Min.php

@@ -0,0 +1,12 @@
+<?php
+
+namespace Elastica\Aggregation;
+
+/**
+ * Class Min.
+ *
+ * @see https://www.elastic.co/guide/en/elasticsearch/reference/current/search-aggregations-metrics-min-aggregation.html
+ */
+class Min extends AbstractSimpleAggregation
+{
+}

+ 33 - 0
lib/Elastica/Aggregation/Missing.php

@@ -0,0 +1,33 @@
+<?php
+
+namespace Elastica\Aggregation;
+
+/**
+ * Class Missing.
+ *
+ * @see https://www.elastic.co/guide/en/elasticsearch/reference/current/search-aggregations-bucket-missing-aggregation.html
+ */
+class Missing extends AbstractAggregation
+{
+    /**
+     * @param string $name  the name of this aggregation
+     * @param string $field the field on which to perform this aggregation
+     */
+    public function __construct($name, $field)
+    {
+        parent::__construct($name);
+        $this->setField($field);
+    }
+
+    /**
+     * Set the field for this aggregation.
+     *
+     * @param string $field the name of the document field on which to perform this aggregation
+     *
+     * @return $this
+     */
+    public function setField($field)
+    {
+        return $this->setParam('field', $field);
+    }
+}

+ 33 - 0
lib/Elastica/Aggregation/Nested.php

@@ -0,0 +1,33 @@
+<?php
+
+namespace Elastica\Aggregation;
+
+/**
+ * Class Nested.
+ *
+ * @see https://www.elastic.co/guide/en/elasticsearch/reference/current/search-aggregations-bucket-nested-aggregation.html
+ */
+class Nested extends AbstractAggregation
+{
+    /**
+     * @param string $name the name of this aggregation
+     * @param string $path the nested path for this aggregation
+     */
+    public function __construct($name, $path)
+    {
+        parent::__construct($name);
+        $this->setPath($path);
+    }
+
+    /**
+     * Set the nested path for this aggregation.
+     *
+     * @param string $path
+     *
+     * @return $this
+     */
+    public function setPath($path)
+    {
+        return $this->setParam('path', $path);
+    }
+}

+ 104 - 0
lib/Elastica/Aggregation/Percentiles.php

@@ -0,0 +1,104 @@
+<?php
+
+namespace Elastica\Aggregation;
+
+/**
+ * Class Percentiles.
+ *
+ * @see https://www.elastic.co/guide/en/elasticsearch/reference/current/search-aggregations-metrics-percentile-aggregation.html
+ */
+class Percentiles extends AbstractSimpleAggregation
+{
+    /**
+     * @param string $name  the name of this aggregation
+     * @param string $field the field on which to perform this aggregation
+     */
+    public function __construct($name, $field = null)
+    {
+        parent::__construct($name);
+
+        if (!is_null($field)) {
+            $this->setField($field);
+        }
+    }
+
+    /**
+     * Set compression parameter.
+     *
+     * @param float $value
+     *
+     * @return Percentiles $this
+     */
+    public function setCompression(float $value): Percentiles
+    {
+        $compression = ['compression' => $value];
+
+        return $this->setParam('tdigest', $compression);
+    }
+
+    /**
+     * Set hdr parameter.
+     *
+     * @param string $key
+     * @param float  $value
+     *
+     * @return Percentiles $this
+     */
+    public function setHdr(string $key, float $value): Percentiles
+    {
+        $compression = [$key => $value];
+
+        return $this->setParam('hdr', $compression);
+    }
+
+    /**
+     * the keyed flag is set to true which associates a unique string
+     * key with each bucket and returns the ranges as a hash
+     * rather than an array.
+     *
+     * @param bool $keyed
+     *
+     * @return Percentiles $this
+     */
+    public function setKeyed(bool $keyed = true): Percentiles
+    {
+        return $this->setParam('keyed', $keyed);
+    }
+
+    /**
+     * Set which percents must be returned.
+     *
+     * @param float[] $percents
+     *
+     * @return Percentiles $this
+     */
+    public function setPercents(array $percents): Percentiles
+    {
+        return $this->setParam('percents', $percents);
+    }
+
+    /**
+     * Add yet another percent to result.
+     *
+     * @param float $percent
+     *
+     * @return Percentiles $this
+     */
+    public function addPercent(float $percent): Percentiles
+    {
+        return $this->addParam('percents', $percent);
+    }
+
+    /**
+     * Defines how documents that are missing a value should
+     * be treated.
+     *
+     * @param float $missing
+     *
+     * @return Percentiles
+     */
+    public function setMissing(float $missing): Percentiles
+    {
+        return $this->setParam('missing', $missing);
+    }
+}

+ 59 - 0
lib/Elastica/Aggregation/Range.php

@@ -0,0 +1,59 @@
+<?php
+
+namespace Elastica\Aggregation;
+
+use Elastica\Exception\InvalidException;
+
+/**
+ * Class Range.
+ *
+ * @see https://www.elastic.co/guide/en/elasticsearch/reference/current/search-aggregations-bucket-range-aggregation.html
+ */
+class Range extends AbstractSimpleAggregation
+{
+    /**
+     * Add a range to this aggregation.
+     *
+     * @param int|float $fromValue low end of this range, exclusive (greater than or equal to)
+     * @param int|float $toValue   high end of this range, exclusive (less than)
+     * @param string    $key       customized key value
+     *
+     * @throws \Elastica\Exception\InvalidException
+     *
+     * @return $this
+     */
+    public function addRange($fromValue = null, $toValue = null, $key = null)
+    {
+        if (is_null($fromValue) && is_null($toValue)) {
+            throw new InvalidException('Either fromValue or toValue must be set. Both cannot be null.');
+        }
+
+        $range = [];
+
+        if (!is_null($fromValue)) {
+            $range['from'] = $fromValue;
+        }
+
+        if (!is_null($toValue)) {
+            $range['to'] = $toValue;
+        }
+
+        if (!is_null($key)) {
+            $range['key'] = $key;
+        }
+
+        return $this->addParam('ranges', $range);
+    }
+
+    /**
+     * If set to true, a unique string key will be associated with each bucket, and ranges will be returned as an associative array.
+     *
+     * @param bool $keyed
+     *
+     * @return $this
+     */
+    public function setKeyedResponse($keyed = true)
+    {
+        return $this->setParam('keyed', (bool) $keyed);
+    }
+}

+ 50 - 0
lib/Elastica/Aggregation/ReverseNested.php

@@ -0,0 +1,50 @@
+<?php
+
+namespace Elastica\Aggregation;
+
+/**
+ * Reversed Nested Aggregation.
+ *
+ * @see https://www.elastic.co/guide/en/elasticsearch/reference/current/search-aggregations-bucket-reverse-nested-aggregation.html
+ */
+class ReverseNested extends AbstractAggregation
+{
+    /**
+     * @param string $name The name of this aggregation
+     * @param string $path Optional path to the nested object for this aggregation. Defaults to the root of the main document.
+     */
+    public function __construct($name, $path = null)
+    {
+        parent::__construct($name);
+
+        if (null !== $path) {
+            $this->setPath($path);
+        }
+    }
+
+    /**
+     * Set the nested path for this aggregation.
+     *
+     * @param string $path
+     *
+     * @return $this
+     */
+    public function setPath($path)
+    {
+        return $this->setParam('path', $path);
+    }
+
+    /**
+     * {@inheritdoc}
+     */
+    public function toArray()
+    {
+        $array = parent::toArray();
+
+        // ensure we have an object for the reverse_nested key.
+        // if we don't have a path, then this would otherwise get encoded as an empty array, which is invalid.
+        $array['reverse_nested'] = (object) $array['reverse_nested'];
+
+        return $array;
+    }
+}

+ 94 - 0
lib/Elastica/Aggregation/ScriptedMetric.php

@@ -0,0 +1,94 @@
+<?php
+
+namespace Elastica\Aggregation;
+
+/**
+ * Class ScriptedMetric.
+ *
+ * @see https://www.elastic.co/guide/en/elasticsearch/reference/current/search-aggregations-metrics-scripted-metric-aggregation.html
+ */
+class ScriptedMetric extends AbstractAggregation
+{
+    /**
+     * @param string      $name          the name if this aggregation
+     * @param string|null $initScript    Executed prior to any collection of documents
+     * @param string|null $mapScript     Executed once per document collected
+     * @param string|null $combineScript Executed once on each shard after document collection is complete
+     * @param string|null $reduceScript  Executed once on the coordinating node after all shards have returned their results
+     */
+    public function __construct($name, $initScript = null, $mapScript = null, $combineScript = null, $reduceScript = null)
+    {
+        parent::__construct($name);
+        if ($initScript) {
+            $this->setInitScript($initScript);
+        }
+        if ($mapScript) {
+            $this->setMapScript($mapScript);
+        }
+        if ($combineScript) {
+            $this->setCombineScript($combineScript);
+        }
+        if ($reduceScript) {
+            $this->setReduceScript($reduceScript);
+        }
+    }
+
+    /**
+     * Executed once on each shard after document collection is complete.
+     *
+     * Allows the aggregation to consolidate the state returned from each shard.
+     * If a combine_script is not provided the combine phase will return the aggregation variable.
+     *
+     * @param string $script
+     *
+     * @return $this
+     */
+    public function setCombineScript($script)
+    {
+        return $this->setParam('combine_script', $script);
+    }
+
+    /**
+     * Executed prior to any collection of documents.
+     *
+     * Allows the aggregation to set up any initial state.
+     *
+     * @param string $script
+     *
+     * @return $this
+     */
+    public function setInitScript($script)
+    {
+        return $this->setParam('init_script', $script);
+    }
+
+    /**
+     * Executed once per document collected.
+     *
+     * This is the only required script. If no combine_script is specified, the resulting state needs to be stored in
+     * an object named _agg.
+     *
+     * @param string $script
+     *
+     * @return $this
+     */
+    public function setMapScript($script)
+    {
+        return $this->setParam('map_script', $script);
+    }
+
+    /**
+     * Executed once on the coordinating node after all shards have returned their results.
+     *
+     * The script is provided with access to a variable _aggs which is an array of the result of the combine_script on
+     * each shard. If a reduce_script is not provided the reduce phase will return the _aggs variable.
+     *
+     * @param string $script
+     *
+     * @return $this
+     */
+    public function setReduceScript($script)
+    {
+        return $this->setParam('reduce_script', $script);
+    }
+}

+ 88 - 0
lib/Elastica/Aggregation/SerialDiff.php

@@ -0,0 +1,88 @@
+<?php
+
+namespace Elastica\Aggregation;
+
+use Elastica\Exception\InvalidException;
+
+/**
+ * Class SerialDiff.
+ *
+ * @see https://www.elastic.co/guide/en/elasticsearch/reference/current/search-aggregations-pipeline-serialdiff-aggregation.html
+ */
+class SerialDiff extends AbstractAggregation
+{
+    /**
+     * @param string      $name
+     * @param string|null $bucketsPath
+     */
+    public function __construct($name, $bucketsPath = null)
+    {
+        parent::__construct($name);
+
+        if (null !== $bucketsPath) {
+            $this->setBucketsPath($bucketsPath);
+        }
+    }
+
+    /**
+     * Set the buckets_path for this aggregation.
+     *
+     * @param string $bucketsPath
+     *
+     * @return $this
+     */
+    public function setBucketsPath($bucketsPath)
+    {
+        return $this->setParam('buckets_path', $bucketsPath);
+    }
+
+    /**
+     * Set the lag for this aggregation.
+     *
+     * @param int $lag
+     *
+     * @return $this
+     */
+    public function setLag($lag)
+    {
+        return $this->setParam('lag', $lag);
+    }
+
+    /**
+     * Set the gap policy for this aggregation.
+     *
+     * @param string $gapPolicy
+     *
+     * @return $this
+     */
+    public function setGapPolicy($gapPolicy)
+    {
+        return $this->setParam('gap_policy', $gapPolicy);
+    }
+
+    /**
+     * Set the format for this aggregation.
+     *
+     * @param string $format
+     *
+     * @return $this
+     */
+    public function setFormat($format)
+    {
+        return $this->setParam('format', $format);
+    }
+
+    /**
+     * @throws InvalidException If buckets path is not set
+     *
+     * @return array
+     */
+    public function toArray()
+    {
+        if (!$this->hasParam('buckets_path')) {
+            throw new InvalidException('Buckets path is required');
+        }
+
+        return parent::toArray();
+    }
+}

+ 28 - 0
lib/Elastica/Aggregation/SignificantTerms.php

@@ -0,0 +1,28 @@
+<?php
+
+namespace Elastica\Aggregation;
+
+use Elastica\Query\AbstractQuery;
+
+/**
+ * Class SignificantTerms.
+ *
+ * @see https://www.elastic.co/guide/en/elasticsearch/reference/current/search-aggregations-bucket-significantterms-aggregation.html
+ */
+class SignificantTerms extends AbstractTermsAggregation
+{
+    /**
+     * The default source of statistical information for background term frequencies is the entire index and this scope can
+     * be narrowed through the use of a background_filter to focus in on significant terms within a narrower context.
+     *
+     * @param AbstractQuery $filter
+     *
+     * @return $this
+     *
+     * @see https://www.elastic.co/guide/en/elasticsearch/reference/current/search-aggregations-bucket-significantterms-aggregation.html#_custom_background_context
+     */
+    public function setBackgroundFilter(AbstractQuery $filter)
+    {
+        return $this->setParam('background_filter', $filter);
+    }
+}

+ 12 - 0
lib/Elastica/Aggregation/Stats.php

@@ -0,0 +1,12 @@
+<?php
+
+namespace Elastica\Aggregation;
+
+/**
+ * Class Stats.
+ *
+ * @see https://www.elastic.co/guide/en/elasticsearch/reference/current/search-aggregations-metrics-stats-aggregation.html
+ */
+class Stats extends AbstractSimpleAggregation
+{
+}

+ 76 - 0
lib/Elastica/Aggregation/StatsBucket.php

@@ -0,0 +1,76 @@
+<?php
+
+namespace Elastica\Aggregation;
+
+use Elastica\Exception\InvalidException;
+
+/**
+ * Class StatsBucket.
+ *
+ * @see https://www.elastic.co/guide/en/elasticsearch/reference/current/search-aggregations-pipeline-stats-bucket-aggregation.html
+ */
+class StatsBucket extends AbstractAggregation
+{
+    /**
+     * @param string      $name
+     * @param string|null $bucketsPath
+     */
+    public function __construct($name, $bucketsPath = null)
+    {
+        parent::__construct($name);
+
+        if (null !== $bucketsPath) {
+            $this->setBucketsPath($bucketsPath);
+        }
+    }
+
+    /**
+     * Set the buckets_path for this aggregation.
+     *
+     * @param string $bucketsPath
+     *
+     * @return $this
+     */
+    public function setBucketsPath($bucketsPath)
+    {
+        return $this->setParam('buckets_path', $bucketsPath);
+    }
+
+    /**
+     * Set the gap policy for this aggregation.
+     *
+     * @param string $gapPolicy
+     *
+     * @return $this
+     */
+    public function setGapPolicy($gapPolicy)
+    {
+        return $this->setParam('gap_policy', $gapPolicy);
+    }
+
+    /**
+     * Set the format for this aggregation.
+     *
+     * @param string $format
+     *
+     * @return $this
+     */
+    public function setFormat($format)
+    {
+        return $this->setParam('format', $format);
+    }
+
+    /**
+     * @throws InvalidException If buckets path or script is not set
+     *
+     * @return array
+     */
+    public function toArray()
+    {
+        if (!$this->hasParam('buckets_path')) {
+            throw new InvalidException('Buckets path is required');
+        }
+
+        return parent::toArray();
+    }
+}

+ 12 - 0
lib/Elastica/Aggregation/Sum.php

@@ -0,0 +1,12 @@
+<?php
+
+namespace Elastica\Aggregation;
+
+/**
+ * Class Sum.
+ *
+ * @see https://www.elastic.co/guide/en/elasticsearch/reference/current/search-aggregations-metrics-sum-aggregation.html
+ */
+class Sum extends AbstractSimpleAggregation
+{
+}

+ 76 - 0
lib/Elastica/Aggregation/SumBucket.php

@@ -0,0 +1,76 @@
+<?php
+
+namespace Elastica\Aggregation;
+
+use Elastica\Exception\InvalidException;
+
+/**
+ * Class SumBucket.
+ *
+ * @see https://www.elastic.co/guide/en/elasticsearch/reference/current/search-aggregations-pipeline-sum-bucket-aggregation.html
+ */
+class SumBucket extends AbstractAggregation
+{
+    /**
+     * @param string      $name
+     * @param string|null $bucketsPath
+     */
+    public function __construct($name, $bucketsPath = null)
+    {
+        parent::__construct($name);
+
+        if (null !== $bucketsPath) {
+            $this->setBucketsPath($bucketsPath);
+        }
+    }
+
+    /**
+     * Set the buckets_path for this aggregation.
+     *
+     * @param string $bucketsPath
+     *
+     * @return $this
+     */
+    public function setBucketsPath($bucketsPath)
+    {
+        return $this->setParam('buckets_path', $bucketsPath);
+    }
+
+    /**
+     * Set the gap policy for this aggregation.
+     *
+     * @param string $gapPolicy
+     *
+     * @return $this
+     */
+    public function setGapPolicy($gapPolicy)
+    {
+        return $this->setParam('gap_policy', $gapPolicy);
+    }
+
+    /**
+     * Set the format for this aggregation.
+     *
+     * @param string $format
+     *
+     * @return $this
+     */
+    public function setFormat($format)
+    {
+        return $this->setParam('format', $format);
+    }
+
+    /**
+     * @throws InvalidException If buckets path or script is not set
+     *
+     * @return array
+     */
+    public function toArray()
+    {
+        if (!$this->hasParam('buckets_path')) {
+            throw new InvalidException('Buckets path is required');
+        }
+
+        return parent::toArray();
+    }
+}

+ 36 - 0
lib/Elastica/Aggregation/Terms.php

@@ -0,0 +1,36 @@
+<?php
+
+namespace Elastica\Aggregation;
+
+/**
+ * Class Terms.
+ *
+ * @see https://www.elastic.co/guide/en/elasticsearch/reference/current/search-aggregations-bucket-terms-aggregation.html
+ */
+class Terms extends AbstractTermsAggregation
+{
+    /**
+     * Set the bucket sort order.
+     *
+     * @param string $order     "_count", "_term", or the name of a sub-aggregation or sub-aggregation response field
+     * @param string $direction "asc" or "desc"
+     *
+     * @return $this
+     */
+    public function setOrder($order, $direction)
+    {
+        return $this->setParam('order', [$order => $direction]);
+    }
+
+    /**
+     * Sets a list of bucket sort orders.
+     *
+     * @param array $orders a list of [<aggregationField>|"_count"|"_term" => <direction>] definitions
+     *
+     * @return $this
+     */
+    public function setOrders(array $orders)
+    {
+        return $this->setParam('order', $orders);
+    }
+}

+ 161 - 0
lib/Elastica/Aggregation/TopHits.php

@@ -0,0 +1,161 @@
+<?php
+
+namespace Elastica\Aggregation;
+
+use Elastica\Script\AbstractScript;
+use Elastica\Script\ScriptFields;
+
+/**
+ * Class TopHits.
+ *
+ * @see https://www.elastic.co/guide/en/elasticsearch/reference/current/search-aggregations-metrics-top-hits-aggregation.html
+ */
+class TopHits extends AbstractAggregation
+{
+    /**
+     * @return array
+     */
+    public function toArray()
+    {
+        $array = parent::toArray();
+
+        // if there are no params, it's ok, but ES will throw exception if json
+        // will be like {"top_hits":[]} instead of {"top_hits":{}}
+        if (empty($array['top_hits'])) {
+            $array['top_hits'] = new \stdClass();
+        }
+
+        return $array;
+    }
+
+    /**
+     * The maximum number of top matching hits to return per bucket. By default the top three matching hits are returned.
+     *
+     * @param int $size
+     *
+     * @return $this
+     */
+    public function setSize($size)
+    {
+        return $this->setParam('size', (int) $size);
+    }
+
+    /**
+     * The offset from the first result you want to fetch.
+     *
+     * @param int $from
+     *
+     * @return $this
+     */
+    public function setFrom($from)
+    {
+        return $this->setParam('from', (int) $from);
+    }
+
+    /**
+     * How the top matching hits should be sorted. By default the hits are sorted by the score of the main query.
+     *
+     * @param array $sortArgs
+     *
+     * @return $this
+     */
+    public function setSort(array $sortArgs)
+    {
+        return $this->setParam('sort', $sortArgs);
+    }
+
+    /**
+     * Allows to control how the _source field is returned with every hit.
+     *
+     * @param array|string|bool $params Fields to be returned or false to disable source
+     *
+     * @return $this
+     */
+    public function setSource($params)
+    {
+        return $this->setParam('_source', $params);
+    }
+
+    /**
+     * Returns a version for each search hit.
+     *
+     * @param bool $version
+     *
+     * @return $this
+     */
+    public function setVersion($version)
+    {
+        return $this->setParam('version', (bool) $version);
+    }
+
+    /**
+     * Enables explanation for each hit on how its score was computed.
+     *
+     * @param bool $explain
+     *
+     * @return $this
+     */
+    public function setExplain($explain)
+    {
+        return $this->setParam('explain', (bool) $explain);
+    }
+
+    /**
+     * Set script fields.
+     *
+     * @param array|\Elastica\Script\ScriptFields $scriptFields
+     *
+     * @return $this
+     */
+    public function setScriptFields($scriptFields)
+    {
+        if (is_array($scriptFields)) {
+            $scriptFields = new ScriptFields($scriptFields);
+        }
+
+        return $this->setParam('script_fields', $scriptFields);
+    }
+
+    /**
+     * Adds a Script to the aggregation.
+     *
+     * @param string                          $name
+     * @param \Elastica\Script\AbstractScript $script
+     *
+     * @return $this
+     */
+    public function addScriptField($name, AbstractScript $script)
+    {
+        if (!isset($this->_params['script_fields'])) {
+            $this->_params['script_fields'] = new ScriptFields();
+        }
+
+        $this->_params['script_fields']->addScript($name, $script);
+
+        return $this;
+    }
+
+    /**
+     * Sets highlight arguments for the results.
+     *
+     * @param array $highlightArgs
+     *
+     * @return $this
+     */
+    public function setHighlight(array $highlightArgs)
+    {
+        return $this->setParam('highlight', $highlightArgs);
+    }
+
+    /**
+     * Allows to return the field data representation of a field for each hit.
+     *
+     * @param array $fields
+     *
+     * @return $this
+     */
+    public function setFieldDataFields(array $fields)
+    {
+        return $this->setParam('docvalue_fields', $fields);
+    }
+}

+ 33 - 0
lib/Elastica/Aggregation/ValueCount.php

@@ -0,0 +1,33 @@
+<?php
+
+namespace Elastica\Aggregation;
+
+/**
+ * Class ValueCount.
+ *
+ * @see https://www.elastic.co/guide/en/elasticsearch/reference/current/search-aggregations-metrics-valuecount-aggregation.html
+ */
+class ValueCount extends AbstractAggregation
+{
+    /**
+     * @param string $name  the name of this aggregation
+     * @param string $field the field on which to perform this aggregation
+     */
+    public function __construct($name, $field)
+    {
+        parent::__construct($name);
+        $this->setField($field);
+    }
+
+    /**
+     * Set the field for this aggregation.
+     *
+     * @param string $field the name of the document field on which to perform this aggregation
+     *
+     * @return $this
+     */
+    public function setField($field)
+    {
+        return $this->setParam('field', $field);
+    }
+}

+ 19 - 0
lib/Elastica/ArrayableInterface.php

@@ -0,0 +1,19 @@
+<?php
+
+namespace Elastica;
+
+/**
+ * Interface for params.
+ *
+ *
+ * @author Evgeniy Sokolov <ewgraf@gmail.com>
+ */
+interface ArrayableInterface
+{
+    /**
+     * Converts the object to an array.
+     *
+     * @return array Object as array
+     */
+    public function toArray();
+}

+ 416 - 0
lib/Elastica/Bulk.php

@@ -0,0 +1,416 @@
+<?php
+
+namespace Elastica;
+
+use Elastica\Bulk\Action;
+use Elastica\Bulk\Action\AbstractDocument as AbstractDocumentAction;
+use Elastica\Bulk\Response as BulkResponse;
+use Elastica\Bulk\ResponseSet;
+use Elastica\Exception\Bulk\ResponseException as BulkResponseException;
+use Elastica\Exception\InvalidException;
+use Elastica\Script\AbstractScript;
+
+class Bulk
+{
+    const DELIMITER = "\n";
+
+    /**
+     * @var \Elastica\Client
+     */
+    protected $_client;
+
+    /**
+     * @var \Elastica\Bulk\Action[]
+     */
+    protected $_actions = [];
+
+    /**
+     * @var string|null
+     */
+    protected $_index;
+
+    /**
+     * @var string|null
+     */
+    protected $_type;
+
+    /**
+     * @var array request parameters to the bulk api
+     */
+    protected $_requestParams = [];
+
+    /**
+     * @param \Elastica\Client $client
+     */
+    public function __construct(Client $client)
+    {
+        $this->_client = $client;
+    }
+
+    /**
+     * @param string|\Elastica\Index $index
+     *
+     * @return $this
+     */
+    public function setIndex($index)
+    {
+        if ($index instanceof Index) {
+            $index = $index->getName();
+        }
+
+        $this->_index = (string) $index;
+
+        return $this;
+    }
+
+    /**
+     * @return string|null
+     */
+    public function getIndex()
+    {
+        return $this->_index;
+    }
+
+    /**
+     * @return bool
+     */
+    public function hasIndex()
+    {
+        return null !== $this->getIndex() && '' !== $this->getIndex();
+    }
+
+    /**
+     * @param string|\Elastica\Type $type
+     *
+     * @return $this
+     */
+    public function setType($type)
+    {
+        if ($type instanceof Type) {
+            $this->setIndex($type->getIndex()->getName());
+            $type = $type->getName();
+        }
+
+        $this->_type = (string) $type;
+
+        return $this;
+    }
+
+    /**
+     * @return string|null
+     */
+    public function getType()
+    {
+        return $this->_type;
+    }
+
+    /**
+     * @return bool
+     */
+    public function hasType()
+    {
+        return null !== $this->getType() && '' !== $this->getType();
+    }
+
+    /**
+     * @return string
+     */
+    public function getPath()
+    {
+        $path = '';
+        if ($this->hasIndex()) {
+            $path .= $this->getIndex().'/';
+            if ($this->hasType()) {
+                $path .= $this->getType().'/';
+            }
+        }
+        $path .= '_bulk';
+
+        return $path;
+    }
+
+    /**
+     * @param \Elastica\Bulk\Action $action
+     *
+     * @return $this
+     */
+    public function addAction(Action $action)
+    {
+        $this->_actions[] = $action;
+
+        return $this;
+    }
+
+    /**
+     * @param \Elastica\Bulk\Action[] $actions
+     *
+     * @return $this
+     */
+    public function addActions(array $actions)
+    {
+        foreach ($actions as $action) {
+            $this->addAction($action);
+        }
+
+        return $this;
+    }
+
+    /**
+     * @return \Elastica\Bulk\Action[]
+     */
+    public function getActions()
+    {
+        return $this->_actions;
+    }
+
+    /**
+     * @param \Elastica\Document $document
+     * @param string             $opType
+     *
+     * @return $this
+     */
+    public function addDocument(Document $document, $opType = null)
+    {
+        $action = AbstractDocumentAction::create($document, $opType);
+
+        return $this->addAction($action);
+    }
+
+    /**
+     * @param \Elastica\Document[] $documents
+     * @param string               $opType
+     *
+     * @return $this
+     */
+    public function addDocuments(array $documents, $opType = null)
+    {
+        foreach ($documents as $document) {
+            $this->addDocument($document, $opType);
+        }
+
+        return $this;
+    }
+
+    /**
+     * @param \Elastica\Script\AbstractScript $script
+     * @param string                          $opType
+     *
+     * @return $this
+     */
+    public function addScript(AbstractScript $script, $opType = null)
+    {
+        $action = AbstractDocumentAction::create($script, $opType);
+
+        return $this->addAction($action);
+    }
+
+    /**
+     * @param \Elastica\Document[] $scripts
+     * @param string               $opType
+     *
+     * @return $this
+     */
+    public function addScripts(array $scripts, $opType = null)
+    {
+        foreach ($scripts as $document) {
+            $this->addScript($document, $opType);
+        }
+
+        return $this;
+    }
+
+    /**
+     * @param \Elastica\Script\AbstractScript|\Elastica\Document|array $data
+     * @param string                                                   $opType
+     *
+     * @return $this
+     */
+    public function addData($data, $opType = null)
+    {
+        if (!is_array($data)) {
+            $data = [$data];
+        }
+
+        foreach ($data as $actionData) {
+            if ($actionData instanceof AbstractScript) {
+                $this->addScript($actionData, $opType);
+            } elseif ($actionData instanceof Document) {
+                $this->addDocument($actionData, $opType);
+            } else {
+                throw new \InvalidArgumentException('Data should be a Document, a Script or an array containing Documents and/or Scripts');
+            }
+        }
+
+        return $this;
+    }
+
+    /**
+     * @param array $data
+     *
+     * @throws \Elastica\Exception\InvalidException
+     *
+     * @return $this
+     */
+    public function addRawData(array $data)
+    {
+        foreach ($data as $row) {
+            if (is_array($row)) {
+                $opType = key($row);
+                $metadata = reset($row);
+                if (Action::isValidOpType($opType)) {
+                    // add previous action
+                    if (isset($action)) {
+                        $this->addAction($action);
+                    }
+                    $action = new Action($opType, $metadata);
+                } elseif (isset($action)) {
+                    $action->setSource($row);
+                    $this->addAction($action);
+                    $action = null;
+                } else {
+                    throw new InvalidException('Invalid bulk data, source must follow action metadata');
+                }
+            } else {
+                throw new InvalidException('Invalid bulk data, should be array of array, Document or Bulk/Action');
+            }
+        }
+
+        // add last action if available
+        if (isset($action)) {
+            $this->addAction($action);
+        }
+
+        return $this;
+    }
+
+    /**
+     * Set a url parameter on the request bulk request.
+     *
+     * @param string $name  name of the parameter
+     * @param string $value value of the parameter
+     *
+     * @return $this
+     */
+    public function setRequestParam($name, $value)
+    {
+        $this->_requestParams[$name] = $value;
+
+        return $this;
+    }
+
+    /**
+     * Set the amount of time that the request will wait the shards to come on line.
+     * Requires Elasticsearch version >= 0.90.8.
+     *
+     * @param string $time timeout in Elasticsearch time format
+     *
+     * @return $this
+     */
+    public function setShardTimeout($time)
+    {
+        return $this->setRequestParam('timeout', $time);
+    }
+
+    /**
+     * @return string
+     */
+    public function __toString()
+    {
+        return $this->toString();
+    }
+
+    /**
+     * @return string
+     */
+    public function toString()
+    {
+        $data = '';
+        foreach ($this->getActions() as $action) {
+            $data .= $action->toString();
+        }
+
+        return $data;
+    }
+
+    /**
+     * @return array
+     */
+    public function toArray()
+    {
+        $data = [];
+        foreach ($this->getActions() as $action) {
+            foreach ($action->toArray() as $row) {
+                $data[] = $row;
+            }
+        }
+
+        return $data;
+    }
+
+    /**
+     * @return \Elastica\Bulk\ResponseSet
+     */
+    public function send()
+    {
+        $path = $this->getPath();
+        $data = $this->toString();
+
+        $response = $this->_client->request($path, Request::POST, $data, $this->_requestParams, Request::NDJSON_CONTENT_TYPE);
+
+        return $this->_processResponse($response);
+    }
+
+    /**
+     * @param \Elastica\Response $response
+     *
+     * @throws \Elastica\Exception\Bulk\ResponseException
+     * @throws \Elastica\Exception\InvalidException
+     *
+     * @return \Elastica\Bulk\ResponseSet
+     */
+    protected function _processResponse(Response $response)
+    {
+        $responseData = $response->getData();
+
+        $actions = $this->getActions();
+
+        $bulkResponses = [];
+
+        if (isset($responseData['items']) && is_array($responseData['items'])) {
+            foreach ($responseData['items'] as $key => $item) {
+                if (!isset($actions[$key])) {
+                    throw new InvalidException('No response found for action #'.$key);
+                }
+
+                $action = $actions[$key];
+
+                $opType = key($item);
+                $bulkResponseData = reset($item);
+
+                if ($action instanceof AbstractDocumentAction) {
+                    $data = $action->getData();
+                    if ($data instanceof Document && $data->isAutoPopulate()
+                        || $this->_client->getConfigValue(['document', 'autoPopulate'], false)
+                    ) {
+                        if (!$data->hasId() && isset($bulkResponseData['_id'])) {
+                            $data->setId($bulkResponseData['_id']);
+                        }
+                        if (isset($bulkResponseData['_version'])) {
+                            $data->setVersion($bulkResponseData['_version']);
+                        }
+                    }
+                }
+
+                $bulkResponses[] = new BulkResponse($bulkResponseData, $action, $opType);
+            }
+        }
+
+        $bulkResponseSet = new ResponseSet($response, $bulkResponses);
+
+        if ($bulkResponseSet->hasError()) {
+            throw new BulkResponseException($bulkResponseSet);
+        }
+
+        return $bulkResponseSet;
+    }
+}

+ 233 - 0
lib/Elastica/Bulk/Action.php

@@ -0,0 +1,233 @@
+<?php
+
+namespace Elastica\Bulk;
+
+use Elastica\Bulk;
+use Elastica\Index;
+use Elastica\JSON;
+use Elastica\Type;
+
+class Action
+{
+    const OP_TYPE_CREATE = 'create';
+    const OP_TYPE_INDEX = 'index';
+    const OP_TYPE_DELETE = 'delete';
+    const OP_TYPE_UPDATE = 'update';
+
+    /**
+     * @var array
+     */
+    public static $opTypes = [
+        self::OP_TYPE_CREATE,
+        self::OP_TYPE_INDEX,
+        self::OP_TYPE_DELETE,
+        self::OP_TYPE_UPDATE,
+    ];
+
+    /**
+     * @var string
+     */
+    protected $_opType;
+
+    /**
+     * @var array
+     */
+    protected $_metadata = [];
+
+    /**
+     * @var array
+     */
+    protected $_source = [];
+
+    /**
+     * @param string $opType
+     * @param array  $metadata
+     * @param array  $source
+     */
+    public function __construct($opType = self::OP_TYPE_INDEX, array $metadata = [], array $source = [])
+    {
+        $this->setOpType($opType);
+        $this->setMetadata($metadata);
+        $this->setSource($source);
+    }
+
+    /**
+     * @param string $type
+     *
+     * @return $this
+     */
+    public function setOpType($type)
+    {
+        $this->_opType = $type;
+
+        return $this;
+    }
+
+    /**
+     * @return string
+     */
+    public function getOpType()
+    {
+        return $this->_opType;
+    }
+
+    /**
+     * @param array $metadata
+     *
+     * @return $this
+     */
+    public function setMetadata(array $metadata)
+    {
+        $this->_metadata = $metadata;
+
+        return $this;
+    }
+
+    /**
+     * @return array
+     */
+    public function getMetadata()
+    {
+        return $this->_metadata;
+    }
+
+    /**
+     * @return array
+     */
+    public function getActionMetadata()
+    {
+        return [$this->_opType => $this->getMetadata()];
+    }
+
+    /**
+     * @param array $source
+     *
+     * @return $this
+     */
+    public function setSource($source)
+    {
+        $this->_source = $source;
+
+        return $this;
+    }
+
+    /**
+     * @return array
+     */
+    public function getSource()
+    {
+        return $this->_source;
+    }
+
+    /**
+     * @return bool
+     */
+    public function hasSource()
+    {
+        return !empty($this->_source);
+    }
+
+    /**
+     * @param string|\Elastica\Index $index
+     *
+     * @return $this
+     */
+    public function setIndex($index)
+    {
+        if ($index instanceof Index) {
+            $index = $index->getName();
+        }
+        $this->_metadata['_index'] = $index;
+
+        return $this;
+    }
+
+    /**
+     * @param string|\Elastica\Type $type
+     *
+     * @return $this
+     */
+    public function setType($type)
+    {
+        if ($type instanceof Type) {
+            $this->setIndex($type->getIndex()->getName());
+            $type = $type->getName();
+        }
+        $this->_metadata['_type'] = $type;
+
+        return $this;
+    }
+
+    /**
+     * @param string $id
+     *
+     * @return $this
+     */
+    public function setId($id)
+    {
+        $this->_metadata['_id'] = $id;
+
+        return $this;
+    }
+
+    /**
+     * @param string $routing
+     *
+     * @return $this
+     */
+    public function setRouting($routing)
+    {
+        $this->_metadata['_routing'] = $routing;
+
+        return $this;
+    }
+
+    /**
+     * @return array
+     */
+    public function toArray()
+    {
+        $data[] = $this->getActionMetadata();
+        if ($this->hasSource()) {
+            $data[] = $this->getSource();
+        }
+
+        return $data;
+    }
+
+    /**
+     * @return string
+     */
+    public function toString()
+    {
+        $string = JSON::stringify($this->getActionMetadata(), JSON_FORCE_OBJECT).Bulk::DELIMITER;
+        if ($this->hasSource()) {
+            $source = $this->getSource();
+            if (is_string($source)) {
+                $string .= $source;
+            } elseif (is_array($source) && array_key_exists('doc', $source) && is_string($source['doc'])) {
+                if (isset($source['doc_as_upsert'])) {
+                    $docAsUpsert = ', "doc_as_upsert": '.($source['doc_as_upsert'] ? 'true' : 'false');
+                } else {
+                    $docAsUpsert = '';
+                }
+                $string .= '{"doc": '.$source['doc'].$docAsUpsert.'}';
+            } else {
+                $string .= JSON::stringify($source, JSON_UNESCAPED_UNICODE);
+            }
+            $string .= Bulk::DELIMITER;
+        }
+
+        return $string;
+    }
+
+    /**
+     * @param string $opType
+     *
+     * @return bool
+     */
+    public static function isValidOpType($opType)
+    {
+        return in_array($opType, self::$opTypes);
+    }
+}

+ 171 - 0
lib/Elastica/Bulk/Action/AbstractDocument.php

@@ -0,0 +1,171 @@
+<?php
+
+namespace Elastica\Bulk\Action;
+
+use Elastica\AbstractUpdateAction;
+use Elastica\Bulk\Action;
+use Elastica\Document;
+use Elastica\Script\AbstractScript;
+
+abstract class AbstractDocument extends Action
+{
+    /**
+     * @var Document|AbstractScript
+     */
+    protected $_data;
+
+    /**
+     * @param Document|AbstractScript $document
+     */
+    public function __construct($document)
+    {
+        $this->setData($document);
+    }
+
+    /**
+     * @param Document $document
+     *
+     * @return $this
+     */
+    public function setDocument(Document $document)
+    {
+        $this->_data = $document;
+
+        $metadata = $this->_getMetadata($document);
+
+        $this->setMetadata($metadata);
+
+        return $this;
+    }
+
+    /**
+     * @param AbstractScript $script
+     *
+     * @return $this
+     */
+    public function setScript(AbstractScript $script)
+    {
+        if (!($this instanceof UpdateDocument)) {
+            throw new \BadMethodCallException('setScript() can only be used for UpdateDocument');
+        }
+
+        $this->_data = $script;
+
+        $metadata = $this->_getMetadata($script);
+        $this->setMetadata($metadata);
+
+        return $this;
+    }
+
+    /**
+     * @param AbstractScript|Document $data
+     *
+     * @throws \InvalidArgumentException
+     *
+     * @return $this
+     */
+    public function setData($data)
+    {
+        if ($data instanceof AbstractScript) {
+            $this->setScript($data);
+        } elseif ($data instanceof Document) {
+            $this->setDocument($data);
+        } else {
+            throw new \InvalidArgumentException('Data should be a Document or a Script.');
+        }
+
+        return $this;
+    }
+
+    /**
+     * Note: This is for backwards compatibility.
+     *
+     * @return Document|null
+     */
+    public function getDocument()
+    {
+        if ($this->_data instanceof Document) {
+            return $this->_data;
+        }
+
+        return;
+    }
+
+    /**
+     * Note: This is for backwards compatibility.
+     *
+     * @return AbstractScript|null
+     */
+    public function getScript()
+    {
+        if ($this->_data instanceof AbstractScript) {
+            return $this->_data;
+        }
+
+        return;
+    }
+
+    /**
+     * @return Document|AbstractScript
+     */
+    public function getData()
+    {
+        return $this->_data;
+    }
+
+    /**
+     * @param \Elastica\AbstractUpdateAction $source
+     *
+     * @return array
+     */
+    abstract protected function _getMetadata(AbstractUpdateAction $source);
+
+    /**
+     * Creates a bulk action for a document or a script.
+     *
+     * The action can be index, update, create or delete based on the $opType param (by default index).
+     *
+     * @param Document|AbstractScript $data
+     * @param string                  $opType
+     *
+     * @return static
+     */
+    public static function create($data, $opType = null)
+    {
+        //Check type
+        if (!($data instanceof Document) && !($data instanceof AbstractScript)) {
+            throw new \InvalidArgumentException('The data needs to be a Document or a Script.');
+        }
+
+        if (null === $opType && $data->hasOpType()) {
+            $opType = $data->getOpType();
+        }
+
+        //Check that scripts can only be used for updates
+        if ($data instanceof AbstractScript) {
+            if (null === $opType) {
+                $opType = self::OP_TYPE_UPDATE;
+            } elseif (self::OP_TYPE_UPDATE != $opType) {
+                throw new \InvalidArgumentException('Scripts can only be used with the update operation type.');
+            }
+        }
+
+        switch ($opType) {
+            case self::OP_TYPE_DELETE:
+                $action = new DeleteDocument($data);
+                break;
+            case self::OP_TYPE_CREATE:
+                $action = new CreateDocument($data);
+                break;
+            case self::OP_TYPE_UPDATE:
+                $action = new UpdateDocument($data);
+                break;
+            case self::OP_TYPE_INDEX:
+            default:
+                $action = new IndexDocument($data);
+                break;
+        }
+
+        return $action;
+    }
+}

+ 11 - 0
lib/Elastica/Bulk/Action/CreateDocument.php

@@ -0,0 +1,11 @@
+<?php
+
+namespace Elastica\Bulk\Action;
+
+class CreateDocument extends IndexDocument
+{
+    /**
+     * @var string
+     */
+    protected $_opType = self::OP_TYPE_CREATE;
+}

+ 34 - 0
lib/Elastica/Bulk/Action/DeleteDocument.php

@@ -0,0 +1,34 @@
+<?php
+
+namespace Elastica\Bulk\Action;
+
+use Elastica\AbstractUpdateAction;
+
+class DeleteDocument extends AbstractDocument
+{
+    /**
+     * @var string
+     */
+    protected $_opType = self::OP_TYPE_DELETE;
+
+    /**
+     * @param \Elastica\AbstractUpdateAction $action
+     *
+     * @return array
+     */
+    protected function _getMetadata(AbstractUpdateAction $action)
+    {
+        $params = [
+            'index',
+            'type',
+            'id',
+            'version',
+            'version_type',
+            'routing',
+            'parent',
+        ];
+        $metadata = $action->getOptions($params, true);
+
+        return $metadata;
+    }
+}

+ 51 - 0
lib/Elastica/Bulk/Action/IndexDocument.php

@@ -0,0 +1,51 @@
+<?php
+
+namespace Elastica\Bulk\Action;
+
+use Elastica\AbstractUpdateAction;
+use Elastica\Document;
+
+class IndexDocument extends AbstractDocument
+{
+    /**
+     * @var string
+     */
+    protected $_opType = self::OP_TYPE_INDEX;
+
+    /**
+     * @param \Elastica\Document $document
+     *
+     * @return $this
+     */
+    public function setDocument(Document $document)
+    {
+        parent::setDocument($document);
+
+        $this->setSource($document->getData());
+
+        return $this;
+    }
+
+    /**
+     * @param \Elastica\AbstractUpdateAction $action
+     *
+     * @return array
+     */
+    protected function _getMetadata(AbstractUpdateAction $action)
+    {
+        $params = [
+            'index',
+            'type',
+            'id',
+            'version',
+            'version_type',
+            'routing',
+            'parent',
+            'retry_on_conflict',
+        ];
+
+        $metadata = $action->getOptions($params, true);
+
+        return $metadata;
+    }
+}

+ 67 - 0
lib/Elastica/Bulk/Action/UpdateDocument.php

@@ -0,0 +1,67 @@
+<?php
+
+namespace Elastica\Bulk\Action;
+
+use Elastica\Document;
+use Elastica\Script\AbstractScript;
+
+class UpdateDocument extends IndexDocument
+{
+    /**
+     * @var string
+     */
+    protected $_opType = self::OP_TYPE_UPDATE;
+
+    /**
+     * Set the document for this bulk update action.
+     *
+     * @param \Elastica\Document $document
+     *
+     * @return $this
+     */
+    public function setDocument(Document $document)
+    {
+        parent::setDocument($document);
+
+        $source = ['doc' => $document->getData()];
+
+        if ($document->getDocAsUpsert()) {
+            $source['doc_as_upsert'] = true;
+        } elseif ($document->hasUpsert()) {
+            $upsert = $document->getUpsert()->getData();
+
+            if (!empty($upsert)) {
+                $source['upsert'] = $upsert;
+            }
+        }
+
+        $this->setSource($source);
+
+        return $this;
+    }
+
+    /**
+     * @param \Elastica\Script\AbstractScript $script
+     *
+     * @return $this
+     */
+    public function setScript(AbstractScript $script)
+    {
+        parent::setScript($script);
+
+        // FIXME: can we throw away toArray cast?
+        $source = $script->toArray();
+
+        if ($script->hasUpsert()) {
+            $upsert = $script->getUpsert()->getData();
+
+            if (!empty($upsert)) {
+                $source['upsert'] = $upsert;
+            }
+        }
+
+        $this->setSource($source);
+
+        return $this;
+    }
+}

+ 47 - 0
lib/Elastica/Bulk/Response.php

@@ -0,0 +1,47 @@
+<?php
+
+namespace Elastica\Bulk;
+
+use Elastica\Response as BaseResponse;
+
+class Response extends BaseResponse
+{
+    /**
+     * @var \Elastica\Bulk\Action
+     */
+    protected $_action;
+
+    /**
+     * @var string
+     */
+    protected $_opType;
+
+    /**
+     * @param array|string          $responseData
+     * @param \Elastica\Bulk\Action $action
+     * @param string                $opType
+     */
+    public function __construct($responseData, Action $action, $opType)
+    {
+        parent::__construct($responseData);
+
+        $this->_action = $action;
+        $this->_opType = $opType;
+    }
+
+    /**
+     * @return \Elastica\Bulk\Action
+     */
+    public function getAction()
+    {
+        return $this->_action;
+    }
+
+    /**
+     * @return string
+     */
+    public function getOpType()
+    {
+        return $this->_opType;
+    }
+}

+ 139 - 0
lib/Elastica/Bulk/ResponseSet.php

@@ -0,0 +1,139 @@
+<?php
+
+namespace Elastica\Bulk;
+
+use Elastica\Response as BaseResponse;
+
+class ResponseSet extends BaseResponse implements \Iterator, \Countable
+{
+    /**
+     * @var \Elastica\Bulk\Response[]
+     */
+    protected $_bulkResponses = [];
+
+    /**
+     * @var int
+     */
+    protected $_position = 0;
+
+    /**
+     * @param \Elastica\Response        $response
+     * @param \Elastica\Bulk\Response[] $bulkResponses
+     */
+    public function __construct(BaseResponse $response, array $bulkResponses)
+    {
+        parent::__construct($response->getData());
+
+        $this->_bulkResponses = $bulkResponses;
+    }
+
+    /**
+     * @return \Elastica\Bulk\Response[]
+     */
+    public function getBulkResponses()
+    {
+        return $this->_bulkResponses;
+    }
+
+    /**
+     * Returns first found error.
+     *
+     * @return string
+     */
+    public function getError()
+    {
+        foreach ($this->getBulkResponses() as $bulkResponse) {
+            if ($bulkResponse->hasError()) {
+                return $bulkResponse->getError();
+            }
+        }
+
+        return '';
+    }
+
+    /**
+     * Returns first found error (full array).
+     *
+     * @return array|string
+     */
+    public function getFullError()
+    {
+        foreach ($this->getBulkResponses() as $bulkResponse) {
+            if ($bulkResponse->hasError()) {
+                return $bulkResponse->getFullError();
+            }
+        }
+
+        return '';
+    }
+
+    /**
+     * @return bool
+     */
+    public function isOk()
+    {
+        foreach ($this->getBulkResponses() as $bulkResponse) {
+            if (!$bulkResponse->isOk()) {
+                return false;
+            }
+        }
+
+        return true;
+    }
+
+    /**
+     * @return bool
+     */
+    public function hasError()
+    {
+        foreach ($this->getBulkResponses() as $bulkResponse) {
+            if ($bulkResponse->hasError()) {
+                return true;
+            }
+        }
+
+        return false;
+    }
+
+    /**
+     * @return \Elastica\Bulk\Response
+     */
+    public function current()
+    {
+        return $this->_bulkResponses[$this->key()];
+    }
+
+    public function next()
+    {
+        ++$this->_position;
+    }
+
+    /**
+     * @return int
+     */
+    public function key()
+    {
+        return $this->_position;
+    }
+
+    /**
+     * @return bool
+     */
+    public function valid()
+    {
+        return isset($this->_bulkResponses[$this->key()]);
+    }
+
+    public function rewind()
+    {
+        $this->_position = 0;
+    }
+
+    /**
+     * @return int
+     */
+    public function count()
+    {
+        return count($this->_bulkResponses);
+    }
+}

+ 834 - 0
lib/Elastica/Client.php

@@ -0,0 +1,834 @@
+<?php
+
+namespace Elastica;
+
+use Elastica\Bulk\Action;
+use Elastica\Exception\ConnectionException;
+use Elastica\Exception\InvalidException;
+use Elastica\Script\AbstractScript;
+use Elasticsearch\Endpoints\AbstractEndpoint;
+use Elasticsearch\Endpoints\Indices\ForceMerge;
+use Elasticsearch\Endpoints\Indices\Refresh;
+use Elasticsearch\Endpoints\Update;
+use Psr\Log\LoggerInterface;
+use Psr\Log\NullLogger;
+
+/**
+ * Client to connect the the elasticsearch server.
+ *
+ * @author Nicolas Ruflin <spam@ruflin.com>
+ */
+class Client
+{
+    /**
+     * Config with defaults.
+     *
+     * log: Set to true, to enable logging, set a string to log to a specific file
+     * retryOnConflict: Use in \Elastica\Client::updateDocument
+     * bigintConversion: Set to true to enable the JSON bigint to string conversion option (see issue #717)
+     *
+     * @var array
+     */
+    protected $_config = [
+        'host' => null,
+        'port' => null,
+        'path' => null,
+        'url' => null,
+        'proxy' => null,
+        'transport' => null,
+        'persistent' => true,
+        'timeout' => null,
+        'connections' => [], // host, port, path, timeout, transport, compression, persistent, timeout, username, password, config -> (curl, headers, url)
+        'roundRobin' => false,
+        'log' => false,
+        'retryOnConflict' => 0,
+        'bigintConversion' => false,
+        'username' => null,
+        'password' => null,
+    ];
+
+    /**
+     * @var callback
+     */
+    protected $_callback;
+
+    /**
+     * @var Connection\ConnectionPool
+     */
+    protected $_connectionPool;
+
+    /**
+     * @var \Elastica\Request|null
+     */
+    protected $_lastRequest;
+
+    /**
+     * @var \Elastica\Response|null
+     */
+    protected $_lastResponse;
+
+    /**
+     * @var LoggerInterface
+     */
+    protected $_logger;
+
+    /**
+     * @var string
+     */
+    protected $_version;
+
+    /**
+     * Creates a new Elastica client.
+     *
+     * @param array           $config   OPTIONAL Additional config options
+     * @param callback        $callback OPTIONAL Callback function which can be used to be notified about errors (for example connection down)
+     * @param LoggerInterface $logger
+     */
+    public function __construct(array $config = [], $callback = null, LoggerInterface $logger = null)
+    {
+        $this->_callback = $callback;
+
+        if (!$logger && isset($config['log']) && $config['log']) {
+            $logger = new Log($config['log']);
+        }
+        $this->_logger = $logger ?: new NullLogger();
+
+        $this->setConfig($config);
+        $this->_initConnections();
+    }
+
+    /**
+     * Get current version.
+     *
+     * @return string
+     */
+    public function getVersion()
+    {
+        if ($this->_version) {
+            return $this->_version;
+        }
+
+        $data = $this->request('/')->getData();
+
+        return $this->_version = $data['version']['number'];
+    }
+
+    /**
+     * Inits the client connections.
+     */
+    protected function _initConnections()
+    {
+        $connections = [];
+
+        foreach ($this->getConfig('connections') as $connection) {
+            $connections[] = Connection::create($this->_prepareConnectionParams($connection));
+        }
+
+        if (isset($this->_config['servers'])) {
+            foreach ($this->getConfig('servers') as $server) {
+                $connections[] = Connection::create($this->_prepareConnectionParams($server));
+            }
+        }
+
+        // If no connections set, create default connection
+        if (empty($connections)) {
+            $connections[] = Connection::create($this->_prepareConnectionParams($this->getConfig()));
+        }
+
+        if (!isset($this->_config['connectionStrategy'])) {
+            if (true === $this->getConfig('roundRobin')) {
+                $this->setConfigValue('connectionStrategy', 'RoundRobin');
+            } else {
+                $this->setConfigValue('connectionStrategy', 'Simple');
+            }
+        }
+
+        $strategy = Connection\Strategy\StrategyFactory::create($this->getConfig('connectionStrategy'));
+
+        $this->_connectionPool = new Connection\ConnectionPool($connections, $strategy, $this->_callback);
+    }
+
+    /**
+     * Creates a Connection params array from a Client or server config array.
+     *
+     * @param array $config
+     *
+     * @return array
+     */
+    protected function _prepareConnectionParams(array $config)
+    {
+        $params = [];
+        $params['config'] = [];
+        foreach ($config as $key => $value) {
+            if (in_array($key, ['bigintConversion', 'curl', 'headers', 'url'])) {
+                $params['config'][$key] = $value;
+            } else {
+                $params[$key] = $value;
+            }
+        }
+
+        return $params;
+    }
+
+    /**
+     * Sets specific config values (updates and keeps default values).
+     *
+     * @param array $config Params
+     *
+     * @return $this
+     */
+    public function setConfig(array $config)
+    {
+        foreach ($config as $key => $value) {
+            $this->_config[$key] = $value;
+        }
+
+        return $this;
+    }
+
+    /**
+     * Returns a specific config key or the whole
+     * config array if not set.
+     *
+     * @param string $key Config key
+     *
+     * @throws \Elastica\Exception\InvalidException
+     *
+     * @return array|string Config value
+     */
+    public function getConfig($key = '')
+    {
+        if (empty($key)) {
+            return $this->_config;
+        }
+
+        if (!array_key_exists($key, $this->_config)) {
+            throw new InvalidException('Config key is not set: '.$key);
+        }
+
+        return $this->_config[$key];
+    }
+
+    /**
+     * Sets / overwrites a specific config value.
+     *
+     * @param string $key   Key to set
+     * @param mixed  $value Value
+     *
+     * @return $this
+     */
+    public function setConfigValue($key, $value)
+    {
+        return $this->setConfig([$key => $value]);
+    }
+
+    /**
+     * @param array|string $keys    config key or path of config keys
+     * @param mixed        $default default value will be returned if key was not found
+     *
+     * @return mixed
+     */
+    public function getConfigValue($keys, $default = null)
+    {
+        $value = $this->_config;
+        foreach ((array) $keys as $key) {
+            if (isset($value[$key])) {
+                $value = $value[$key];
+            } else {
+                return $default;
+            }
+        }
+
+        return $value;
+    }
+
+    /**
+     * Returns the index for the given connection.
+     *
+     * @param string $name Index name to create connection to
+     *
+     * @return \Elastica\Index Index for the given name
+     */
+    public function getIndex($name)
+    {
+        return new Index($this, $name);
+    }
+
+    /**
+     * Adds a HTTP Header.
+     *
+     * @param string $header      The HTTP Header
+     * @param string $headerValue The HTTP Header Value
+     *
+     * @throws \Elastica\Exception\InvalidException If $header or $headerValue is not a string
+     *
+     * @return $this
+     */
+    public function addHeader($header, $headerValue)
+    {
+        if (is_string($header) && is_string($headerValue)) {
+            $this->_config['headers'][$header] = $headerValue;
+        } else {
+            throw new InvalidException('Header must be a string');
+        }
+
+        return $this;
+    }
+
+    /**
+     * Remove a HTTP Header.
+     *
+     * @param string $header The HTTP Header to remove
+     *
+     * @throws \Elastica\Exception\InvalidException If $header is not a string
+     *
+     * @return $this
+     */
+    public function removeHeader($header)
+    {
+        if (is_string($header)) {
+            if (array_key_exists($header, $this->_config['headers'])) {
+                unset($this->_config['headers'][$header]);
+            }
+        } else {
+            throw new InvalidException('Header must be a string');
+        }
+
+        return $this;
+    }
+
+    /**
+     * Uses _bulk to send documents to the server.
+     *
+     * Array of \Elastica\Document as input. Index and type has to be
+     * set inside the document, because for bulk settings documents,
+     * documents can belong to any type and index
+     *
+     * @see https://www.elastic.co/guide/en/elasticsearch/reference/current/docs-bulk.html
+     *
+     * @param array|\Elastica\Document[] $docs          Array of Elastica\Document
+     * @param array                      $requestParams
+     *
+     * @throws \Elastica\Exception\InvalidException If docs is empty
+     *
+     * @return \Elastica\Bulk\ResponseSet Response object
+     */
+    public function updateDocuments(array $docs, array $requestParams = [])
+    {
+        if (empty($docs)) {
+            throw new InvalidException('Array has to consist of at least one element');
+        }
+
+        $bulk = new Bulk($this);
+
+        $bulk->addDocuments($docs, Action::OP_TYPE_UPDATE);
+        foreach ($requestParams as $key => $value) {
+            $bulk->setRequestParam($key, $value);
+        }
+
+        return $bulk->send();
+    }
+
+    /**
+     * Uses _bulk to send documents to the server.
+     *
+     * Array of \Elastica\Document as input. Index and type has to be
+     * set inside the document, because for bulk settings documents,
+     * documents can belong to any type and index
+     *
+     * @see https://www.elastic.co/guide/en/elasticsearch/reference/current/docs-bulk.html
+     *
+     * @param array|\Elastica\Document[] $docs          Array of Elastica\Document
+     * @param array                      $requestParams
+     *
+     * @throws \Elastica\Exception\InvalidException If docs is empty
+     *
+     * @return \Elastica\Bulk\ResponseSet Response object
+     */
+    public function addDocuments(array $docs, array $requestParams = [])
+    {
+        if (empty($docs)) {
+            throw new InvalidException('Array has to consist of at least one element');
+        }
+
+        $bulk = new Bulk($this);
+
+        $bulk->addDocuments($docs);
+
+        foreach ($requestParams as $key => $value) {
+            $bulk->setRequestParam($key, $value);
+        }
+
+        return $bulk->send();
+    }
+
+    /**
+     * Update document, using update script. Requires elasticsearch >= 0.19.0.
+     *
+     * @param int|string                                               $id      document id
+     * @param array|\Elastica\Script\AbstractScript|\Elastica\Document $data    raw data for request body
+     * @param string                                                   $index   index to update
+     * @param string                                                   $type    type of index to update
+     * @param array                                                    $options array of query params to use for query. For possible options check es api
+     *
+     * @return \Elastica\Response
+     *
+     * @see https://www.elastic.co/guide/en/elasticsearch/reference/current/docs-update.html
+     */
+    public function updateDocument($id, $data, $index, $type, array $options = [])
+    {
+        $endpoint = new Update();
+        $endpoint->setID($id);
+        $endpoint->setIndex($index);
+        $endpoint->setType($type);
+
+        if ($data instanceof AbstractScript) {
+            $requestData = $data->toArray();
+        } elseif ($data instanceof Document) {
+            $requestData = ['doc' => $data->getData()];
+
+            if ($data->getDocAsUpsert()) {
+                $requestData['doc_as_upsert'] = true;
+            }
+
+            $docOptions = $data->getOptions(
+                [
+                    'version',
+                    'version_type',
+                    'routing',
+                    'percolate',
+                    'parent',
+                    'fields',
+                    'retry_on_conflict',
+                    'consistency',
+                    'replication',
+                    'refresh',
+                    'timeout',
+                ]
+            );
+            $options += $docOptions;
+            // set fields param to source only if options was not set before
+            if ($data instanceof Document && ($data->isAutoPopulate()
+                || $this->getConfigValue(['document', 'autoPopulate'], false))
+                && !isset($options['fields'])
+            ) {
+                $options['fields'] = '_source';
+            }
+        } else {
+            $requestData = $data;
+        }
+
+        //If an upsert document exists
+        if ($data instanceof AbstractScript || $data instanceof Document) {
+            if ($data->hasUpsert()) {
+                $requestData['upsert'] = $data->getUpsert()->getData();
+            }
+        }
+
+        if (!isset($options['retry_on_conflict'])) {
+            if ($retryOnConflict = $this->getConfig('retryOnConflict')) {
+                $options['retry_on_conflict'] = $retryOnConflict;
+            }
+        }
+
+        $endpoint->setBody($requestData);
+        $endpoint->setParams($options);
+
+        $response = $this->requestEndpoint($endpoint);
+
+        if ($response->isOk()
+            && $data instanceof Document
+            && ($data->isAutoPopulate() || $this->getConfigValue(['document', 'autoPopulate'], false))
+        ) {
+            $responseData = $response->getData();
+            if (isset($responseData['_version'])) {
+                $data->setVersion($responseData['_version']);
+            }
+            if (isset($options['fields'])) {
+                $this->_populateDocumentFieldsFromResponse($response, $data, $options['fields']);
+            }
+        }
+
+        return $response;
+    }
+
+    /**
+     * @param \Elastica\Response $response
+     * @param \Elastica\Document $document
+     * @param string             $fields   Array of field names to be populated or '_source' if whole document data should be updated
+     */
+    protected function _populateDocumentFieldsFromResponse(Response $response, Document $document, $fields)
+    {
+        $responseData = $response->getData();
+        if ('_source' == $fields) {
+            if (isset($responseData['get']['_source']) && is_array($responseData['get']['_source'])) {
+                $document->setData($responseData['get']['_source']);
+            }
+        } else {
+            $keys = explode(',', $fields);
+            $data = $document->getData();
+            foreach ($keys as $key) {
+                if (isset($responseData['get']['fields'][$key])) {
+                    $data[$key] = $responseData['get']['fields'][$key];
+                } elseif (isset($data[$key])) {
+                    unset($data[$key]);
+                }
+            }
+            $document->setData($data);
+        }
+    }
+
+    /**
+     * Bulk deletes documents.
+     *
+     * @param array|\Elastica\Document[] $docs
+     * @param array                      $requestParams
+     *
+     * @throws \Elastica\Exception\InvalidException
+     *
+     * @return \Elastica\Bulk\ResponseSet
+     */
+    public function deleteDocuments(array $docs, array $requestParams = [])
+    {
+        if (empty($docs)) {
+            throw new InvalidException('Array has to consist of at least one element');
+        }
+
+        $bulk = new Bulk($this);
+        $bulk->addDocuments($docs, Action::OP_TYPE_DELETE);
+
+        foreach ($requestParams as $key => $value) {
+            $bulk->setRequestParam($key, $value);
+        }
+
+        return $bulk->send();
+    }
+
+    /**
+     * Returns the status object for all indices.
+     *
+     * @return \Elastica\Status Status object
+     */
+    public function getStatus()
+    {
+        return new Status($this);
+    }
+
+    /**
+     * Returns the current cluster.
+     *
+     * @return \Elastica\Cluster Cluster object
+     */
+    public function getCluster()
+    {
+        return new Cluster($this);
+    }
+
+    /**
+     * Establishes the client connections.
+     */
+    public function connect()
+    {
+        return $this->_initConnections();
+    }
+
+    /**
+     * @param \Elastica\Connection $connection
+     *
+     * @return $this
+     */
+    public function addConnection(Connection $connection)
+    {
+        $this->_connectionPool->addConnection($connection);
+
+        return $this;
+    }
+
+    /**
+     * Determines whether a valid connection is available for use.
+     *
+     * @return bool
+     */
+    public function hasConnection()
+    {
+        return $this->_connectionPool->hasConnection();
+    }
+
+    /**
+     * @throws \Elastica\Exception\ClientException
+     *
+     * @return \Elastica\Connection
+     */
+    public function getConnection()
+    {
+        return $this->_connectionPool->getConnection();
+    }
+
+    /**
+     * @return \Elastica\Connection[]
+     */
+    public function getConnections()
+    {
+        return $this->_connectionPool->getConnections();
+    }
+
+    /**
+     * @return \Elastica\Connection\Strategy\StrategyInterface
+     */
+    public function getConnectionStrategy()
+    {
+        return $this->_connectionPool->getStrategy();
+    }
+
+    /**
+     * @param array|\Elastica\Connection[] $connections
+     *
+     * @return $this
+     */
+    public function setConnections(array $connections)
+    {
+        $this->_connectionPool->setConnections($connections);
+
+        return $this;
+    }
+
+    /**
+     * Deletes documents with the given ids, index, type from the index.
+     *
+     * @see https://www.elastic.co/guide/en/elasticsearch/reference/current/docs-bulk.html
+     *
+     * @param array                  $ids     Document ids
+     * @param string|\Elastica\Index $index   Index name
+     * @param string|\Elastica\Type  $type    Type of documents
+     * @param string|bool            $routing Optional routing key for all ids
+     *
+     * @throws \Elastica\Exception\InvalidException
+     *
+     * @return \Elastica\Bulk\ResponseSet Response  object
+     */
+    public function deleteIds(array $ids, $index, $type, $routing = false)
+    {
+        if (empty($ids)) {
+            throw new InvalidException('Array has to consist of at least one id');
+        }
+
+        $bulk = new Bulk($this);
+        $bulk->setIndex($index);
+        $bulk->setType($type);
+
+        foreach ($ids as $id) {
+            $action = new Action(Action::OP_TYPE_DELETE);
+            $action->setId($id);
+
+            if (!empty($routing)) {
+                $action->setRouting($routing);
+            }
+
+            $bulk->addAction($action);
+        }
+
+        return $bulk->send();
+    }
+
+    /**
+     * Bulk operation.
+     *
+     * Every entry in the params array has to exactly on array
+     * of the bulk operation. An example param array would be:
+     *
+     * array(
+     *         array('index' => array('_index' => 'test', '_type' => 'user', '_id' => '1')),
+     *         array('user' => array('name' => 'hans')),
+     *         array('delete' => array('_index' => 'test', '_type' => 'user', '_id' => '2'))
+     * );
+     *
+     * @see https://www.elastic.co/guide/en/elasticsearch/reference/current/docs-bulk.html
+     *
+     * @param array $params Parameter array
+     *
+     * @throws \Elastica\Exception\ResponseException
+     * @throws \Elastica\Exception\InvalidException
+     *
+     * @return \Elastica\Bulk\ResponseSet Response object
+     */
+    public function bulk(array $params)
+    {
+        if (empty($params)) {
+            throw new InvalidException('Array has to consist of at least one param');
+        }
+
+        $bulk = new Bulk($this);
+
+        $bulk->addRawData($params);
+
+        return $bulk->send();
+    }
+
+    /**
+     * Makes calls to the elasticsearch server based on this index.
+     *
+     * It's possible to make any REST query directly over this method
+     *
+     * @param string       $path        Path to call
+     * @param string       $method      Rest method to use (GET, POST, DELETE, PUT)
+     * @param array|string $data        OPTIONAL Arguments as array or pre-encoded string
+     * @param array        $query       OPTIONAL Query params
+     * @param string       $contentType Content-Type sent with this request
+     *
+     * @throws Exception\ConnectionException|Exception\ClientException
+     *
+     * @return Response Response object
+     */
+    public function request($path, $method = Request::GET, $data = [], array $query = [], $contentType = Request::DEFAULT_CONTENT_TYPE)
+    {
+        $connection = $this->getConnection();
+        $request = $this->_lastRequest = new Request($path, $method, $data, $query, $connection, $contentType);
+        $this->_lastResponse = null;
+
+        try {
+            $response = $this->_lastResponse = $request->send();
+        } catch (ConnectionException $e) {
+            $this->_connectionPool->onFail($connection, $e, $this);
+
+            $this->_log($e);
+
+            // In case there is no valid connection left, throw exception which caused the disabling of the connection.
+            if (!$this->hasConnection()) {
+                throw $e;
+            }
+
+            return $this->request($path, $method, $data, $query);
+        }
+
+        $this->_log($request);
+
+        return $response;
+    }
+
+    /**
+     * Makes calls to the elasticsearch server with usage official client Endpoint.
+     *
+     * @param AbstractEndpoint $endpoint
+     *
+     * @return Response
+     */
+    public function requestEndpoint(AbstractEndpoint $endpoint)
+    {
+        return $this->request(
+            ltrim($endpoint->getURI(), '/'),
+            $endpoint->getMethod(),
+            null === $endpoint->getBody() ? [] : $endpoint->getBody(),
+            $endpoint->getParams()
+        );
+    }
+
+    /**
+     * logging.
+     *
+     * @deprecated Overwriting Client->_log is deprecated. Handle logging functionality by using a custom LoggerInterface.
+     *
+     * @param mixed $context
+     */
+    protected function _log($context)
+    {
+        if ($context instanceof ConnectionException) {
+            $this->_logger->error('Elastica Request Failure', [
+                'exception' => $context,
+                'request' => $context->getRequest()->toArray(),
+                'retry' => $this->hasConnection(),
+            ]);
+
+            return;
+        }
+
+        if ($context instanceof Request) {
+            $this->_logger->debug('Elastica Request', [
+                'request' => $context->toArray(),
+                'response' => $this->_lastResponse ? $this->_lastResponse->getData() : null,
+                'responseStatus' => $this->_lastResponse ? $this->_lastResponse->getStatus() : null,
+            ]);
+
+            return;
+        }
+
+        $this->_logger->debug('Elastica Request', [
+            'message' => $context,
+        ]);
+    }
+
+    /**
+     * Optimizes all search indices.
+     *
+     * @param array $args OPTIONAL Optional arguments
+     *
+     * @return \Elastica\Response Response object
+     *
+     * @deprecated Replaced by forcemergeAll
+     * @see https://www.elastic.co/guide/en/elasticsearch/reference/current/indices-optimize.html
+     */
+    public function optimizeAll($args = [])
+    {
+        trigger_error('Deprecated: Elastica\Client::optimizeAll() is deprecated and will be removed in further Elastica releases. Use Elastica\Client::forcemergeAll() instead.', E_USER_DEPRECATED);
+
+        return $this->forcemergeAll($args);
+    }
+
+    /**
+     * Force merges all search indices.
+     *
+     * @param array $args OPTIONAL Optional arguments
+     *
+     * @return \Elastica\Response Response object
+     *
+     * @see https://www.elastic.co/guide/en/elasticsearch/reference/current/indices-forcemerge.html
+     */
+    public function forcemergeAll($args = [])
+    {
+        $endpoint = new ForceMerge();
+        $endpoint->setParams($args);
+
+        return $this->requestEndpoint($endpoint);
+    }
+
+    /**
+     * Refreshes all search indices.
+     *
+     * @return \Elastica\Response Response object
+     *
+     * @see https://www.elastic.co/guide/en/elasticsearch/reference/current/indices-refresh.html
+     */
+    public function refreshAll()
+    {
+        return $this->requestEndpoint(new Refresh());
+    }
+
+    /**
+     * @return Request|null
+     */
+    public function getLastRequest()
+    {
+        return $this->_lastRequest;
+    }
+
+    /**
+     * @return Response|null
+     */
+    public function getLastResponse()
+    {
+        return $this->_lastResponse;
+    }
+
+    /**
+     * Replace the existing logger.
+     *
+     * @param LoggerInterface $logger
+     *
+     * @return $this
+     */
+    public function setLogger(LoggerInterface $logger)
+    {
+        $this->_logger = $logger;
+
+        return $this;
+    }
+}

+ 170 - 0
lib/Elastica/Cluster.php

@@ -0,0 +1,170 @@
+<?php
+
+namespace Elastica;
+
+use Elastica\Cluster\Health;
+use Elastica\Cluster\Settings;
+use Elastica\Exception\NotImplementedException;
+use Elasticsearch\Endpoints\Cluster\State;
+
+/**
+ * Cluster information for elasticsearch.
+ *
+ * @author Nicolas Ruflin <spam@ruflin.com>
+ *
+ * @see https://www.elastic.co/guide/en/elasticsearch/reference/current/cluster.html
+ */
+class Cluster
+{
+    /**
+     * Client.
+     *
+     * @var \Elastica\Client Client object
+     */
+    protected $_client;
+
+    /**
+     * Cluster state response.
+     *
+     * @var \Elastica\Response
+     */
+    protected $_response;
+
+    /**
+     * Cluster state data.
+     *
+     * @var array
+     */
+    protected $_data;
+
+    /**
+     * Creates a cluster object.
+     *
+     * @param \Elastica\Client $client Connection client object
+     */
+    public function __construct(Client $client)
+    {
+        $this->_client = $client;
+        $this->refresh();
+    }
+
+    /**
+     * Refreshes all cluster information (state).
+     */
+    public function refresh()
+    {
+        $this->_response = $this->_client->requestEndpoint(new State());
+        $this->_data = $this->getResponse()->getData();
+    }
+
+    /**
+     * Returns the response object.
+     *
+     * @return \Elastica\Response Response object
+     */
+    public function getResponse()
+    {
+        return $this->_response;
+    }
+
+    /**
+     * Return list of index names.
+     *
+     * @return array List of index names
+     */
+    public function getIndexNames()
+    {
+        return array_keys($this->_data['metadata']['indices']);
+    }
+
+    /**
+     * Returns the full state of the cluster.
+     *
+     * @return array State array
+     *
+     * @see https://www.elastic.co/guide/en/elasticsearch/reference/current/cluster-state.html
+     */
+    public function getState()
+    {
+        return $this->_data;
+    }
+
+    /**
+     * Returns a list of existing node names.
+     *
+     * @return array List of node names
+     */
+    public function getNodeNames()
+    {
+        $data = $this->getState();
+        $nodeNames = [];
+        foreach ($data['nodes'] as $node) {
+            $nodeNames[] = $node['name'];
+        }
+
+        return $nodeNames;
+    }
+
+    /**
+     * Returns all nodes of the cluster.
+     *
+     * @return \Elastica\Node[]
+     */
+    public function getNodes()
+    {
+        $nodes = [];
+        $data = $this->getState();
+
+        foreach ($data['nodes'] as $id => $name) {
+            $nodes[] = new Node($id, $this->getClient());
+        }
+
+        return $nodes;
+    }
+
+    /**
+     * Returns the client object.
+     *
+     * @return \Elastica\Client Client object
+     */
+    public function getClient()
+    {
+        return $this->_client;
+    }
+
+    /**
+     * Returns the cluster information (not implemented yet).
+     *
+     * @see https://www.elastic.co/guide/en/elasticsearch/reference/current/cluster-nodes-info.html
+     *
+     * @param array $args Additional arguments
+     *
+     * @throws \Elastica\Exception\NotImplementedException
+     */
+    public function getInfo(array $args)
+    {
+        throw new NotImplementedException('not implemented yet');
+    }
+
+    /**
+     * Return Cluster health.
+     *
+     * @see https://www.elastic.co/guide/en/elasticsearch/reference/current/cluster-health.html
+     *
+     * @return \Elastica\Cluster\Health
+     */
+    public function getHealth()
+    {
+        return new Health($this->getClient());
+    }
+
+    /**
+     * Return Cluster settings.
+     *
+     * @return \Elastica\Cluster\Settings
+     */
+    public function getSettings()
+    {
+        return new Settings($this->getClient());
+    }
+}

+ 229 - 0
lib/Elastica/Cluster/Health.php

@@ -0,0 +1,229 @@
+<?php
+
+namespace Elastica\Cluster;
+
+use Elastica\Client;
+use Elastica\Cluster\Health\Index;
+
+/**
+ * Elastic cluster health.
+ *
+ * @author Ray Ward <ray.ward@bigcommerce.com>
+ *
+ * @see https://www.elastic.co/guide/en/elasticsearch/reference/current/cluster-health.html
+ */
+class Health
+{
+    /**
+     * @var \Elastica\Client client object
+     */
+    protected $_client;
+
+    /**
+     * @var array the cluster health data
+     */
+    protected $_data;
+
+    /**
+     * @param \Elastica\Client $client the Elastica client
+     */
+    public function __construct(Client $client)
+    {
+        $this->_client = $client;
+        $this->refresh();
+    }
+
+    /**
+     * Retrieves the health data from the cluster.
+     *
+     * @return array
+     */
+    protected function _retrieveHealthData()
+    {
+        $endpoint = new \Elasticsearch\Endpoints\Cluster\Health();
+        $endpoint->setParams(['level' => 'shards']);
+
+        $response = $this->_client->requestEndpoint($endpoint);
+
+        return $response->getData();
+    }
+
+    /**
+     * Gets the health data.
+     *
+     * @return array
+     */
+    public function getData()
+    {
+        return $this->_data;
+    }
+
+    /**
+     * Refreshes the health data for the cluster.
+     *
+     * @return $this
+     */
+    public function refresh()
+    {
+        $this->_data = $this->_retrieveHealthData();
+
+        return $this;
+    }
+
+    /**
+     * Gets the name of the cluster.
+     *
+     * @return string
+     */
+    public function getClusterName()
+    {
+        return $this->_data['cluster_name'];
+    }
+
+    /**
+     * Gets the status of the cluster.
+     *
+     * @return string green, yellow or red
+     */
+    public function getStatus()
+    {
+        return $this->_data['status'];
+    }
+
+    /**
+     * TODO determine the purpose of this.
+     *
+     * @return bool
+     */
+    public function getTimedOut()
+    {
+        return $this->_data['timed_out'];
+    }
+
+    /**
+     * Gets the number of nodes in the cluster.
+     *
+     * @return int
+     */
+    public function getNumberOfNodes()
+    {
+        return $this->_data['number_of_nodes'];
+    }
+
+    /**
+     * Gets the number of data nodes in the cluster.
+     *
+     * @return int
+     */
+    public function getNumberOfDataNodes()
+    {
+        return $this->_data['number_of_data_nodes'];
+    }
+
+    /**
+     * Gets the number of active primary shards.
+     *
+     * @return int
+     */
+    public function getActivePrimaryShards()
+    {
+        return $this->_data['active_primary_shards'];
+    }
+
+    /**
+     * Gets the number of active shards.
+     *
+     * @return int
+     */
+    public function getActiveShards()
+    {
+        return $this->_data['active_shards'];
+    }
+
+    /**
+     * Gets the number of relocating shards.
+     *
+     * @return int
+     */
+    public function getRelocatingShards()
+    {
+        return $this->_data['relocating_shards'];
+    }
+
+    /**
+     * Gets the number of initializing shards.
+     *
+     * @return int
+     */
+    public function getInitializingShards()
+    {
+        return $this->_data['initializing_shards'];
+    }
+
+    /**
+     * Gets the number of unassigned shards.
+     *
+     * @return int
+     */
+    public function getUnassignedShards()
+    {
+        return $this->_data['unassigned_shards'];
+    }
+
+    /**
+     * get the number of delayed unassined shards.
+     *
+     * @return int
+     */
+    public function getDelayedUnassignedShards()
+    {
+        return $this->_data['delayed_unassigned_shards'];
+    }
+
+    /**
+     * @return int
+     */
+    public function getNumberOfPendingTasks()
+    {
+        return $this->_data['number_of_pending_tasks'];
+    }
+
+    /**
+     * @return int
+     */
+    public function getNumberOfInFlightFetch()
+    {
+        return $this->_data['number_of_in_flight_fetch'];
+    }
+
+    /**
+     * @return int
+     */
+    public function getTaskMaxWaitingInQueueMillis()
+    {
+        return $this->_data['task_max_waiting_in_queue_millis'];
+    }
+
+    /**
+     * @return int
+     */
+    public function getActiveShardsPercentAsNumber()
+    {
+        return $this->_data['active_shards_percent_as_number'];
+    }
+
+    /**
+     * Gets the status of the indices.
+     *
+     * @return \Elastica\Cluster\Health\Index[]
+     */
+    public function getIndices()
+    {
+        $indices = [];
+        foreach ($this->_data['indices'] as $indexName => $index) {
+            $indices[$indexName] = new Index($indexName, $index);
+        }
+
+        return $indices;
+    }
+}

+ 138 - 0
lib/Elastica/Cluster/Health/Index.php

@@ -0,0 +1,138 @@
+<?php
+
+namespace Elastica\Cluster\Health;
+
+/**
+ * Wraps status information for an index.
+ *
+ * @author Ray Ward <ray.ward@bigcommerce.com>
+ *
+ * @see https://www.elastic.co/guide/en/elasticsearch/reference/current/cluster-health.html
+ */
+class Index
+{
+    /**
+     * @var string the name of the index
+     */
+    protected $_name;
+
+    /**
+     * @var array the index health data
+     */
+    protected $_data;
+
+    /**
+     * @param string $name the name of the index
+     * @param array  $data the index health data
+     */
+    public function __construct($name, $data)
+    {
+        $this->_name = $name;
+        $this->_data = $data;
+    }
+
+    /**
+     * Gets the name of the index.
+     *
+     * @return string
+     */
+    public function getName()
+    {
+        return $this->_name;
+    }
+
+    /**
+     * Gets the status of the index.
+     *
+     * @return string green, yellow or red
+     */
+    public function getStatus()
+    {
+        return $this->_data['status'];
+    }
+
+    /**
+     * Gets the number of nodes in the index.
+     *
+     * @return int
+     */
+    public function getNumberOfShards()
+    {
+        return $this->_data['number_of_shards'];
+    }
+
+    /**
+     * Gets the number of data nodes in the index.
+     *
+     * @return int
+     */
+    public function getNumberOfReplicas()
+    {
+        return $this->_data['number_of_replicas'];
+    }
+
+    /**
+     * Gets the number of active primary shards.
+     *
+     * @return int
+     */
+    public function getActivePrimaryShards()
+    {
+        return $this->_data['active_primary_shards'];
+    }
+
+    /**
+     * Gets the number of active shards.
+     *
+     * @return int
+     */
+    public function getActiveShards()
+    {
+        return $this->_data['active_shards'];
+    }
+
+    /**
+     * Gets the number of relocating shards.
+     *
+     * @return int
+     */
+    public function getRelocatingShards()
+    {
+        return $this->_data['relocating_shards'];
+    }
+
+    /**
+     * Gets the number of initializing shards.
+     *
+     * @return int
+     */
+    public function getInitializingShards()
+    {
+        return $this->_data['initializing_shards'];
+    }
+
+    /**
+     * Gets the number of unassigned shards.
+     *
+     * @return int
+     */
+    public function getUnassignedShards()
+    {
+        return $this->_data['unassigned_shards'];
+    }
+
+    /**
+     * Gets the health of the shards in this index.
+     *
+     * @return \Elastica\Cluster\Health\Shard[]
+     */
+    public function getShards()
+    {
+        $shards = [];
+        foreach ($this->_data['shards'] as $shardNumber => $shard) {
+            $shards[] = new Shard($shardNumber, $shard);
+        }
+
+        return $shards;
+    }
+}

+ 103 - 0
lib/Elastica/Cluster/Health/Shard.php

@@ -0,0 +1,103 @@
+<?php
+
+namespace Elastica\Cluster\Health;
+
+/**
+ * Wraps status information for a shard.
+ *
+ * @author Ray Ward <ray.ward@bigcommerce.com>
+ *
+ * @see https://www.elastic.co/guide/en/elasticsearch/reference/current/cluster-health.html
+ */
+class Shard
+{
+    /**
+     * @var int the shard index/number
+     */
+    protected $_shardNumber;
+
+    /**
+     * @var array the shard health data
+     */
+    protected $_data;
+
+    /**
+     * @param int   $shardNumber the shard index/number
+     * @param array $data        the shard health data
+     */
+    public function __construct($shardNumber, $data)
+    {
+        $this->_shardNumber = $shardNumber;
+        $this->_data = $data;
+    }
+
+    /**
+     * Gets the index/number of this shard.
+     *
+     * @return int
+     */
+    public function getShardNumber()
+    {
+        return $this->_shardNumber;
+    }
+
+    /**
+     * Gets the status of this shard.
+     *
+     * @return string green, yellow or red
+     */
+    public function getStatus()
+    {
+        return $this->_data['status'];
+    }
+
+    /**
+     * Is the primary active?
+     *
+     * @return bool
+     */
+    public function isPrimaryActive()
+    {
+        return $this->_data['primary_active'];
+    }
+
+    /**
+     * Is this shard active?
+     *
+     * @return bool
+     */
+    public function isActive()
+    {
+        return 1 == $this->_data['active_shards'];
+    }
+
+    /**
+     * Is this shard relocating?
+     *
+     * @return bool
+     */
+    public function isRelocating()
+    {
+        return 1 == $this->_data['relocating_shards'];
+    }
+
+    /**
+     * Is this shard initialized?
+     *
+     * @return bool
+     */
+    public function isInitialized()
+    {
+        return 1 == $this->_data['initializing_shards'];
+    }
+
+    /**
+     * Is this shard unassigned?
+     *
+     * @return bool
+     */
+    public function isUnassigned()
+    {
+        return 1 == $this->_data['unassigned_shards'];
+    }
+}

+ 199 - 0
lib/Elastica/Cluster/Settings.php

@@ -0,0 +1,199 @@
+<?php
+
+namespace Elastica\Cluster;
+
+use Elastica\Client;
+use Elastica\Request;
+
+/**
+ * Cluster settings.
+ *
+ * @author   Nicolas Ruflin <spam@ruflin.com>
+ *
+ * @see     https://www.elastic.co/guide/en/elasticsearch/reference/current/cluster-update-settings.html
+ */
+class Settings
+{
+    /**
+     * @var \Elastica\Client Client object
+     */
+    protected $_client = null;
+
+    /**
+     * Creates a cluster object.
+     *
+     * @param \Elastica\Client $client Connection client object
+     */
+    public function __construct(Client $client)
+    {
+        $this->_client = $client;
+    }
+
+    /**
+     * Returns settings data.
+     *
+     * @return array Settings data (persistent and transient)
+     */
+    public function get()
+    {
+        return $this->request()->getData();
+    }
+
+    /**
+     * Returns the current persistent settings of the cluster.
+     *
+     * If param is set, only specified setting is return.
+     *
+     * @param string $setting OPTIONAL Setting name to return
+     *
+     * @return array|string|null Settings data
+     */
+    public function getPersistent($setting = '')
+    {
+        $data = $this->get();
+        $settings = $data['persistent'];
+
+        if (!empty($setting)) {
+            if (isset($settings[$setting])) {
+                return $settings[$setting];
+            }
+
+            return;
+        }
+
+        return $settings;
+    }
+
+    /**
+     * Returns the current transient settings of the cluster.
+     *
+     * If param is set, only specified setting is return.
+     *
+     * @param string $setting OPTIONAL Setting name to return
+     *
+     * @return array|string|null Settings data
+     */
+    public function getTransient($setting = '')
+    {
+        $data = $this->get();
+        $settings = $data['transient'];
+
+        if (!empty($setting)) {
+            if (isset($settings[$setting])) {
+                return $settings[$setting];
+            }
+
+            if (false !== strpos($setting, '.')) {
+                // convert dot notation to nested arrays
+                $keys = explode('.', $setting);
+                foreach ($keys as $key) {
+                    if (isset($settings[$key])) {
+                        $settings = $settings[$key];
+                    } else {
+                        return;
+                    }
+                }
+
+                return $settings;
+            }
+
+            return;
+        }
+
+        return $settings;
+    }
+
+    /**
+     * Sets persistent setting.
+     *
+     * @param string $key
+     * @param string $value
+     *
+     * @return \Elastica\Response
+     */
+    public function setPersistent($key, $value)
+    {
+        return $this->set(
+            [
+                'persistent' => [
+                    $key => $value,
+                ],
+            ]
+        );
+    }
+
+    /**
+     * Sets transient settings.
+     *
+     * @param string $key
+     * @param string $value
+     *
+     * @return \Elastica\Response
+     */
+    public function setTransient($key, $value)
+    {
+        return $this->set(
+            [
+                'transient' => [
+                    $key => $value,
+                ],
+            ]
+        );
+    }
+
+    /**
+     * Sets the cluster to read only.
+     *
+     * Second param can be used to set it persistent
+     *
+     * @param bool $readOnly
+     * @param bool $persistent
+     *
+     * @return \Elastica\Response $response
+     */
+    public function setReadOnly($readOnly = true, $persistent = false)
+    {
+        $key = 'cluster.blocks.read_only';
+
+        return $persistent
+            ? $this->setPersistent($key, $readOnly)
+            : $this->setTransient($key, $readOnly);
+    }
+
+    /**
+     * Set settings for cluster.
+     *
+     * @param array $settings Raw settings (including persistent or transient)
+     *
+     * @return \Elastica\Response
+     */
+    public function set(array $settings)
+    {
+        return $this->request($settings, Request::PUT);
+    }
+
+    /**
+     * Get the client.
+     *
+     * @return \Elastica\Client
+     */
+    public function getClient()
+    {
+        return $this->_client;
+    }
+
+    /**
+     * Sends settings request.
+     *
+     * @param array  $data   OPTIONAL Data array
+     * @param string $method OPTIONAL Transfer method (default = \Elastica\Request::GET)
+     *
+     * @return \Elastica\Response Response object
+     */
+    public function request(array $data = [], $method = Request::GET)
+    {
+        $path = '_cluster/settings';
+
+        return $this->getClient()->request($path, $method, $data);
+    }
+}

+ 360 - 0
lib/Elastica/Connection.php

@@ -0,0 +1,360 @@
+<?php
+
+namespace Elastica;
+
+use Elastica\Exception\InvalidException;
+use Elastica\Transport\AbstractTransport;
+
+/**
+ * Elastica connection instance to an elasticasearch node.
+ *
+ * @author   Nicolas Ruflin <spam@ruflin.com>
+ */
+class Connection extends Param
+{
+    /**
+     * Default elastic search port.
+     */
+    const DEFAULT_PORT = 9200;
+
+    /**
+     * Default host.
+     */
+    const DEFAULT_HOST = 'localhost';
+
+    /**
+     * Default transport.
+     *
+     * @var string
+     */
+    const DEFAULT_TRANSPORT = 'Http';
+
+    /**
+     * Default compression.
+     *
+     * @var string
+     */
+    const DEFAULT_COMPRESSION = false;
+
+    /**
+     * Number of seconds after a timeout occurs for every request
+     * If using indexing of file large value necessary.
+     */
+    const TIMEOUT = 300;
+
+    /**
+     * Number of seconds after a connection timeout occurs for every request during the connection phase.
+     *
+     * @see Connection::setConnectTimeout();
+     */
+    const CONNECT_TIMEOUT = 0;
+
+    /**
+     * Creates a new connection object. A connection is enabled by default.
+     *
+     * @param array $params OPTIONAL Connection params: host, port, transport, timeout. All are optional
+     */
+    public function __construct(array $params = [])
+    {
+        $this->setParams($params);
+        $this->setEnabled(true);
+
+        // Set empty config param if not exists
+        if (!$this->hasParam('config')) {
+            $this->setParam('config', []);
+        }
+    }
+
+    /**
+     * @return int Server port
+     */
+    public function getPort()
+    {
+        return $this->hasParam('port') ? $this->getParam('port') : self::DEFAULT_PORT;
+    }
+
+    /**
+     * @param int $port
+     *
+     * @return $this
+     */
+    public function setPort($port)
+    {
+        return $this->setParam('port', (int) $port);
+    }
+
+    /**
+     * @return string Host
+     */
+    public function getHost()
+    {
+        return $this->hasParam('host') ? $this->getParam('host') : self::DEFAULT_HOST;
+    }
+
+    /**
+     * @param string $host
+     *
+     * @return $this
+     */
+    public function setHost($host)
+    {
+        return $this->setParam('host', $host);
+    }
+
+    /**
+     * @return string|null Host
+     */
+    public function getProxy()
+    {
+        return $this->hasParam('proxy') ? $this->getParam('proxy') : null;
+    }
+
+    /**
+     * Set proxy for http connections. Null is for environmental proxy,
+     * empty string to disable proxy and proxy string to set actual http proxy.
+     *
+     * @see http://curl.haxx.se/libcurl/c/curl_easy_setopt.html#CURLOPTPROXY
+     *
+     * @param string|null $proxy
+     *
+     * @return $this
+     */
+    public function setProxy($proxy)
+    {
+        return $this->setParam('proxy', $proxy);
+    }
+
+    /**
+     * @return string|array
+     */
+    public function getTransport()
+    {
+        return $this->hasParam('transport') ? $this->getParam('transport') : self::DEFAULT_TRANSPORT;
+    }
+
+    /**
+     * @param string|array $transport
+     *
+     * @return $this
+     */
+    public function setTransport($transport)
+    {
+        return $this->setParam('transport', $transport);
+    }
+
+    /**
+     * @return bool
+     */
+    public function hasCompression()
+    {
+        return (bool) $this->hasParam('compression') ? $this->getParam('compression') : self::DEFAULT_COMPRESSION;
+    }
+
+    /**
+     * @param bool $compression
+     *
+     * @return $this
+     */
+    public function setCompression($compression = null)
+    {
+        return $this->setParam('compression', $compression);
+    }
+
+    /**
+     * @return string
+     */
+    public function getPath()
+    {
+        return $this->hasParam('path') ? $this->getParam('path') : '';
+    }
+
+    /**
+     * @param string $path
+     *
+     * @return $this
+     */
+    public function setPath($path)
+    {
+        return $this->setParam('path', $path);
+    }
+
+    /**
+     * @param int $timeout Timeout in seconds
+     *
+     * @return $this
+     */
+    public function setTimeout($timeout)
+    {
+        return $this->setParam('timeout', $timeout);
+    }
+
+    /**
+     * @return int Connection timeout in seconds
+     */
+    public function getTimeout()
+    {
+        return (int) $this->hasParam('timeout') ? $this->getParam('timeout') : self::TIMEOUT;
+    }
+
+    /**
+     * Number of seconds after a connection timeout occurs for every request during the connection phase.
+     * Use a small value if you need a fast fail in case of dead, unresponsive or unreachable servers (~5 sec).
+     *
+     * Set to zero to switch to the default built-in connection timeout (300 seconds in curl).
+     *
+     * @see http://curl.haxx.se/libcurl/c/CURLOPT_CONNECTTIMEOUT.html
+     *
+     * @param int $timeout Connect timeout in seconds
+     *
+     * @return $this
+     */
+    public function setConnectTimeout($timeout)
+    {
+        return $this->setParam('connectTimeout', $timeout);
+    }
+
+    /**
+     * @return int Connection timeout in seconds
+     */
+    public function getConnectTimeout()
+    {
+        return (int) $this->hasParam('connectTimeout') ? $this->getParam('connectTimeout') : self::CONNECT_TIMEOUT;
+    }
+
+    /**
+     * Enables a connection.
+     *
+     * @param bool $enabled OPTIONAL (default = true)
+     *
+     * @return $this
+     */
+    public function setEnabled($enabled = true)
+    {
+        return $this->setParam('enabled', $enabled);
+    }
+
+    /**
+     * @return bool True if enabled
+     */
+    public function isEnabled()
+    {
+        return (bool) $this->getParam('enabled');
+    }
+
+    /**
+     * Returns an instance of the transport type.
+     *
+     * @throws \Elastica\Exception\InvalidException If invalid transport type
+     *
+     * @return \Elastica\Transport\AbstractTransport Transport object
+     */
+    public function getTransportObject()
+    {
+        $transport = $this->getTransport();
+
+        return AbstractTransport::create($transport, $this);
+    }
+
+    /**
+     * @return bool Returns true if connection is persistent. True by default
+     */
+    public function isPersistent()
+    {
+        return (bool) $this->hasParam('persistent') ? $this->getParam('persistent') : true;
+    }
+
+    /**
+     * @param array $config
+     *
+     * @return $this
+     */
+    public function setConfig(array $config)
+    {
+        return $this->setParam('config', $config);
+    }
+
+    /**
+     * @param string $key
+     * @param mixed  $value
+     *
+     * @return $this
+     */
+    public function addConfig($key, $value)
+    {
+        $this->_params['config'][$key] = $value;
+
+        return $this;
+    }
+
+    /**
+     * @param string $key
+     *
+     * @return bool
+     */
+    public function hasConfig($key)
+    {
+        $config = $this->getConfig();
+
+        return isset($config[$key]);
+    }
+
+    /**
+     * Returns a specific config key or the whole
+     * config array if not set.
+     *
+     * @param string $key Config key
+     *
+     * @throws \Elastica\Exception\InvalidException
+     *
+     * @return array|string Config value
+     */
+    public function getConfig($key = '')
+    {
+        $config = $this->getParam('config');
+        if (empty($key)) {
+            return $config;
+        }
+
+        if (!array_key_exists($key, $config)) {
+            throw new InvalidException('Config key is not set: '.$key);
+        }
+
+        return $config[$key];
+    }
+
+    /**
+     * @param \Elastica\Connection|array $params Params to create a connection
+     *
+     * @throws Exception\InvalidException
+     *
+     * @return self
+     */
+    public static function create($params = [])
+    {
+        if (is_array($params)) {
+            return new self($params);
+        }
+
+        if ($params instanceof self) {
+            return $params;
+        }
+
+        throw new InvalidException('Invalid data type');
+    }
+
+    /**
+     * @return string User
+     */
+    public function getUsername()
+    {
+        return $this->hasParam('username') ? $this->getParam('username') : null;
+    }
+
+    /**
+     * @return string Password
+     */
+    public function getPassword()
+    {
+        return $this->hasParam('password') ? $this->getParam('password') : null;
+    }
+}

+ 123 - 0
lib/Elastica/Connection/ConnectionPool.php

@@ -0,0 +1,123 @@
+<?php
+
+namespace Elastica\Connection;
+
+use Elastica\Client;
+use Elastica\Connection;
+use Elastica\Connection\Strategy\StrategyInterface;
+use Exception;
+
+/**
+ * Description of ConnectionPool.
+ *
+ * @author chabior
+ */
+class ConnectionPool
+{
+    /**
+     * @var array|\Elastica\Connection[] Connections array
+     */
+    protected $_connections;
+
+    /**
+     * @var \Elastica\Connection\Strategy\StrategyInterface Strategy for connection
+     */
+    protected $_strategy;
+
+    /**
+     * @var callback Function called on connection fail
+     */
+    protected $_callback;
+
+    /**
+     * @param array                                           $connections
+     * @param \Elastica\Connection\Strategy\StrategyInterface $strategy
+     * @param callback                                        $callback
+     */
+    public function __construct(array $connections, StrategyInterface $strategy, $callback = null)
+    {
+        $this->_connections = $connections;
+
+        $this->_strategy = $strategy;
+
+        $this->_callback = $callback;
+    }
+
+    /**
+     * @param \Elastica\Connection $connection
+     *
+     * @return $this
+     */
+    public function addConnection(Connection $connection)
+    {
+        $this->_connections[] = $connection;
+
+        return $this;
+    }
+
+    /**
+     * @param array|\Elastica\Connection[] $connections
+     *
+     * @return $this
+     */
+    public function setConnections(array $connections)
+    {
+        $this->_connections = $connections;
+
+        return $this;
+    }
+
+    /**
+     * @return bool
+     */
+    public function hasConnection()
+    {
+        foreach ($this->_connections as $connection) {
+            if ($connection->isEnabled()) {
+                return true;
+            }
+        }
+
+        return false;
+    }
+
+    /**
+     * @return array
+     */
+    public function getConnections()
+    {
+        return $this->_connections;
+    }
+
+    /**
+     * @throws \Elastica\Exception\ClientException
+     *
+     * @return \Elastica\Connection
+     */
+    public function getConnection()
+    {
+        return $this->_strategy->getConnection($this->getConnections());
+    }
+
+    /**
+     * @param \Elastica\Connection $connection
+     * @param \Exception           $e
+     * @param Client               $client
+     */
+    public function onFail(Connection $connection, Exception $e, Client $client)
+    {
+        $connection->setEnabled(false);
+
+        if ($this->_callback) {
+            call_user_func($this->_callback, $connection, $e, $client);
+        }
+    }
+
+    /**
+     * @return \Elastica\Connection\Strategy\StrategyInterface
+     */
+    public function getStrategy()
+    {
+        return $this->_strategy;
+    }
+}

+ 52 - 0
lib/Elastica/Connection/Strategy/CallbackStrategy.php

@@ -0,0 +1,52 @@
+<?php
+
+namespace Elastica\Connection\Strategy;
+
+use Elastica\Exception\InvalidException;
+
+/**
+ * Description of CallbackStrategy.
+ *
+ * @author chabior
+ */
+class CallbackStrategy implements StrategyInterface
+{
+    /**
+     * @var callable
+     */
+    protected $_callback;
+
+    /**
+     * @param callable $callback
+     *
+     * @throws \Elastica\Exception\InvalidException
+     */
+    public function __construct($callback)
+    {
+        if (!self::isValid($callback)) {
+            throw new InvalidException(sprintf('Callback should be a callable, %s given!', gettype($callback)));
+        }
+
+        $this->_callback = $callback;
+    }
+
+    /**
+     * @param array|\Elastica\Connection[] $connections
+     *
+     * @return \Elastica\Connection
+     */
+    public function getConnection($connections)
+    {
+        return call_user_func_array($this->_callback, [$connections]);
+    }
+
+    /**
+     * @param callable $callback
+     *
+     * @return bool
+     */
+    public static function isValid($callback)
+    {
+        return is_callable($callback);
+    }
+}

+ 25 - 0
lib/Elastica/Connection/Strategy/RoundRobin.php

@@ -0,0 +1,25 @@
+<?php
+
+namespace Elastica\Connection\Strategy;
+
+/**
+ * Description of RoundRobin.
+ *
+ * @author chabior
+ */
+class RoundRobin extends Simple
+{
+    /**
+     * @param array|\Elastica\Connection[] $connections
+     *
+     * @throws \Elastica\Exception\ClientException
+     *
+     * @return \Elastica\Connection
+     */
+    public function getConnection($connections)
+    {
+        shuffle($connections);
+
+        return parent::getConnection($connections);
+    }
+}

+ 31 - 0
lib/Elastica/Connection/Strategy/Simple.php

@@ -0,0 +1,31 @@
+<?php
+
+namespace Elastica\Connection\Strategy;
+
+use Elastica\Exception\ClientException;
+
+/**
+ * Description of SimpleStrategy.
+ *
+ * @author chabior
+ */
+class Simple implements StrategyInterface
+{
+    /**
+     * @param array|\Elastica\Connection[] $connections
+     *
+     * @throws \Elastica\Exception\ClientException
+     *
+     * @return \Elastica\Connection
+     */
+    public function getConnection($connections)
+    {
+        foreach ($connections as $connection) {
+            if ($connection->isEnabled()) {
+                return $connection;
+            }
+        }
+
+        throw new ClientException('No enabled connection');
+    }
+}

+ 46 - 0
lib/Elastica/Connection/Strategy/StrategyFactory.php

@@ -0,0 +1,46 @@
+<?php
+
+namespace Elastica\Connection\Strategy;
+
+use Elastica\Exception\InvalidException;
+
+/**
+ * Description of StrategyFactory.
+ *
+ * @author chabior
+ */
+class StrategyFactory
+{
+    /**
+     * @param mixed|callable|string|StrategyInterface $strategyName
+     *
+     * @throws \Elastica\Exception\InvalidException
+     *
+     * @return \Elastica\Connection\Strategy\StrategyInterface
+     */
+    public static function create($strategyName)
+    {
+        if ($strategyName instanceof StrategyInterface) {
+            return $strategyName;
+        }
+
+        if (CallbackStrategy::isValid($strategyName)) {
+            return new CallbackStrategy($strategyName);
+        }
+
+        if (is_string($strategyName)) {
+            $requiredInterface = '\\Elastica\\Connection\\Strategy\\StrategyInterface';
+            $predefinedStrategy = '\\Elastica\\Connection\\Strategy\\'.$strategyName;
+
+            if (class_exists($predefinedStrategy) && class_implements($predefinedStrategy, $requiredInterface)) {
+                return new $predefinedStrategy();
+            }
+
+            if (class_exists($strategyName) && class_implements($strategyName, $requiredInterface)) {
+                return new $strategyName();
+            }
+        }
+
+        throw new InvalidException('Can\'t create strategy instance by given argument');
+    }
+}

+ 18 - 0
lib/Elastica/Connection/Strategy/StrategyInterface.php

@@ -0,0 +1,18 @@
+<?php
+
+namespace Elastica\Connection\Strategy;
+
+/**
+ * Description of AbstractStrategy.
+ *
+ * @author chabior
+ */
+interface StrategyInterface
+{
+    /**
+     * @param array|\Elastica\Connection[] $connections
+     *
+     * @return \Elastica\Connection
+     */
+    public function getConnection($connections);
+}

+ 339 - 0
lib/Elastica/Document.php

@@ -0,0 +1,339 @@
+<?php
+
+namespace Elastica;
+
+use Elastica\Bulk\Action;
+use Elastica\Exception\InvalidException;
+
+/**
+ * Single document stored in elastic search.
+ *
+ * @author   Nicolas Ruflin <spam@ruflin.com>
+ */
+class Document extends AbstractUpdateAction
+{
+    const OP_TYPE_CREATE = Action::OP_TYPE_CREATE;
+
+    /**
+     * Document data.
+     *
+     * @var array Document data
+     */
+    protected $_data = [];
+
+    /**
+     * Whether to use this document to upsert if the document does not exist.
+     *
+     * @var bool
+     */
+    protected $_docAsUpsert = false;
+
+    /**
+     * @var bool
+     */
+    protected $_autoPopulate = false;
+
+    /**
+     * Creates a new document.
+     *
+     * @param int|string   $id    OPTIONAL $id Id is create if empty
+     * @param array|string $data  OPTIONAL Data array
+     * @param Type|string  $type  OPTIONAL Type name
+     * @param Index|string $index OPTIONAL Index name
+     */
+    public function __construct($id = '', $data = [], $type = '', $index = '')
+    {
+        $this->setId($id);
+        $this->setData($data);
+        $this->setType($type);
+        $this->setIndex($index);
+    }
+
+    /**
+     * @param string $key
+     *
+     * @return mixed
+     */
+    public function __get($key)
+    {
+        return $this->get($key);
+    }
+
+    /**
+     * @param string $key
+     * @param mixed  $value
+     */
+    public function __set($key, $value)
+    {
+        $this->set($key, $value);
+    }
+
+    /**
+     * @param string $key
+     *
+     * @return bool
+     */
+    public function __isset($key)
+    {
+        return $this->has($key) && null !== $this->get($key);
+    }
+
+    /**
+     * @param string $key
+     */
+    public function __unset($key)
+    {
+        $this->remove($key);
+    }
+
+    /**
+     * @param string $key
+     *
+     * @throws \Elastica\Exception\InvalidException
+     *
+     * @return mixed
+     */
+    public function get($key)
+    {
+        if (!$this->has($key)) {
+            throw new InvalidException("Field {$key} does not exist");
+        }
+
+        return $this->_data[$key];
+    }
+
+    /**
+     * @param string $key
+     * @param mixed  $value
+     *
+     * @throws \Elastica\Exception\InvalidException
+     *
+     * @return $this
+     */
+    public function set($key, $value)
+    {
+        if (!is_array($this->_data)) {
+            throw new InvalidException('Document data is serialized data. Data creation is forbidden.');
+        }
+        $this->_data[$key] = $value;
+
+        return $this;
+    }
+
+    /**
+     * @param string $key
+     *
+     * @return bool
+     */
+    public function has($key)
+    {
+        return is_array($this->_data) && array_key_exists($key, $this->_data);
+    }
+
+    /**
+     * @param string $key
+     *
+     * @throws \Elastica\Exception\InvalidException
+     *
+     * @return $this
+     */
+    public function remove($key)
+    {
+        if (!$this->has($key)) {
+            throw new InvalidException("Field {$key} does not exist");
+        }
+        unset($this->_data[$key]);
+
+        return $this;
+    }
+
+    /**
+     * Adds a file to the index.
+     *
+     * To use this feature you have to call the following command in the
+     * elasticsearch directory:
+     * <code>
+     * ./bin/plugin -install elasticsearch/elasticsearch-mapper-attachments/1.6.0
+     * </code>
+     * This installs the tika file analysis plugin. More infos about supported formats
+     * can be found here: {@link http://tika.apache.org/0.7/formats.html}
+     *
+     * @param string $key      Key to add the file to
+     * @param string $filepath Path to add the file
+     * @param string $mimeType OPTIONAL Header mime type
+     *
+     * @return $this
+     */
+    public function addFile($key, $filepath, $mimeType = '')
+    {
+        $value = base64_encode(file_get_contents($filepath));
+
+        if (!empty($mimeType)) {
+            $value = ['_content_type' => $mimeType, '_name' => $filepath, '_content' => $value];
+        }
+
+        $this->set($key, $value);
+
+        return $this;
+    }
+
+    /**
+     * Add file content.
+     *
+     * @param string $key     Document key
+     * @param string $content Raw file content
+     *
+     * @return $this
+     */
+    public function addFileContent($key, $content)
+    {
+        return $this->set($key, base64_encode($content));
+    }
+
+    /**
+     * Adds a geopoint to the document.
+     *
+     * Geohashes are not yet supported
+     *
+     * @param string $key       Field key
+     * @param float  $latitude  Latitude value
+     * @param float  $longitude Longitude value
+     *
+     * @link https://www.elastic.co/guide/en/elasticsearch/reference/current/mapping-geo-point-type.html
+     *
+     * @return $this
+     */
+    public function addGeoPoint($key, $latitude, $longitude)
+    {
+        $value = ['lat' => $latitude, 'lon' => $longitude];
+
+        $this->set($key, $value);
+
+        return $this;
+    }
+
+    /**
+     * Overwrites the current document data with the given data.
+     *
+     * @param array|string $data Data array
+     *
+     * @return $this
+     */
+    public function setData($data)
+    {
+        $this->_data = $data;
+
+        return $this;
+    }
+
+    /**
+     * Returns the document data.
+     *
+     * @return array|string Document data
+     */
+    public function getData()
+    {
+        return $this->_data;
+    }
+
+    /**
+     * @param bool $value
+     *
+     * @return $this
+     */
+    public function setDocAsUpsert($value)
+    {
+        $this->_docAsUpsert = (bool) $value;
+
+        return $this;
+    }
+
+    /**
+     * @return bool
+     */
+    public function getDocAsUpsert()
+    {
+        return $this->_docAsUpsert;
+    }
+
+    /**
+     * @param bool $autoPopulate
+     *
+     * @return $this
+     */
+    public function setAutoPopulate($autoPopulate = true)
+    {
+        $this->_autoPopulate = (bool) $autoPopulate;
+
+        return $this;
+    }
+
+    /**
+     * @return bool
+     */
+    public function isAutoPopulate()
+    {
+        return $this->_autoPopulate;
+    }
+
+    /**
+     * Sets pipeline.
+     *
+     * @param string $pipeline
+     *
+     * @return $this
+     */
+    public function setPipeline($pipeline)
+    {
+        return $this->setParam('_pipeline', $pipeline);
+    }
+
+    /**
+     * @return string
+     */
+    public function getPipeline()
+    {
+        return $this->getParam('_pipeline');
+    }
+
+    /**
+     * @return bool
+     */
+    public function hasPipeline()
+    {
+        return $this->hasParam('_pipeline');
+    }
+
+    /**
+     * Returns the document as an array.
+     *
+     * @return array
+     */
+    public function toArray()
+    {
+        $doc = $this->getParams();
+        $doc['_source'] = $this->getData();
+
+        return $doc;
+    }
+
+    /**
+     * @param array|\Elastica\Document $data
+     *
+     * @throws \Elastica\Exception\InvalidException
+     *
+     * @return self
+     */
+    public static function create($data)
+    {
+        if ($data instanceof self) {
+            return $data;
+        }
+
+        if (is_array($data)) {
+            return new self('', $data);
+        }
+
+        throw new InvalidException('Failed to create document. Invalid data passed.');
+    }
+}

+ 66 - 0
lib/Elastica/Exception/Bulk/Response/ActionException.php

@@ -0,0 +1,66 @@
+<?php
+
+namespace Elastica\Exception\Bulk\Response;
+
+use Elastica\Bulk\Response;
+use Elastica\Exception\BulkException;
+
+class ActionException extends BulkException
+{
+    /**
+     * @var \Elastica\Response
+     */
+    protected $_response;
+
+    /**
+     * @param \Elastica\Bulk\Response $response
+     */
+    public function __construct(Response $response)
+    {
+        $this->_response = $response;
+
+        parent::__construct($this->getErrorMessage($response));
+    }
+
+    /**
+     * @return \Elastica\Bulk\Action
+     */
+    public function getAction()
+    {
+        return $this->getResponse()->getAction();
+    }
+
+    /**
+     * @return \Elastica\Bulk\Response
+     */
+    public function getResponse()
+    {
+        return $this->_response;
+    }
+
+    /**
+     * @param \Elastica\Bulk\Response $response
+     *
+     * @return string
+     */
+    public function getErrorMessage(Response $response)
+    {
+        $error = $response->getError();
+        $opType = $response->getOpType();
+        $data = $response->getData();
+
+        $path = '';
+        if (isset($data['_index'])) {
+            $path .= '/'.$data['_index'];
+        }
+        if (isset($data['_type'])) {
+            $path .= '/'.$data['_type'];
+        }
+        if (isset($data['_id'])) {
+            $path .= '/'.$data['_id'];
+        }
+        $message = "$opType: $path caused $error";
+
+        return $message;
+    }
+}

+ 99 - 0
lib/Elastica/Exception/Bulk/ResponseException.php

@@ -0,0 +1,99 @@
+<?php
+
+namespace Elastica\Exception\Bulk;
+
+use Elastica\Bulk\ResponseSet;
+use Elastica\Exception\Bulk\Response\ActionException;
+use Elastica\Exception\BulkException;
+
+/**
+ * Bulk Response exception.
+ */
+class ResponseException extends BulkException
+{
+    /**
+     * @var \Elastica\Bulk\ResponseSet ResponseSet object
+     */
+    protected $_responseSet;
+
+    /**
+     * @var \Elastica\Exception\Bulk\Response\ActionException[]
+     */
+    protected $_actionExceptions = [];
+
+    /**
+     * Construct Exception.
+     *
+     * @param \Elastica\Bulk\ResponseSet $responseSet
+     */
+    public function __construct(ResponseSet $responseSet)
+    {
+        $this->_init($responseSet);
+
+        $message = 'Error in one or more bulk request actions:'.PHP_EOL.PHP_EOL;
+        $message .= $this->getActionExceptionsAsString();
+
+        parent::__construct($message);
+    }
+
+    /**
+     * @param \Elastica\Bulk\ResponseSet $responseSet
+     */
+    protected function _init(ResponseSet $responseSet)
+    {
+        $this->_responseSet = $responseSet;
+
+        foreach ($responseSet->getBulkResponses() as $bulkResponse) {
+            if ($bulkResponse->hasError()) {
+                $this->_actionExceptions[] = new ActionException($bulkResponse);
+            }
+        }
+    }
+
+    /**
+     * Returns bulk response set object.
+     *
+     * @return \Elastica\Bulk\ResponseSet
+     */
+    public function getResponseSet()
+    {
+        return $this->_responseSet;
+    }
+
+    /**
+     * Returns array of failed actions.
+     *
+     * @return array Array of failed actions
+     */
+    public function getFailures()
+    {
+        $errors = [];
+
+        foreach ($this->getActionExceptions() as $actionException) {
+            $errors[] = $actionException->getMessage();
+        }
+
+        return $errors;
+    }
+
+    /**
+     * @return \Elastica\Exception\Bulk\Response\ActionException[]
+     */
+    public function getActionExceptions()
+    {
+        return $this->_actionExceptions;
+    }
+
+    /**
+     * @return string
+     */
+    public function getActionExceptionsAsString()
+    {
+        $message = '';
+        foreach ($this->getActionExceptions() as $actionException) {
+            $message .= $actionException->getMessage().PHP_EOL;
+        }
+
+        return $message;
+    }
+}

+ 7 - 0
lib/Elastica/Exception/BulkException.php

@@ -0,0 +1,7 @@
+<?php
+
+namespace Elastica\Exception;
+
+class BulkException extends \RuntimeException implements ExceptionInterface
+{
+}

+ 12 - 0
lib/Elastica/Exception/ClientException.php

@@ -0,0 +1,12 @@
+<?php
+
+namespace Elastica\Exception;
+
+/**
+ * Client exception.
+ *
+ * @author Nicolas Ruflin <spam@ruflin.com>
+ */
+class ClientException extends \RuntimeException implements ExceptionInterface
+{
+}

+ 51 - 0
lib/Elastica/Exception/Connection/GuzzleException.php

@@ -0,0 +1,51 @@
+<?php
+
+namespace Elastica\Exception\Connection;
+
+use Elastica\Exception\ConnectionException;
+use Elastica\Request;
+use Elastica\Response;
+use GuzzleHttp\Exception\TransferException;
+
+/**
+ * Transport exception.
+ *
+ * @author Milan Magudia <milan@magudia.com>
+ */
+class GuzzleException extends ConnectionException
+{
+    /**
+     * @var TransferException
+     */
+    protected $_guzzleException;
+
+    /**
+     * @param \GuzzleHttp\Exception\TransferException $guzzleException
+     * @param \Elastica\Request                       $request
+     * @param \Elastica\Response                      $response
+     */
+    public function __construct(TransferException $guzzleException, Request $request = null, Response $response = null)
+    {
+        $this->_guzzleException = $guzzleException;
+        $message = $this->getErrorMessage($this->getGuzzleException());
+        parent::__construct($message, $request, $response);
+    }
+
+    /**
+     * @param \GuzzleHttp\Exception\TransferException $guzzleException
+     *
+     * @return string
+     */
+    public function getErrorMessage(TransferException $guzzleException)
+    {
+        return $guzzleException->getMessage();
+    }
+
+    /**
+     * @return TransferException
+     */
+    public function getGuzzleException()
+    {
+        return $this->_guzzleException;
+    }
+}

+ 77 - 0
lib/Elastica/Exception/Connection/HttpException.php

@@ -0,0 +1,77 @@
+<?php
+
+namespace Elastica\Exception\Connection;
+
+use Elastica\Exception\ConnectionException;
+use Elastica\Request;
+use Elastica\Response;
+
+/**
+ * Connection exception.
+ *
+ * @author Nicolas Ruflin <spam@ruflin.com>
+ */
+class HttpException extends ConnectionException
+{
+    /**
+     * Error code / message.
+     *
+     * @var int|string Error code / message
+     */
+    protected $_error = 0;
+
+    /**
+     * Construct Exception.
+     *
+     * @param int|string         $error    Error
+     * @param \Elastica\Request  $request
+     * @param \Elastica\Response $response
+     */
+    public function __construct($error, Request $request = null, Response $response = null)
+    {
+        $this->_error = $error;
+
+        $message = $this->getErrorMessage($this->getError());
+        parent::__construct($message, $request, $response);
+    }
+
+    /**
+     * Returns the error message corresponding to the error code
+     * cUrl error code reference can be found here {@link http://curl.haxx.se/libcurl/c/libcurl-errors.html}.
+     *
+     * @param string $error Error code
+     *
+     * @return string Error message
+     */
+    public function getErrorMessage($error)
+    {
+        switch ($error) {
+            case CURLE_UNSUPPORTED_PROTOCOL:
+                return 'Unsupported protocol';
+            case CURLE_FAILED_INIT:
+                return 'Internal cUrl error?';
+            case CURLE_URL_MALFORMAT:
+                return 'Malformed URL';
+            case CURLE_COULDNT_RESOLVE_PROXY:
+                return "Couldn't resolve proxy";
+            case CURLE_COULDNT_RESOLVE_HOST:
+                return "Couldn't resolve host";
+            case CURLE_COULDNT_CONNECT:
+                return "Couldn't connect to host, Elasticsearch down?";
+            case 28:
+                return 'Operation timed out';
+        }
+
+        return 'Unknown error:'.$error;
+    }
+
+    /**
+     * Return Error code / message.
+     *
+     * @return string Error code / message
+     */
+    public function getError()
+    {
+        return $this->_error;
+    }
+}

+ 59 - 0
lib/Elastica/Exception/ConnectionException.php

@@ -0,0 +1,59 @@
+<?php
+
+namespace Elastica\Exception;
+
+use Elastica\Request;
+use Elastica\Response;
+
+/**
+ * Connection exception.
+ *
+ * @author Nicolas Ruflin <spam@ruflin.com>
+ */
+class ConnectionException extends \RuntimeException implements ExceptionInterface
+{
+    /**
+     * @var \Elastica\Request Request object
+     */
+    protected $_request;
+
+    /**
+     * @var \Elastica\Response Response object
+     */
+    protected $_response;
+
+    /**
+     * Construct Exception.
+     *
+     * @param string             $message  Message
+     * @param \Elastica\Request  $request
+     * @param \Elastica\Response $response
+     */
+    public function __construct($message, Request $request = null, Response $response = null)
+    {
+        $this->_request = $request;
+        $this->_response = $response;
+
+        parent::__construct($message);
+    }
+
+    /**
+     * Returns request object.
+     *
+     * @return \Elastica\Request Request object
+     */
+    public function getRequest()
+    {
+        return $this->_request;
+    }
+
+    /**
+     * Returns response object.
+     *
+     * @return \Elastica\Response Response object
+     */
+    public function getResponse()
+    {
+        return $this->_response;
+    }
+}

+ 14 - 0
lib/Elastica/Exception/DeprecatedException.php

@@ -0,0 +1,14 @@
+<?php
+
+namespace Elastica\Exception;
+
+/**
+ * Deprecated exception.
+ *
+ * Is thrown if a function or feature is deprecated and its usage can't be supported by BC layer
+ *
+ * @author Evgeniy Sokolov <ewgraf@gmail.com>
+ */
+class DeprecatedException extends NotImplementedException
+{
+}

+ 107 - 0
lib/Elastica/Exception/ElasticsearchException.php

@@ -0,0 +1,107 @@
+<?php
+
+namespace Elastica\Exception;
+
+trigger_error('Elastica\Exception\ElasticsearchException is deprecated. Use Elastica\Exception\ResponseException::getResponse::getFullError instead.', E_USER_DEPRECATED);
+
+/**
+ * Elasticsearch exception.
+ *
+ * @author Ian Babrou <ibobrik@gmail.com>
+ */
+class ElasticsearchException extends \Exception implements ExceptionInterface
+{
+    const REMOTE_TRANSPORT_EXCEPTION = 'RemoteTransportException';
+
+    /**
+     * @var string|null Elasticsearch exception name
+     */
+    private $_exception;
+
+    /**
+     * @var bool Whether exception was local to server node or remote
+     */
+    private $_isRemote = false;
+
+    /**
+     * @var array Error array
+     */
+    protected $_error = [];
+
+    /**
+     * Constructs elasticsearch exception.
+     *
+     * @param int    $code  Error code
+     * @param string $error Error message from elasticsearch
+     */
+    public function __construct($code, $error)
+    {
+        $this->_parseError($error);
+        parent::__construct($error, $code);
+    }
+
+    /**
+     * Parse error message from elasticsearch.
+     *
+     * @param string $error Error message
+     */
+    protected function _parseError($error)
+    {
+        $errors = explode(']; nested: ', $error);
+
+        if (1 == count($errors)) {
+            $this->_exception = $this->_extractException($errors[0]);
+        } else {
+            if (self::REMOTE_TRANSPORT_EXCEPTION == $this->_extractException($errors[0])) {
+                $this->_isRemote = true;
+                $this->_exception = $this->_extractException($errors[1]);
+            } else {
+                $this->_exception = $this->_extractException($errors[0]);
+            }
+        }
+    }
+
+    /**
+     * Extract exception name from error response.
+     *
+     * @param string $error
+     *
+     * @return string|null
+     */
+    protected function _extractException($error)
+    {
+        if (preg_match('/^(\w+)\[.*\]/', $error, $matches)) {
+            return $matches[1];
+        }
+
+        return;
+    }
+
+    /**
+     * Returns elasticsearch exception name.
+     *
+     * @return string|null
+     */
+    public function getExceptionName()
+    {
+        return $this->_exception;
+    }
+
+    /**
+     * Returns whether exception was local to server node or remote.
+     *
+     * @return bool
+     */
+    public function isRemoteTransportException()
+    {
+        return $this->_isRemote;
+    }
+
+    /**
+     * @return array Error array
+     */
+    public function getError()
+    {
+        return $this->_error;
+    }
+}

+ 12 - 0
lib/Elastica/Exception/ExceptionInterface.php

@@ -0,0 +1,12 @@
+<?php
+
+namespace Elastica\Exception;
+
+/**
+ * General Elastica exception interface.
+ *
+ * @author Nicolas Ruflin <spam@ruflin.com>
+ */
+interface ExceptionInterface
+{
+}

+ 12 - 0
lib/Elastica/Exception/InvalidException.php

@@ -0,0 +1,12 @@
+<?php
+
+namespace Elastica\Exception;
+
+/**
+ * Invalid exception.
+ *
+ * @author Nicolas Ruflin <spam@ruflin.com>
+ */
+class InvalidException extends \InvalidArgumentException implements ExceptionInterface
+{
+}

+ 10 - 0
lib/Elastica/Exception/JSONParseException.php

@@ -0,0 +1,10 @@
+<?php
+
+namespace Elastica\Exception;
+
+/**
+ * JSON Parse exception.
+ */
+class JSONParseException extends \RuntimeException implements ExceptionInterface
+{
+}

+ 12 - 0
lib/Elastica/Exception/NotFoundException.php

@@ -0,0 +1,12 @@
+<?php
+
+namespace Elastica\Exception;
+
+/**
+ * Not found exception.
+ *
+ * @author Nicolas Ruflin <spam@ruflin.com>
+ */
+class NotFoundException extends \RuntimeException implements ExceptionInterface
+{
+}

+ 14 - 0
lib/Elastica/Exception/NotImplementedException.php

@@ -0,0 +1,14 @@
+<?php
+
+namespace Elastica\Exception;
+
+/**
+ * Not implemented exception.
+ *
+ * Is thrown if a function or feature is not implemented yet
+ *
+ * @author Nicolas Ruflin <spam@ruflin.com>
+ */
+class NotImplementedException extends \BadMethodCallException implements ExceptionInterface
+{
+}

+ 29 - 0
lib/Elastica/Exception/PartialShardFailureException.php

@@ -0,0 +1,29 @@
+<?php
+
+namespace Elastica\Exception;
+
+use Elastica\JSON;
+use Elastica\Request;
+use Elastica\Response;
+
+/**
+ * Partial shard failure exception.
+ *
+ * @author Ian Babrou <ibobrik@gmail.com>
+ */
+class PartialShardFailureException extends ResponseException
+{
+    /**
+     * Construct Exception.
+     *
+     * @param \Elastica\Request  $request
+     * @param \Elastica\Response $response
+     */
+    public function __construct(Request $request, Response $response)
+    {
+        parent::__construct($request, $response);
+
+        $shardsStatistics = $response->getShardsStatistics();
+        $this->message = JSON::stringify($shardsStatistics);
+    }
+}

+ 12 - 0
lib/Elastica/Exception/QueryBuilderException.php

@@ -0,0 +1,12 @@
+<?php
+
+namespace Elastica\Exception;
+
+/**
+ * QueryBuilder exception.
+ *
+ * @author Manuel Andreo Garcia <andreo.garcia@googlemail.com>
+ */
+class QueryBuilderException extends \RuntimeException implements ExceptionInterface
+{
+}

+ 69 - 0
lib/Elastica/Exception/ResponseException.php

@@ -0,0 +1,69 @@
+<?php
+
+namespace Elastica\Exception;
+
+use Elastica\Request;
+use Elastica\Response;
+
+/**
+ * Response exception.
+ *
+ * @author Nicolas Ruflin <spam@ruflin.com>
+ */
+class ResponseException extends \RuntimeException implements ExceptionInterface
+{
+    /**
+     * @var \Elastica\Request Request object
+     */
+    protected $_request;
+
+    /**
+     * @var \Elastica\Response Response object
+     */
+    protected $_response;
+
+    /**
+     * Construct Exception.
+     *
+     * @param \Elastica\Request  $request
+     * @param \Elastica\Response $response
+     */
+    public function __construct(Request $request, Response $response)
+    {
+        $this->_request = $request;
+        $this->_response = $response;
+        parent::__construct($response->getErrorMessage());
+    }
+
+    /**
+     * Returns request object.
+     *
+     * @return \Elastica\Request Request object
+     */
+    public function getRequest()
+    {
+        return $this->_request;
+    }
+
+    /**
+     * Returns response object.
+     *
+     * @return \Elastica\Response Response object
+     */
+    public function getResponse()
+    {
+        return $this->_response;
+    }
+
+    /**
+     * Returns elasticsearch exception.
+     *
+     * @return ElasticsearchException
+     */
+    public function getElasticsearchException()
+    {
+        $response = $this->getResponse();
+
+        return new ElasticsearchException($response->getStatus(), $response->getErrorMessage());
+    }
+}

+ 12 - 0
lib/Elastica/Exception/RuntimeException.php

@@ -0,0 +1,12 @@
+<?php
+
+namespace Elastica\Exception;
+
+/**
+ * Client exception.
+ *
+ * @author Mikhail Shamin <munk13@gmail.com>
+ */
+class RuntimeException extends \RuntimeException implements ExceptionInterface
+{
+}

+ 617 - 0
lib/Elastica/Index.php

@@ -0,0 +1,617 @@
+<?php
+
+namespace Elastica;
+
+use Elastica\Exception\InvalidException;
+use Elastica\Exception\ResponseException;
+use Elastica\Index\Recovery as IndexRecovery;
+use Elastica\Index\Settings as IndexSettings;
+use Elastica\Index\Stats as IndexStats;
+use Elastica\ResultSet\BuilderInterface;
+use Elastica\Script\AbstractScript;
+use Elasticsearch\Endpoints\AbstractEndpoint;
+use Elasticsearch\Endpoints\DeleteByQuery;
+use Elasticsearch\Endpoints\Indices\Aliases\Update;
+use Elasticsearch\Endpoints\Indices\Analyze;
+use Elasticsearch\Endpoints\Indices\Cache\Clear;
+use Elasticsearch\Endpoints\Indices\Close;
+use Elasticsearch\Endpoints\Indices\Create;
+use Elasticsearch\Endpoints\Indices\Delete;
+use Elasticsearch\Endpoints\Indices\Exists;
+use Elasticsearch\Endpoints\Indices\Flush;
+use Elasticsearch\Endpoints\Indices\ForceMerge;
+use Elasticsearch\Endpoints\Indices\Mapping\Get;
+use Elasticsearch\Endpoints\Indices\Open;
+use Elasticsearch\Endpoints\Indices\Refresh;
+use Elasticsearch\Endpoints\Indices\Settings\Put;
+use Elasticsearch\Endpoints\UpdateByQuery;
+
+/**
+ * Elastica index object.
+ *
+ * Handles reads, deletes and configurations of an index
+ *
+ * @author   Nicolas Ruflin <spam@ruflin.com>
+ */
+class Index implements SearchableInterface
+{
+    /**
+     * Index name.
+     *
+     * @var string Index name
+     */
+    protected $_name;
+
+    /**
+     * Client object.
+     *
+     * @var \Elastica\Client Client object
+     */
+    protected $_client;
+
+    /**
+     * Creates a new index object.
+     *
+     * All the communication to and from an index goes of this object
+     *
+     * @param \Elastica\Client $client Client object
+     * @param string           $name   Index name
+     */
+    public function __construct(Client $client, $name)
+    {
+        $this->_client = $client;
+
+        if (!is_scalar($name)) {
+            throw new InvalidException('Index name should be a scalar type');
+        }
+        $this->_name = (string) $name;
+    }
+
+    /**
+     * Returns a type object for the current index with the given name.
+     *
+     * @param string $type Type name
+     *
+     * @return \Elastica\Type Type object
+     */
+    public function getType($type)
+    {
+        return new Type($this, $type);
+    }
+
+    /**
+     * Return Index Stats.
+     *
+     * @return \Elastica\Index\Stats
+     */
+    public function getStats()
+    {
+        return new IndexStats($this);
+    }
+
+    /**
+     * Return Index Recovery.
+     *
+     * @return \Elastica\Index\Recovery
+     */
+    public function getRecovery()
+    {
+        return new IndexRecovery($this);
+    }
+
+    /**
+     * Gets all the type mappings for an index.
+     *
+     * @return array
+     */
+    public function getMapping()
+    {
+        $response = $this->requestEndpoint(new Get());
+        $data = $response->getData();
+
+        // Get first entry as if index is an Alias, the name of the mapping is the real name and not alias name
+        $mapping = array_shift($data);
+
+        if (isset($mapping['mappings'])) {
+            return $mapping['mappings'];
+        }
+
+        return [];
+    }
+
+    /**
+     * Returns the index settings object.
+     *
+     * @return \Elastica\Index\Settings Settings object
+     */
+    public function getSettings()
+    {
+        return new IndexSettings($this);
+    }
+
+    /**
+     * Uses _bulk to send documents to the server.
+     *
+     * @param array|\Elastica\Document[] $docs    Array of Elastica\Document
+     * @param array                      $options Array of query params to use for query. For possible options check es api
+     *
+     * @return \Elastica\Bulk\ResponseSet
+     *
+     * @see https://www.elastic.co/guide/en/elasticsearch/reference/current/docs-bulk.html
+     */
+    public function updateDocuments(array $docs, array $options = [])
+    {
+        foreach ($docs as $doc) {
+            $doc->setIndex($this->getName());
+        }
+
+        return $this->getClient()->updateDocuments($docs, $options);
+    }
+
+    /**
+     * Update entries in the db based on a query.
+     *
+     * @param \Elastica\Query|string|array $query   Query object or array
+     * @param AbstractScript               $script  Script
+     * @param array                        $options Optional params
+     *
+     * @return \Elastica\Response
+     *
+     * @see https://www.elastic.co/guide/en/elasticsearch/reference/current/docs-update-by-query.html
+     */
+    public function updateByQuery($query, AbstractScript $script, array $options = [])
+    {
+        $query = Query::create($query)->getQuery();
+
+        $endpoint = new UpdateByQuery();
+        $body = ['query' => is_array($query)
+            ? $query
+            : $query->toArray(), ];
+
+        $body['script'] = $script->toArray()['script'];
+        $endpoint->setBody($body);
+        $endpoint->setParams($options);
+
+        return $this->requestEndpoint($endpoint);
+    }
+
+    /**
+     * Uses _bulk to send documents to the server.
+     *
+     * @param array|\Elastica\Document[] $docs    Array of Elastica\Document
+     * @param array                      $options Array of query params to use for query. For possible options check es api
+     *
+     * @return \Elastica\Bulk\ResponseSet
+     *
+     * @see https://www.elastic.co/guide/en/elasticsearch/reference/current/docs-bulk.html
+     */
+    public function addDocuments(array $docs, array $options = [])
+    {
+        foreach ($docs as $doc) {
+            $doc->setIndex($this->getName());
+        }
+
+        return $this->getClient()->addDocuments($docs, $options);
+    }
+
+    /**
+     * Deletes entries in the db based on a query.
+     *
+     * @param \Elastica\Query|\Elastica\Query\AbstractQuery|string|array $query   Query object or array
+     * @param array                                                      $options Optional params
+     *
+     * @return \Elastica\Response
+     *
+     * @see https://www.elastic.co/guide/en/elasticsearch/reference/5.0/docs-delete-by-query.html
+     */
+    public function deleteByQuery($query, array $options = [])
+    {
+        $query = Query::create($query)->getQuery();
+
+        $endpoint = new DeleteByQuery();
+        $endpoint->setBody(['query' => is_array($query) ? $query : $query->toArray()]);
+        $endpoint->setParams($options);
+
+        return $this->requestEndpoint($endpoint);
+    }
+
+    /**
+     * Deletes the index.
+     *
+     * @return \Elastica\Response Response object
+     */
+    public function delete()
+    {
+        return $this->requestEndpoint(new Delete());
+    }
+
+    /**
+     * Uses _bulk to delete documents from the server.
+     *
+     * @param array|\Elastica\Document[] $docs Array of Elastica\Document
+     *
+     * @return \Elastica\Bulk\ResponseSet
+     *
+     * @see https://www.elastic.co/guide/en/elasticsearch/reference/current/docs-bulk.html
+     */
+    public function deleteDocuments(array $docs)
+    {
+        foreach ($docs as $doc) {
+            $doc->setIndex($this->getName());
+        }
+
+        return $this->getClient()->deleteDocuments($docs);
+    }
+
+    /**
+     * Force merges index.
+     *
+     * Detailed arguments can be found here in the link
+     *
+     * @param array $args OPTIONAL Additional arguments
+     *
+     * @return Response
+     *
+     * @see https://www.elastic.co/guide/en/elasticsearch/reference/current/indices-forcemerge.html
+     */
+    public function forcemerge($args = [])
+    {
+        $endpoint = new ForceMerge();
+        $endpoint->setParams($args);
+
+        return $this->requestEndpoint($endpoint);
+    }
+
+    /**
+     * Refreshes the index.
+     *
+     * @return \Elastica\Response Response object
+     *
+     * @see https://www.elastic.co/guide/en/elasticsearch/reference/current/indices-refresh.html
+     */
+    public function refresh()
+    {
+        return $this->requestEndpoint(new Refresh());
+    }
+
+    /**
+     * Creates a new index with the given arguments.
+     *
+     * @see https://www.elastic.co/guide/en/elasticsearch/reference/current/indices-create-index.html
+     *
+     * @param array      $args    OPTIONAL Arguments to use
+     * @param bool|array $options OPTIONAL
+     *                            bool=> Deletes index first if already exists (default = false).
+     *                            array => Associative array of options (option=>value)
+     *
+     * @throws \Elastica\Exception\InvalidException
+     * @throws \Elastica\Exception\ResponseException
+     *
+     * @return \Elastica\Response Server response
+     */
+    public function create(array $args = [], $options = null)
+    {
+        if (is_bool($options) && $options) {
+            try {
+                $this->delete();
+            } catch (ResponseException $e) {
+                // Table can't be deleted, because doesn't exist
+            }
+        } elseif (is_array($options)) {
+            foreach ($options as $key => $value) {
+                switch ($key) {
+                    case 'recreate':
+                        try {
+                            $this->delete();
+                        } catch (ResponseException $e) {
+                            // Table can't be deleted, because doesn't exist
+                        }
+                        break;
+                    default:
+                        throw new InvalidException('Invalid option '.$key);
+                        break;
+                }
+            }
+        }
+
+        $endpoint = new Create();
+        $endpoint->setBody($args);
+
+        return $this->requestEndpoint($endpoint);
+    }
+
+    /**
+     * Checks if the given index is already created.
+     *
+     * @return bool True if index exists
+     */
+    public function exists()
+    {
+        $response = $this->requestEndpoint(new Exists());
+
+        return 200 === $response->getStatus();
+    }
+
+    /**
+     * @param string|array|\Elastica\Query $query
+     * @param int|array                    $options
+     * @param BuilderInterface             $builder
+     *
+     * @return Search
+     */
+    public function createSearch($query = '', $options = null, BuilderInterface $builder = null)
+    {
+        $search = new Search($this->getClient(), $builder);
+        $search->addIndex($this);
+        $search->setOptionsAndQuery($options, $query);
+
+        return $search;
+    }
+
+    /**
+     * Searches in this index.
+     *
+     * @param string|array|\Elastica\Query $query   Array with all query data inside or a Elastica\Query object
+     * @param int|array                    $options OPTIONAL Limit or associative array of options (option=>value)
+     *
+     * @return \Elastica\ResultSet with all results inside
+     *
+     * @see \Elastica\SearchableInterface::search
+     */
+    public function search($query = '', $options = null)
+    {
+        $search = $this->createSearch($query, $options);
+
+        return $search->search();
+    }
+
+    /**
+     * Counts results of query.
+     *
+     * @param string|array|\Elastica\Query $query Array with all query data inside or a Elastica\Query object
+     *
+     * @return int number of documents matching the query
+     *
+     * @see \Elastica\SearchableInterface::count
+     */
+    public function count($query = '')
+    {
+        $search = $this->createSearch($query);
+
+        return $search->count();
+    }
+
+    /**
+     * Opens an index.
+     *
+     * @return \Elastica\Response Response object
+     *
+     * @see https://www.elastic.co/guide/en/elasticsearch/reference/current/indices-open-close.html
+     */
+    public function open()
+    {
+        return $this->requestEndpoint(new Open());
+    }
+
+    /**
+     * Closes the index.
+     *
+     * @return \Elastica\Response Response object
+     *
+     * @see https://www.elastic.co/guide/en/elasticsearch/reference/current/indices-open-close.html
+     */
+    public function close()
+    {
+        return $this->requestEndpoint(new Close());
+    }
+
+    /**
+     * Returns the index name.
+     *
+     * @return string Index name
+     */
+    public function getName()
+    {
+        return $this->_name;
+    }
+
+    /**
+     * Returns index client.
+     *
+     * @return \Elastica\Client Index client object
+     */
+    public function getClient()
+    {
+        return $this->_client;
+    }
+
+    /**
+     * Adds an alias to the current index.
+     *
+     * @param string $name    Alias name
+     * @param bool   $replace OPTIONAL If set, an existing alias will be replaced
+     *
+     * @return \Elastica\Response Response
+     *
+     * @see https://www.elastic.co/guide/en/elasticsearch/reference/current/indices-aliases.html
+     */
+    public function addAlias($name, $replace = false)
+    {
+        $data = ['actions' => []];
+
+        if ($replace) {
+            $status = new Status($this->getClient());
+            foreach ($status->getIndicesWithAlias($name) as $index) {
+                $data['actions'][] = ['remove' => ['index' => $index->getName(), 'alias' => $name]];
+            }
+        }
+
+        $data['actions'][] = ['add' => ['index' => $this->getName(), 'alias' => $name]];
+
+        $endpoint = new Update();
+        $endpoint->setBody($data);
+
+        return $this->getClient()->requestEndpoint($endpoint);
+    }
+
+    /**
+     * Removes an alias pointing to the current index.
+     *
+     * @param string $name Alias name
+     *
+     * @return \Elastica\Response Response
+     *
+     * @see https://www.elastic.co/guide/en/elasticsearch/reference/current/indices-aliases.html
+     */
+    public function removeAlias($name)
+    {
+        $endpoint = new \Elasticsearch\Endpoints\Indices\Alias\Delete();
+        $endpoint->setName($name);
+
+        return $this->requestEndpoint($endpoint);
+    }
+
+    /**
+     * Returns all index aliases.
+     *
+     * @return array Aliases
+     */
+    public function getAliases()
+    {
+        $endpoint = new \Elasticsearch\Endpoints\Indices\Alias\Get();
+        $endpoint->setName('*');
+
+        $responseData = $this->requestEndpoint($endpoint)->getData();
+
+        if (!isset($responseData[$this->getName()])) {
+            return [];
+        }
+
+        $data = $responseData[$this->getName()];
+        if (!empty($data['aliases'])) {
+            return array_keys($data['aliases']);
+        }
+
+        return [];
+    }
+
+    /**
+     * Checks if the index has the given alias.
+     *
+     * @param string $name Alias name
+     *
+     * @return bool
+     */
+    public function hasAlias($name)
+    {
+        return in_array($name, $this->getAliases());
+    }
+
+    /**
+     * Clears the cache of an index.
+     *
+     * @return \Elastica\Response Response object
+     *
+     * @see https://www.elastic.co/guide/en/elasticsearch/reference/current/indices-clearcache.html
+     */
+    public function clearCache()
+    {
+        // TODO: add additional cache clean arguments
+        return $this->requestEndpoint(new Clear());
+    }
+
+    /**
+     * Flushes the index to storage.
+     *
+     * @param array $options
+     *
+     * @return Response Response object
+     *
+     * @see https://www.elastic.co/guide/en/elasticsearch/reference/current/indices-flush.html
+     */
+    public function flush(array $options = [])
+    {
+        $endpoint = new Flush();
+        $endpoint->setParams($options);
+
+        return $this->requestEndpoint($endpoint);
+    }
+
+    /**
+     * Can be used to change settings during runtime. One example is to use it for bulk updating.
+     *
+     * @param array $data Data array
+     *
+     * @return \Elastica\Response Response object
+     *
+     * @see https://www.elastic.co/guide/en/elasticsearch/reference/current/indices-update-settings.html
+     */
+    public function setSettings(array $data)
+    {
+        $endpoint = new Put();
+        $endpoint->setBody($data);
+
+        return $this->requestEndpoint($endpoint);
+    }
+
+    /**
+     * Makes calls to the elasticsearch server based on this index.
+     *
+     * @param string       $path   Path to call
+     * @param string       $method Rest method to use (GET, POST, DELETE, PUT)
+     * @param array|string $data   OPTIONAL Arguments as array or encoded string
+     * @param array        $query  OPTIONAL Query params
+     *
+     * @return \Elastica\Response Response object
+     */
+    public function request($path, $method, $data = [], array $query = [])
+    {
+        $path = $this->getName().'/'.$path;
+
+        return $this->getClient()->request($path, $method, $data, $query);
+    }
+
+    /**
+     * Makes calls to the elasticsearch server with usage official client Endpoint based on this index.
+     *
+     * @param AbstractEndpoint $endpoint
+     *
+     * @return Response
+     */
+    public function requestEndpoint(AbstractEndpoint $endpoint)
+    {
+        $cloned = clone $endpoint;
+        $cloned->setIndex($this->getName());
+
+        return $this->getClient()->requestEndpoint($cloned);
+    }
+
+    /**
+     * Analyzes a string.
+     *
+     * Detailed arguments can be found here in the link
+     *
+     * @param array $body String to be analyzed
+     * @param array $args OPTIONAL Additional arguments
+     *
+     * @return array Server response
+     *
+     * @see https://www.elastic.co/guide/en/elasticsearch/reference/current/indices-analyze.html
+     */
+    public function analyze(array $body, $args = [])
+    {
+        $endpoint = new Analyze();
+        $endpoint->setBody($body);
+        $endpoint->setParams($args);
+
+        $data = $this->requestEndpoint($endpoint)->getData();
+
+        // Support for "Explain" parameter, that returns a different response structure from Elastic
+        // @see: https://www.elastic.co/guide/en/elasticsearch/reference/current/_explain_analyze.html
+        if (isset($body['explain']) && $body['explain']) {
+            return $data['detail'];
+        }
+
+        return $data['tokens'];
+    }
+}

+ 101 - 0
lib/Elastica/Index/Recovery.php

@@ -0,0 +1,101 @@
+<?php
+
+namespace Elastica\Index;
+
+use Elastica\Index as BaseIndex;
+
+/**
+ * Elastica index recovery object.
+ *
+ * @author Federico Panini <fpanini@gmail.com>
+ *
+ * @see https://www.elastic.co/guide/en/elasticsearch/reference/current/indices-recovery.html
+ */
+class Recovery
+{
+    /**
+     * Response.
+     *
+     * @var \Elastica\Response Response object
+     */
+    protected $_response;
+
+    /**
+     * Recovery info.
+     *
+     * @var array Recovery info
+     */
+    protected $_data = [];
+
+    /**
+     * Index.
+     *
+     * @var \Elastica\Index Index object
+     */
+    protected $_index;
+
+    /**
+     * Construct.
+     *
+     * @param \Elastica\Index $index Index object
+     */
+    public function __construct(BaseIndex $index)
+    {
+        $this->_index = $index;
+        $this->refresh();
+    }
+
+    /**
+     * Returns the index object.
+     *
+     * @return \Elastica\Index Index object
+     */
+    public function getIndex()
+    {
+        return $this->_index;
+    }
+
+    /**
+     * Returns response object.
+     *
+     * @return \Elastica\Response Response object
+     */
+    public function getResponse()
+    {
+        return $this->_response;
+    }
+
+    /**
+     * Returns the raw recovery info.
+     *
+     * @return array Recovery info
+     */
+    public function getData()
+    {
+        return $this->_data;
+    }
+
+    /**
+     * @return mixed
+     */
+    protected function getRecoveryData()
+    {
+        $endpoint = new \Elasticsearch\Endpoints\Indices\Recovery();
+
+        $this->_response = $this->getIndex()->requestEndpoint($endpoint);
+
+        return $this->getResponse()->getData();
+    }
+
+    /**
+     * Retrieve the Recovery data.
+     *
+     * @return $this
+     */
+    public function refresh()
+    {
+        $this->_data = $this->getRecoveryData();
+
+        return $this;
+    }
+}

+ 385 - 0
lib/Elastica/Index/Settings.php

@@ -0,0 +1,385 @@
+<?php
+
+namespace Elastica\Index;
+
+use Elastica\Exception\NotFoundException;
+use Elastica\Exception\ResponseException;
+use Elastica\Index as BaseIndex;
+use Elastica\Request;
+
+/**
+ * Elastica index settings object.
+ *
+ * All settings listed in the update settings API (https://www.elastic.co/guide/en/elasticsearch/reference/current/indices-update-settings.html)
+ * can be changed on a running indices. To make changes like the merge policy (https://www.elastic.co/guide/en/elasticsearch/reference/current/index-modules-merge.html)
+ * the index has to be closed first and reopened after the call
+ *
+ * @author Nicolas Ruflin <spam@ruflin.com>
+ *
+ * @see https://www.elastic.co/guide/en/elasticsearch/reference/current/indices-update-settings.html
+ * @see https://www.elastic.co/guide/en/elasticsearch/reference/current/index-modules-merge.html
+ */
+class Settings
+{
+    const DEFAULT_REFRESH_INTERVAL = '1s';
+
+    const DEFAULT_NUMBER_OF_REPLICAS = 1;
+
+    const DEFAULT_NUMBER_OF_SHARDS = 5;
+
+    /**
+     * Response.
+     *
+     * @var \Elastica\Response Response object
+     */
+    protected $_response;
+
+    /**
+     * Stats info.
+     *
+     * @var array Stats info
+     */
+    protected $_data = [];
+
+    /**
+     * Index.
+     *
+     * @var \Elastica\Index Index object
+     */
+    protected $_index;
+
+    /**
+     * Construct.
+     *
+     * @param \Elastica\Index $index Index object
+     */
+    public function __construct(BaseIndex $index)
+    {
+        $this->_index = $index;
+    }
+
+    /**
+     * Returns the current settings of the index.
+     *
+     * If param is set, only specified setting is return.
+     * 'index.' is added in front of $setting.
+     *
+     * @param string $setting OPTIONAL Setting name to return
+     *
+     * @return array|string|null Settings data
+     *
+     * @see https://www.elastic.co/guide/en/elasticsearch/reference/current/indices-update-settings.html
+     */
+    public function get($setting = '')
+    {
+        $requestData = $this->request()->getData();
+        $data = reset($requestData);
+
+        if (empty($data['settings']) || empty($data['settings']['index'])) {
+            // should not append, the request should throw a ResponseException
+            throw new NotFoundException('Index '.$this->getIndex()->getName().' not found');
+        }
+        $settings = $data['settings']['index'];
+
+        if (!$setting) {
+            // return all array
+            return $settings;
+        }
+
+        if (isset($settings[$setting])) {
+            return $settings[$setting];
+        }
+        if (false !== strpos($setting, '.')) {
+            // translate old dot-notation settings to nested arrays
+            $keys = explode('.', $setting);
+            foreach ($keys as $key) {
+                if (isset($settings[$key])) {
+                    $settings = $settings[$key];
+                } else {
+                    return;
+                }
+            }
+
+            return $settings;
+        }
+
+        return;
+    }
+
+    /**
+     * Returns a setting interpreted as a bool.
+     *
+     * One can use a real bool, int(0), int(1) to set bool settings.
+     * But Elasticsearch stores and returns all settings as strings and does
+     * not normalize bool values. This method ensures a bool is returned for
+     * whichever string representation is used like 'true', '1', 'on', 'yes'.
+     *
+     * @param string $setting Setting name to return
+     *
+     * @return bool
+     */
+    public function getBool($setting)
+    {
+        $data = $this->get($setting);
+
+        return 'true' === $data || '1' === $data || 'on' === $data || 'yes' === $data;
+    }
+
+    /**
+     * Sets the number of replicas.
+     *
+     * @param int $replicas Number of replicas
+     *
+     * @return \Elastica\Response Response object
+     */
+    public function setNumberOfReplicas($replicas)
+    {
+        return $this->set(['number_of_replicas' => (int) $replicas]);
+    }
+
+    /**
+     * Returns the number of replicas.
+     *
+     * If no number of replicas is set, the default number is returned
+     *
+     * @return int The number of replicas
+     */
+    public function getNumberOfReplicas()
+    {
+        $replicas = $this->get('number_of_replicas');
+
+        if (null === $replicas) {
+            $replicas = self::DEFAULT_NUMBER_OF_REPLICAS;
+        }
+
+        return $replicas;
+    }
+
+    /**
+     * Returns the number of shards.
+     *
+     * If no number of shards is set, the default number is returned
+     *
+     * @return int The number of shards
+     */
+    public function getNumberOfShards()
+    {
+        $shards = $this->get('number_of_shards');
+
+        if (null === $shards) {
+            $shards = self::DEFAULT_NUMBER_OF_SHARDS;
+        }
+
+        return $shards;
+    }
+
+    /**
+     * Sets the index to read only.
+     *
+     * @param bool $readOnly (default = true)
+     *
+     * @return \Elastica\Response
+     */
+    public function setReadOnly($readOnly = true)
+    {
+        return $this->set(['blocks.read_only' => $readOnly]);
+    }
+
+    /**
+     * @return bool
+     */
+    public function getReadOnly()
+    {
+        return $this->getBool('blocks.read_only');
+    }
+
+    /**
+     * @return bool
+     */
+    public function getBlocksRead()
+    {
+        return $this->getBool('blocks.read');
+    }
+
+    /**
+     * @param bool $state OPTIONAL (default = true)
+     *
+     * @return \Elastica\Response
+     */
+    public function setBlocksRead($state = true)
+    {
+        return $this->set(['blocks.read' => $state]);
+    }
+
+    /**
+     * @return bool
+     */
+    public function getBlocksWrite()
+    {
+        return $this->getBool('blocks.write');
+    }
+
+    /**
+     * @param bool $state OPTIONAL (default = true)
+     *
+     * @return \Elastica\Response
+     */
+    public function setBlocksWrite($state = true)
+    {
+        return $this->set(['blocks.write' => $state]);
+    }
+
+    /**
+     * @return bool
+     */
+    public function getBlocksMetadata()
+    {
+        // When blocks.metadata is enabled, reading the settings is not possible anymore.
+        // So when a cluster_block_exception happened it must be enabled.
+        try {
+            return $this->getBool('blocks.metadata');
+        } catch (ResponseException $e) {
+            if ('cluster_block_exception' === $e->getResponse()->getFullError()['type']) {
+                return true;
+            }
+
+            throw $e;
+        }
+    }
+
+    /**
+     * Set to true to disable index metadata reads and writes.
+     *
+     * @param bool $state OPTIONAL (default = true)
+     *
+     * @return \Elastica\Response
+     */
+    public function setBlocksMetadata($state = true)
+    {
+        return $this->set(['blocks.metadata' => $state]);
+    }
+
+    /**
+     * Sets the index refresh interval.
+     *
+     * Value can be for example 3s for 3 seconds or
+     * 5m for 5 minutes. -1 to disabled refresh.
+     *
+     * @param string $interval Duration of the refresh interval
+     *
+     * @return \Elastica\Response Response object
+     */
+    public function setRefreshInterval($interval)
+    {
+        return $this->set(['refresh_interval' => $interval]);
+    }
+
+    /**
+     * Returns the refresh interval.
+     *
+     * If no interval is set, the default interval is returned
+     *
+     * @return string Refresh interval
+     */
+    public function getRefreshInterval()
+    {
+        $interval = $this->get('refresh_interval');
+
+        if (empty($interval)) {
+            $interval = self::DEFAULT_REFRESH_INTERVAL;
+        }
+
+        return $interval;
+    }
+
+    /**
+     * Sets the specific merge policies.
+     *
+     * To have this changes made the index has to be closed and reopened
+     *
+     * @param string $key   Merge policy key (for ex. expunge_deletes_allowed)
+     * @param string $value
+     *
+     * @return \Elastica\Response
+     *
+     * @see https://www.elastic.co/guide/en/elasticsearch/reference/current/index-modules-merge.html
+     */
+    public function setMergePolicy($key, $value)
+    {
+        $this->getIndex()->close();
+        $response = $this->set(['merge.policy.'.$key => $value]);
+        $this->getIndex()->open();
+
+        return $response;
+    }
+
+    /**
+     * Returns the specific merge policy value.
+     *
+     * @param string $key Merge policy key (for ex. expunge_deletes_allowed)
+     *
+     * @return string Refresh interval
+     *
+     * @see https://www.elastic.co/guide/en/elasticsearch/reference/current/index-modules-merge.html
+     */
+    public function getMergePolicy($key)
+    {
+        $settings = $this->get();
+        if (isset($settings['merge']['policy'][$key])) {
+            return $settings['merge']['policy'][$key];
+        }
+
+        return;
+    }
+
+    /**
+     * Can be used to set/update settings.
+     *
+     * @param array $data Arguments
+     *
+     * @return \Elastica\Response Response object
+     */
+    public function set(array $data)
+    {
+        return $this->request($data, Request::PUT);
+    }
+
+    /**
+     * Returns the index object.
+     *
+     * @return \Elastica\Index Index object
+     */
+    public function getIndex()
+    {
+        return $this->_index;
+    }
+
+    /**
+     * Updates the given settings for the index.
+     *
+     * With elasticsearch 0.16 the following settings are supported
+     * - index.term_index_interval
+     * - index.term_index_divisor
+     * - index.translog.flush_threshold_ops
+     * - index.translog.flush_threshold_size
+     * - index.translog.flush_threshold_period
+     * - index.refresh_interval
+     * - index.merge.policy
+     * - index.auto_expand_replicas
+     *
+     * @param array  $data   OPTIONAL Data array
+     * @param string $method OPTIONAL Transfer method (default = \Elastica\Request::GET)
+     *
+     * @return \Elastica\Response Response object
+     */
+    public function request(array $data = [], $method = Request::GET)
+    {
+        $path = '_settings';
+
+        if (!empty($data)) {
+            $data = ['index' => $data];
+        }
+
+        return $this->getIndex()->request($path, $method, $data);
+    }
+}

+ 107 - 0
lib/Elastica/Index/Stats.php

@@ -0,0 +1,107 @@
+<?php
+
+namespace Elastica\Index;
+
+use Elastica\Index as BaseIndex;
+
+/**
+ * Elastica index stats object.
+ *
+ * @author Nicolas Ruflin <spam@ruflin.com>
+ *
+ * @see https://www.elastic.co/guide/en/elasticsearch/reference/current/indices-stats.html
+ */
+class Stats
+{
+    /**
+     * Response.
+     *
+     * @var \Elastica\Response Response object
+     */
+    protected $_response;
+
+    /**
+     * Stats info.
+     *
+     * @var array Stats info
+     */
+    protected $_data = [];
+
+    /**
+     * Index.
+     *
+     * @var \Elastica\Index Index object
+     */
+    protected $_index;
+
+    /**
+     * Construct.
+     *
+     * @param \Elastica\Index $index Index object
+     */
+    public function __construct(BaseIndex $index)
+    {
+        $this->_index = $index;
+        $this->refresh();
+    }
+
+    /**
+     * Returns the raw stats info.
+     *
+     * @return array Stats info
+     */
+    public function getData()
+    {
+        return $this->_data;
+    }
+
+    /**
+     * Returns the entry in the data array based on the params.
+     * Various params possible.
+     *
+     * @return mixed Data array entry or null if not found
+     */
+    public function get()
+    {
+        $data = $this->getData();
+
+        foreach (func_get_args() as $arg) {
+            if (isset($data[$arg])) {
+                $data = $data[$arg];
+            } else {
+                return;
+            }
+        }
+
+        return $data;
+    }
+
+    /**
+     * Returns the index object.
+     *
+     * @return \Elastica\Index Index object
+     */
+    public function getIndex()
+    {
+        return $this->_index;
+    }
+
+    /**
+     * Returns response object.
+     *
+     * @return \Elastica\Response Response object
+     */
+    public function getResponse()
+    {
+        return $this->_response;
+    }
+
+    /**
+     * Reloads all status data of this object.
+     */
+    public function refresh()
+    {
+        $this->_response = $this->getIndex()->requestEndpoint(new \Elasticsearch\Endpoints\Indices\Stats());
+        $this->_data = $this->getResponse()->getData();
+    }
+}

+ 120 - 0
lib/Elastica/IndexTemplate.php

@@ -0,0 +1,120 @@
+<?php
+
+namespace Elastica;
+
+use Elastica\Exception\InvalidException;
+
+/**
+ * Elastica index template object.
+ *
+ * @author Dmitry Balabka <dmitry.balabka@gmail.com>
+ *
+ * @see https://www.elastic.co/guide/en/elasticsearch/reference/current/indices-templates.html
+ */
+class IndexTemplate
+{
+    /**
+     * Index template name.
+     *
+     * @var string Index pattern
+     */
+    protected $_name;
+
+    /**
+     * Client object.
+     *
+     * @var \Elastica\Client Client object
+     */
+    protected $_client;
+
+    /**
+     * Creates a new index template object.
+     *
+     * @param \Elastica\Client $client Client object
+     * @param string           $name   Index template name
+     *
+     * @throws \Elastica\Exception\InvalidException
+     */
+    public function __construct(Client $client, $name)
+    {
+        $this->_client = $client;
+
+        if (!is_scalar($name)) {
+            throw new InvalidException('Index template should be a scalar type');
+        }
+        $this->_name = (string) $name;
+    }
+
+    /**
+     * Deletes the index template.
+     *
+     * @return \Elastica\Response Response object
+     */
+    public function delete()
+    {
+        $response = $this->request(Request::DELETE);
+
+        return $response;
+    }
+
+    /**
+     * Creates a new index template with the given arguments.
+     *
+     * @see https://www.elastic.co/guide/en/elasticsearch/reference/current/indices-templates.html
+     *
+     * @param array $args OPTIONAL Arguments to use
+     *
+     * @return \Elastica\Response
+     */
+    public function create(array $args = [])
+    {
+        return $this->request(Request::PUT, $args);
+    }
+
+    /**
+     * Checks if the given index template is already created.
+     *
+     * @return bool True if index exists
+     */
+    public function exists()
+    {
+        $response = $this->request(Request::HEAD);
+
+        return 200 === $response->getStatus();
+    }
+
+    /**
+     * Returns the index template name.
+     *
+     * @return string Index name
+     */
+    public function getName()
+    {
+        return $this->_name;
+    }
+
+    /**
+     * Returns index template client.
+     *
+     * @return \Elastica\Client Index client object
+     */
+    public function getClient()
+    {
+        return $this->_client;
+    }
+
+    /**
+     * Makes calls to the elasticsearch server based on this index template name.
+     *
+     * @param string $method Rest method to use (GET, POST, DELETE, PUT)
+     * @param array  $data   OPTIONAL Arguments as array
+     *
+     * @return \Elastica\Response Response object
+     */
+    public function request($method, $data = [])
+    {
+        $path = '_template/'.$this->getName();
+
+        return $this->getClient()->request($path, $method, $data);
+    }
+}

+ 88 - 0
lib/Elastica/JSON.php

@@ -0,0 +1,88 @@
+<?php
+
+namespace Elastica;
+
+use Elastica\Exception\JSONParseException;
+
+/**
+ * Elastica JSON tools.
+ */
+class JSON
+{
+    /**
+     * Parse JSON string to an array.
+     *
+     * @see http://php.net/manual/en/function.json-decode.php
+     * @see http://php.net/manual/en/function.json-last-error.php
+     *
+     * @param string $args,... JSON string to parse
+     *
+     * @throws JSONParseException
+     *
+     * @return array PHP array representation of JSON string
+     */
+    public static function parse($args/* inherit from json_decode */)
+    {
+        // extract arguments
+        $args = func_get_args();
+
+        // default to decoding into an assoc array
+        if (1 === count($args)) {
+            $args[] = true;
+        }
+
+        // run decode
+        $array = call_user_func_array('json_decode', $args);
+
+        // turn errors into exceptions for easier catching
+        if ($error = self::getJsonLastErrorMsg()) {
+            throw new JSONParseException($error);
+        }
+
+        // output
+        return $array;
+    }
+
+    /**
+     * Convert input to JSON string with standard options.
+     *
+     * @see http://php.net/manual/en/function.json-encode.php
+     * @see http://php.net/manual/en/function.json-last-error.php
+     *
+     * @param mixed $args,... Target to stringify
+     *
+     * @throws JSONParseException
+     *
+     * @return string Valid JSON representation of $input
+     */
+    public static function stringify($args/* inherit from json_encode */)
+    {
+        // extract arguments
+        $args = func_get_args();
+
+        // run encode and output
+        $string = call_user_func_array('json_encode', $args);
+
+        // turn errors into exceptions for easier catching
+        if ($error = self::getJsonLastErrorMsg()) {
+            throw new JSONParseException($error);
+        }
+
+        // output
+        return $string;
+    }
+
+    /**
+     * Get Json Last Error.
+     *
+     * @see http://php.net/manual/en/function.json-last-error.php
+     * @see http://php.net/manual/en/function.json-last-error-msg.php
+     * @see https://github.com/php/php-src/blob/master/ext/json/json.c#L308
+     *
+     * @return string
+     */
+    private static function getJsonLastErrorMsg()
+    {
+        return JSON_ERROR_NONE !== json_last_error() ? json_last_error_msg() : false;
+    }
+}

+ 82 - 0
lib/Elastica/Log.php

@@ -0,0 +1,82 @@
+<?php
+
+namespace Elastica;
+
+use Psr\Log\AbstractLogger;
+
+/**
+ * Elastica log object.
+ *
+ * @author Nicolas Ruflin <spam@ruflin.com>
+ */
+class Log extends AbstractLogger
+{
+    /**
+     * Log path or true if enabled.
+     *
+     * @var string|bool
+     */
+    protected $_log;
+
+    /**
+     * Last logged message.
+     *
+     * @var string Last logged message
+     */
+    protected $_lastMessage;
+
+    /**
+     * Inits log object.
+     *
+     * @param string|bool String to set a specific file for logging
+     */
+    public function __construct($log = '')
+    {
+        $this->setLog($log);
+    }
+
+    /**
+     * Log a message.
+     *
+     * @param mixed  $level
+     * @param string $message
+     * @param array  $context
+     *
+     * @return void|null
+     */
+    public function log($level, $message, array $context = [])
+    {
+        $context['error_message'] = $message;
+        $this->_lastMessage = JSON::stringify($context);
+
+        if (!empty($this->_log) && is_string($this->_log)) {
+            error_log($this->_lastMessage.PHP_EOL, 3, $this->_log);
+        } else {
+            error_log($this->_lastMessage);
+        }
+    }
+
+    /**
+     * Enable/disable log or set log path.
+     *
+     * @param bool|string $log Enables log or sets log path
+     *
+     * @return $this
+     */
+    public function setLog($log)
+    {
+        $this->_log = $log;
+
+        return $this;
+    }
+
+    /**
+     * Return last logged message.
+     *
+     * @return string Last logged message
+     */
+    public function getLastMessage()
+    {
+        return $this->_lastMessage;
+    }
+}

+ 60 - 0
lib/Elastica/Multi/MultiBuilder.php

@@ -0,0 +1,60 @@
+<?php
+
+namespace Elastica\Multi;
+
+use Elastica\Response;
+use Elastica\Search as BaseSearch;
+
+class MultiBuilder implements MultiBuilderInterface
+{
+    /**
+     * @param Response     $response
+     * @param BaseSearch[] $searches
+     *
+     * @return ResultSet
+     */
+    public function buildMultiResultSet(Response $response, $searches)
+    {
+        $resultSets = $this->buildResultSets($response, $searches);
+
+        return new ResultSet($response, $resultSets);
+    }
+
+    /**
+     * @param Response   $childResponse
+     * @param BaseSearch $search
+     *
+     * @return \Elastica\ResultSet
+     */
+    private function buildResultSet(Response $childResponse, BaseSearch $search)
+    {
+        return $search->getResultSetBuilder()->buildResultSet($childResponse, $search->getQuery());
+    }
+
+    /**
+     * @param Response     $response
+     * @param BaseSearch[] $searches
+     *
+     * @return \Elastica\ResultSet[]
+     */
+    private function buildResultSets(Response $response, $searches)
+    {
+        $data = $response->getData();
+        if (!isset($data['responses']) || !is_array($data['responses'])) {
+            return [];
+        }
+
+        $resultSets = [];
+        reset($searches);
+
+        foreach ($data['responses'] as $responseData) {
+            $search = current($searches);
+            $key = key($searches);
+            next($searches);
+
+            $resultSets[$key] = $this->buildResultSet(new Response($responseData), $search);
+        }
+
+        return $resultSets;
+    }
+}

+ 17 - 0
lib/Elastica/Multi/MultiBuilderInterface.php

@@ -0,0 +1,17 @@
+<?php
+
+namespace Elastica\Multi;
+
+use Elastica\Response;
+use Elastica\Search as BaseSearch;
+
+interface MultiBuilderInterface
+{
+    /**
+     * @param Response     $response
+     * @param BaseSearch[] $searches
+     *
+     * @return ResultSet
+     */
+    public function buildMultiResultSet(Response $response, $searches);
+}

+ 164 - 0
lib/Elastica/Multi/ResultSet.php

@@ -0,0 +1,164 @@
+<?php
+
+namespace Elastica\Multi;
+
+use Elastica\Response;
+
+/**
+ * Elastica multi search result set
+ * List of result sets for each search request.
+ *
+ * @author munkie
+ */
+class ResultSet implements \Iterator, \ArrayAccess, \Countable
+{
+    /**
+     * Result Sets.
+     *
+     * @var array|\Elastica\ResultSet[] Result Sets
+     */
+    protected $_resultSets = [];
+
+    /**
+     * Current position.
+     *
+     * @var int Current position
+     */
+    protected $_position = 0;
+
+    /**
+     * Response.
+     *
+     * @var \Elastica\Response Response object
+     */
+    protected $_response;
+
+    /**
+     * Constructs ResultSet object.
+     *
+     * @param \Elastica\Response    $response
+     * @param \Elastica\ResultSet[] $resultSets
+     */
+    public function __construct(Response $response, $resultSets)
+    {
+        $this->_response = $response;
+        $this->_resultSets = $resultSets;
+    }
+
+    /**
+     * @return array|\Elastica\ResultSet[]
+     */
+    public function getResultSets()
+    {
+        return $this->_resultSets;
+    }
+
+    /**
+     * Returns response object.
+     *
+     * @return \Elastica\Response Response object
+     */
+    public function getResponse()
+    {
+        return $this->_response;
+    }
+
+    /**
+     * There is at least one result set with error.
+     *
+     * @return bool
+     */
+    public function hasError()
+    {
+        foreach ($this->getResultSets() as $resultSet) {
+            if ($resultSet->getResponse()->hasError()) {
+                return true;
+            }
+        }
+
+        return false;
+    }
+
+    /**
+     * @return \Elastica\ResultSet
+     */
+    public function current()
+    {
+        return $this->_resultSets[$this->key()];
+    }
+
+    public function next()
+    {
+        ++$this->_position;
+    }
+
+    /**
+     * @return int
+     */
+    public function key()
+    {
+        return $this->_position;
+    }
+
+    /**
+     * @return bool
+     */
+    public function valid()
+    {
+        return isset($this->_resultSets[$this->key()]);
+    }
+
+    public function rewind()
+    {
+        $this->_position = 0;
+    }
+
+    /**
+     * @return int
+     */
+    public function count()
+    {
+        return count($this->_resultSets);
+    }
+
+    /**
+     * @param string|int $offset
+     *
+     * @return bool true on success or false on failure
+     */
+    public function offsetExists($offset)
+    {
+        return isset($this->_resultSets[$offset]);
+    }
+
+    /**
+     * @param mixed $offset
+     *
+     * @return mixed can return all value types
+     */
+    public function offsetGet($offset)
+    {
+        return $this->_resultSets[$offset] ?? null;
+    }
+
+    /**
+     * @param mixed $offset
+     * @param mixed $value
+     */
+    public function offsetSet($offset, $value)
+    {
+        if (is_null($offset)) {
+            $this->_resultSets[] = $value;
+        } else {
+            $this->_resultSets[$offset] = $value;
+        }
+    }
+
+    /**
+     * @param mixed $offset
+     */
+    public function offsetUnset($offset)
+    {
+        unset($this->_resultSets[$offset]);
+    }
+}

+ 210 - 0
lib/Elastica/Multi/Search.php

@@ -0,0 +1,210 @@
+<?php
+
+namespace Elastica\Multi;
+
+use Elastica\Client;
+use Elastica\JSON;
+use Elastica\Request;
+use Elastica\Search as BaseSearch;
+
+/**
+ * Elastica multi search.
+ *
+ * @author munkie
+ *
+ * @see https://www.elastic.co/guide/en/elasticsearch/reference/current/search-multi-search.html
+ */
+class Search
+{
+    /**
+     * @const string[] valid header options
+     */
+    private static $HEADER_OPTIONS = ['index', 'types', 'search_type',
+                                      'routing', 'preference', ];
+    /**
+     * @var MultiBuilderInterface
+     */
+    private $_builder;
+
+    /**
+     * @var \Elastica\Client
+     */
+    protected $_client;
+
+    /**
+     * @var array
+     */
+    protected $_options = [];
+
+    /**
+     * @var array|\Elastica\Search[]
+     */
+    protected $_searches = [];
+
+    /**
+     * Constructs search object.
+     *
+     * @param \Elastica\Client      $client  Client object
+     * @param MultiBuilderInterface $builder
+     */
+    public function __construct(Client $client, MultiBuilderInterface $builder = null)
+    {
+        $this->_builder = $builder ?: new MultiBuilder();
+        $this->_client = $client;
+    }
+
+    /**
+     * @return \Elastica\Client
+     */
+    public function getClient()
+    {
+        return $this->_client;
+    }
+
+    /**
+     * @return $this
+     */
+    public function clearSearches()
+    {
+        $this->_searches = [];
+
+        return $this;
+    }
+
+    /**
+     * @param \Elastica\Search $search
+     * @param string           $key    Optional key
+     *
+     * @return $this
+     */
+    public function addSearch(BaseSearch $search, $key = null)
+    {
+        if ($key) {
+            $this->_searches[$key] = $search;
+        } else {
+            $this->_searches[] = $search;
+        }
+
+        return $this;
+    }
+
+    /**
+     * @param array|\Elastica\Search[] $searches
+     *
+     * @return $this
+     */
+    public function addSearches(array $searches)
+    {
+        foreach ($searches as $key => $search) {
+            $this->addSearch($search, $key);
+        }
+
+        return $this;
+    }
+
+    /**
+     * @param array|\Elastica\Search[] $searches
+     *
+     * @return $this
+     */
+    public function setSearches(array $searches)
+    {
+        $this->clearSearches();
+        $this->addSearches($searches);
+
+        return $this;
+    }
+
+    /**
+     * @return array|\Elastica\Search[]
+     */
+    public function getSearches()
+    {
+        return $this->_searches;
+    }
+
+    /**
+     * @param string $searchType
+     *
+     * @return $this
+     */
+    public function setSearchType($searchType)
+    {
+        $this->_options[BaseSearch::OPTION_SEARCH_TYPE] = $searchType;
+
+        return $this;
+    }
+
+    /**
+     * @return \Elastica\Multi\ResultSet
+     */
+    public function search()
+    {
+        $data = $this->_getData();
+
+        $response = $this->getClient()->request(
+            '_msearch',
+            Request::POST,
+            $data,
+            $this->_options,
+        Request::NDJSON_CONTENT_TYPE
+        );
+
+        return $this->_builder->buildMultiResultSet($response, $this->getSearches());
+    }
+
+    /**
+     * @return string
+     */
+    protected function _getData()
+    {
+        $data = '';
+        foreach ($this->getSearches() as $search) {
+            $data .= $this->_getSearchData($search);
+        }
+
+        return $data;
+    }
+
+    /**
+     * @param \Elastica\Search $search
+     *
+     * @return string
+     */
+    protected function _getSearchData(BaseSearch $search)
+    {
+        $header = $this->_getSearchDataHeader($search);
+
+        $header = (empty($header)) ? new \stdClass() : $header;
+        $query = $search->getQuery();
+
+        // Keep other query options as part of the search body
+        $queryOptions = array_diff_key($search->getOptions(), array_flip(self::$HEADER_OPTIONS));
+
+        $data = JSON::stringify($header)."\n";
+        $data .= JSON::stringify($query->toArray() + $queryOptions)."\n";
+
+        return $data;
+    }
+
+    /**
+     * @param \Elastica\Search $search
+     *
+     * @return array
+     */
+    protected function _getSearchDataHeader(BaseSearch $search)
+    {
+        $header = $search->getOptions();
+
+        if ($search->hasIndices()) {
+            $header['index'] = $search->getIndices();
+        }
+
+        if ($search->hasTypes()) {
+            $header['types'] = $search->getTypes();
+        }
+
+        // Filter options accepted in the "header"
+        return array_intersect_key($header, array_flip(self::$HEADER_OPTIONS));
+    }
+}

برخی فایل ها در این مقایسه diff نمایش داده نمی شوند زیرا تعداد فایل ها بسیار زیاد است