diff --git a/composer.json b/composer.json index d5bf7fb..8a745f9 100644 --- a/composer.json +++ b/composer.json @@ -19,6 +19,7 @@ } }, "require": { - "guzzlehttp/guzzle": "^6.0" + "guzzlehttp/guzzle": "^6.0", + "ext-json": "*" } } diff --git a/examples/alias_operations.php b/examples/alias_operations.php index ec43e9e..3cde1b2 100644 --- a/examples/alias_operations.php +++ b/examples/alias_operations.php @@ -7,21 +7,15 @@ try { $client = new Client( [ - 'master_node' => [ - 'host' => 'HOST', - 'port' => '8108', - 'protocol' => 'http', - 'api_key' => 'API_KEY', - ], - 'read_replica_nodes' => [ + 'api_key' => 'abcd', + 'nodes' => [ [ - 'host' => 'HOST', + 'host' => 'localhost', 'port' => '8108', 'protocol' => 'http', - 'api_key' => 'API_KEY', ], ], - 'timeout_seconds' => 2, + 'connection_timeout_seconds' => 2, ] ); echo '
';
diff --git a/examples/collection_operations.php b/examples/collection_operations.php
index 2b3aac9..7471ec5 100644
--- a/examples/collection_operations.php
+++ b/examples/collection_operations.php
@@ -7,24 +7,19 @@
 try {
     $client = new Client(
       [
-        'master_node'        => [
-          'host'     => 'host',
-          'port'     => '8108',
-          'protocol' => 'http',
-          'api_key'  => 'api_key',
-        ],
-        'read_replica_nodes' => [
+        'api_key'         => 'abcd',
+        'nodes'           => [
           [
-            'host'     => 'host',
+            'host'     => 'localhost',
             'port'     => '8108',
             'protocol' => 'http',
-            'api_key'  => 'api_key',
           ],
         ],
-        'timeout_seconds'    => 2,
+        'connection_timeout_seconds' => 2,
       ]
     );
     echo '
';
+    //print_r($client->collections['books']->delete());
     echo "--------Create Collection-------\n";
     print_r(
       $client->collections->create(
@@ -104,7 +99,8 @@
     echo "--------Create Document-------\n";
     echo "\n";
     echo "--------Export Documents-------\n";
-    print_r($client->collections['books']->documents->export());
+    $exportedDocStrs = $client->collections['books']->documents->export();
+    print_r($exportedDocStrs);
     echo "--------Export Documents-------\n";
     echo "\n";
     echo "--------Fetch Single Document-------\n";
@@ -127,6 +123,16 @@
     print_r($client->collections['books']->documents['1']->delete());
     echo "--------Delete Document-------\n";
     echo "\n";
+    echo "--------Import Documents-------\n";
+    $docsToImport = [];
+    foreach ($exportedDocStrs as $exportedDocStr) {
+        $docsToImport[] = json_decode($exportedDocStr, true);
+    }
+    $importRes =
+      $client->collections['books']->documents->create_many($docsToImport);
+    print_r($importRes);
+    echo "--------Import Documents-------\n";
+    echo "\n";
     echo "--------Delete Collection-------\n";
     print_r($client->collections['books']->delete());
     echo "--------Delete Collection-------\n";
diff --git a/examples/curation_operations.php b/examples/curation_operations.php
index 5d2a25e..6ad88f4 100644
--- a/examples/curation_operations.php
+++ b/examples/curation_operations.php
@@ -7,21 +7,15 @@
 try {
     $client = new Client(
       [
-        'master_node'        => [
-          'host'     => 'HOST',
-          'port'     => '8108',
-          'protocol' => 'http',
-          'api_key'  => 'API_KEY',
-        ],
-        'read_replica_nodes' => [
+        'api_key'         => 'abcd',
+        'nodes'           => [
           [
-            'host'     => 'HOST',
+            'host'     => 'localhost',
             'port'     => '8108',
             'protocol' => 'http',
-            'api_key'  => 'API_KEY',
           ],
         ],
-        'timeout_seconds'    => 2,
+        'connection_timeout_seconds' => 2,
       ]
     );
     echo '
';
diff --git a/src/Alias.php b/src/Alias.php
index 730c0ad..5f863e1 100644
--- a/src/Alias.php
+++ b/src/Alias.php
@@ -63,7 +63,6 @@ public function retrieve(): array
     /**
      * @return array
      * @throws \Devloops\Typesence\Exceptions\TypesenseClientError
-     * @throws \GuzzleHttp\Exception\GuzzleException
      */
     public function delete(): array
     {
diff --git a/src/Aliases.php b/src/Aliases.php
index f60c210..fa70ebe 100644
--- a/src/Aliases.php
+++ b/src/Aliases.php
@@ -59,7 +59,6 @@ public function endPointPath(string $aliasName): string
      *
      * @return array
      * @throws \Devloops\Typesence\Exceptions\TypesenseClientError
-     * @throws \GuzzleHttp\Exception\GuzzleException
      */
     public function upsert(string $name, array $mapping): array
     {
diff --git a/src/ApiCall.php b/src/ApiCall.php
index ee92bd8..8926f27 100644
--- a/src/ApiCall.php
+++ b/src/ApiCall.php
@@ -2,6 +2,7 @@
 
 namespace Devloops\Typesence;
 
+use Devloops\Typesence\Lib\Node;
 use GuzzleHttp\Exception\GuzzleException;
 use Devloops\Typesence\Lib\Configuration;
 use GuzzleHttp\Exception\ClientException;
@@ -9,6 +10,7 @@
 use Devloops\Typesence\Exceptions\ServerError;
 use Devloops\Typesence\Exceptions\ObjectNotFound;
 use Devloops\Typesence\Exceptions\RequestMalformed;
+use Devloops\Typesence\Exceptions\ServiceUnavailable;
 use Devloops\Typesence\Exceptions\RequestUnauthorized;
 use Devloops\Typesence\Exceptions\ObjectAlreadyExists;
 use Devloops\Typesence\Exceptions\ObjectUnprocessable;
@@ -26,6 +28,8 @@ class ApiCall
 
     private const API_KEY_HEADER_NAME = 'X-TYPESENSE-API-KEY';
 
+    private const CHECK_FAILED_NODE_INTERVAL_S = 60;
+
     /**
      * @var \GuzzleHttp\Client
      */
@@ -36,232 +40,291 @@ class ApiCall
      */
     private $config;
 
+    /**
+     * @var array|\Devloops\Typesence\Lib\Node[]
+     */
+    private static $nodes;
+
+    /**
+     * @var \Devloops\Typesence\Lib\Node
+     */
+    private static $nearestNode;
+
+    /**
+     * @var int
+     */
+    private $nodeIndex;
+
+    /**
+     * @var int
+     */
+    private $lastHealthCheckTs;
+
     /**
      * ApiCall constructor.
      *
-     * @param   \Devloops\Typesence\Lib\Configuration  $config
+     * @param  \Devloops\Typesence\Lib\Configuration  $config
      */
     public function __construct(Configuration $config)
     {
-        $this->config = $config;
-        $this->client = new \GuzzleHttp\Client();
+        $this->config            = $config;
+        $this->client            = new \GuzzleHttp\Client();
+        self::$nodes             = $this->config->getNodes();
+        self::$nearestNode       = $this->config->getNearestNode();
+        $this->nodeIndex         = 0;
+        $this->lastHealthCheckTs = time();
+        $this->initializeNodes();
+    }
+
+    /**
+     *  Initialize Nodes
+     */
+    private function initializeNodes(): void
+    {
+        if (!empty(self::$nearestNode)) {
+            $this->setNodeHealthcheck(self::$nearestNode, true);
+        }
+
+        foreach (self::$nodes as &$node) {
+            $this->setNodeHealthcheck($node, true);
+        }
+    }
+
+    /**
+     * @param  string  $endPoint
+     * @param  array  $params
+     * @param  bool  $asJson
+     *
+     * @return string|array
+     * @throws \Devloops\Typesence\Exceptions\TypesenseClientError
+     * @throws \Exception
+     */
+    public function get(string $endPoint, array $params, bool $asJson = true)
+    {
+        return $this->makeRequest('get', $endPoint, $asJson, [
+          'data' => $params ?? [],
+        ]);
     }
 
     /**
+     * @param  string  $endPoint
+     * @param  mixed  $body
+     *
      * @return array
+     * @throws \Devloops\Typesence\Exceptions\TypesenseClientError
      */
-    private function nodes(): array
+    public function post(string $endPoint, $body): array
     {
-        return [$this->config->getMasterNode()]
-               + $this->config->getReadReplicaNodes();
+        return $this->makeRequest('post', $endPoint, true, [
+          'data' => $body ?? [],
+        ]);
     }
 
     /**
-     * @param   int  $httpCode
+     * @param  string  $endPoint
+     * @param  array  $body
      *
-     * @return \Devloops\Typesence\Exceptions\TypesenseClientError
+     * @return array
+     * @throws \Devloops\Typesence\Exceptions\TypesenseClientError
      */
-    public function getException(int $httpCode): TypesenseClientError
+    public function put(string $endPoint, array $body): array
     {
-        switch ($httpCode) {
-            case 400:
-                return new RequestMalformed();
-            case 401:
-                return new RequestUnauthorized();
-            case 404:
-                return new ObjectNotFound();
-            case 409:
-                return new ObjectAlreadyExists();
-            case 422:
-                return new ObjectUnprocessable();
-            case 500:
-                return new ServerError();
-            default:
-                return new TypesenseClientError();
-        }
+        return $this->makeRequest('put', $endPoint, true, [
+          'data' => $body ?? [],
+        ]);
     }
 
     /**
-     * @param   string  $endPoint
-     * @param   array   $params
-     * @param   bool    $asJson
+     * @param  string  $endPoint
      *
-     * @return string|array
+     * @return array
      * @throws \Devloops\Typesence\Exceptions\TypesenseClientError
-     * @throws \Exception
      */
-    public function get(string $endPoint, array $params, bool $asJson = true)
+    public function delete(string $endPoint): array
     {
-        $params = $params ?? [];
-        foreach ($this->nodes() as $node) {
-            $url = $node->url() . $endPoint;
+        return $this->makeRequest('delete', $endPoint, true, []);
+    }
+
+    /**
+     * Makes the actual http request, along with retries
+     *
+     * @param  string  $method
+     * @param  string  $endPoint
+     * @param  bool  $asJson
+     * @param  array  $options
+     *
+     * @return string|array
+     * @throws \Devloops\Typesence\Exceptions\TypesenseClientError
+     */
+    private function makeRequest(
+      string $method,
+      string $endPoint,
+      bool $asJson,
+      array $options
+    ) {
+        $numRetries = 0;
+        while ($numRetries < $this->config->getNumRetries()) {
+            $numRetries++;
+            $node = $this->getNode();
+            $node->setHealthy(false);
+
             try {
-                $request = $this->client->get(
-                  $url,
-                  [
-                    'headers'         => [
-                      self::API_KEY_HEADER_NAME => $node->getApiKey(),
-                    ],
-                    'query'           => http_build_query($params),
-                    'connect_timeout' => $this->config->getTimeoutSeconds(),
-                  ]
-                );
-                if ($request->getStatusCode() !== 200) {
-                    $errorMessage = \GuzzleHttp\json_decode(
-                                      $request->getBody(),
-                                      true
-                                    )['message'] ?? 'API error';
-                    throw $this->getException($request->getStatusCode())
+                $url   = $node->url().$endPoint;
+                $reqOp = $this->getRequestOptions();
+                if (isset($options['data'])) {
+                    if ($method === 'get') {
+                        $reqOp['query'] = http_build_query($options['data']);
+                    } elseif (is_string($options['data'])) {
+                        $reqOp['body'] = $options['data'];
+                    } else {
+                        $reqOp['json'] = $options['data'];
+                    }
+                }
+
+                $response = $this->client->request($method, $url, $reqOp);
+
+                $statusCode = $response->getStatusCode();
+                if (0 < $statusCode && $statusCode < 500) {
+                    $this->setNodeHealthcheck($node, true);
+                }
+
+                if (!(200 <= $statusCode && $statusCode < 300)) {
+                    $errorMessage =
+                      json_decode($response->getBody()->getContents(),
+                        true)['message'] ?? 'API error.';
+                    throw $this->getException($statusCode)
                                ->setMessage($errorMessage);
                 }
-                return $asJson ? \GuzzleHttp\json_decode(
-                  $request->getBody(),
-                  true
-                ) : $request->getBody()->getContents();
+
+                return $asJson ? json_decode($response->getBody()
+                                                      ->getContents(),
+                  true) : $response->getBody()->getContents();
             } catch (ClientException $exception) {
                 if ($exception->getResponse()->getStatusCode() === 408) {
                     continue;
                 }
-                throw $this->getException(
-                  $exception->getResponse()->getStatusCode()
-                )->setMessage($exception->getMessage());
+                $this->setNodeHealthcheck($node, false);
+                throw $this->getException($exception->getResponse()
+                                                    ->getStatusCode())
+                           ->setMessage($exception->getMessage());
             } catch (RequestException $exception) {
                 if ($exception->getResponse()->getStatusCode() === 408) {
                     continue;
                 }
-                throw $this->getException(
-                  $exception->getResponse()->getStatusCode()
-                )->setMessage($exception->getMessage());
-            } catch (GuzzleException $e) {
-                continue;
+                $this->setNodeHealthcheck($node, false);
+                throw $this->getException($exception->getResponse()
+                                                    ->getStatusCode())
+                           ->setMessage($exception->getMessage());
             } catch (TypesenseClientError $exception) {
+                $this->setNodeHealthcheck($node, false);
                 throw $exception;
             } catch (\Exception $exception) {
+                $this->setNodeHealthcheck($node, false);
                 throw $exception;
             }
+
+            sleep($this->config->getRetryIntervalSeconds());
         }
 
-        throw new TypesenseClientError('All hosts are bad');
+        throw new TypesenseClientError('Retries exceeded.');
     }
 
     /**
-     * @param   string  $endPoint
-     * @param   array   $body
-     *
      * @return array
-     * @throws \Devloops\Typesence\Exceptions\TypesenseClientError
-     * @throws \GuzzleHttp\Exception\GuzzleException
      */
-    public function post(string $endPoint, array $body): array
+    private function getRequestOptions(): array
     {
-        $url    = $this->config->getMasterNode()->url() . $endPoint;
-        $apiKey = $this->config->getMasterNode()->getApiKey();
-        try {
-            $request = $this->client->post(
-              $url,
-              [
-                'headers'         => [
-                  self::API_KEY_HEADER_NAME => $apiKey,
-                ],
-                'json'            => $body,
-                'connect_timeout' => $this->config->getTimeoutSeconds(),
-              ]
-            );
-            if ($request->getStatusCode() !== 201) {
-                $errorMessage =
-                  \GuzzleHttp\json_decode($request->getBody(), true)['message']
-                  ?? 'API error';
-                throw $this->getException($request->getStatusCode())
-                           ->setMessage(
-                             $errorMessage
-                           );
-            }
-        } catch (ClientException $exception) {
-            throw $this->getException(
-              $exception->getResponse()->getStatusCode()
-            )->setMessage($exception->getMessage());
+        return [
+          'headers'            => [
+            self::API_KEY_HEADER_NAME => $this->config->getApiKey(),
+          ], 'connect_timeout' => $this->config->getConnectionTimeoutSeconds(),
+        ];
+    }
+
+    /**
+     * @param  \Devloops\Typesence\Lib\Node  $node
+     *
+     * @return bool
+     */
+    private function nodeDueForHealthCheck(Node $node): bool
+    {
+        $currentTimestamp = time();
+        $checkNode        = ($currentTimestamp - $node->getLastAccessTs())
+                            > self::CHECK_FAILED_NODE_INTERVAL_S;
+        if ($checkNode) {
+            $this->lastHealthCheckTs = $currentTimestamp;
         }
 
-        return \GuzzleHttp\json_decode($request->getBody(), true);
+        return $checkNode;
     }
 
     /**
-     * @param   string  $endPoint
-     * @param   array   $body
+     * @param  \Devloops\Typesence\Lib\Node  $node
+     * @param  bool  $isHealthy
+     */
+    public function setNodeHealthcheck(Node $node, bool $isHealthy): void
+    {
+        $node->setHealthy($isHealthy);
+        $node->setLastAccessTs(time());
+    }
+
+    /**
+     * Returns a healthy host from the pool in a round-robin fashion
+     * Might return an unhealthy host periodically to check for recovery.
      *
-     * @return array
-     * @throws \Devloops\Typesence\Exceptions\TypesenseClientError
-     * @throws \GuzzleHttp\Exception\GuzzleException
+     * @return \Devloops\Typesence\Lib\Node
      */
-    public function put(string $endPoint, array $body): array
+    public function getNode(): Lib\Node
     {
-        $url    = $this->config->getMasterNode()->url() . $endPoint;
-        $apiKey = $this->config->getMasterNode()->getApiKey();
-        try {
-            $request = $this->client->put(
-              $url,
-              [
-                'headers'         => [
-                  self::API_KEY_HEADER_NAME => $apiKey,
-                ],
-                'json'            => $body,
-                'connect_timeout' => $this->config->getTimeoutSeconds(),
-              ]
-            );
-            if ($request->getStatusCode() !== 200) {
-                $errorMessage =
-                  \GuzzleHttp\json_decode($request->getBody(), true)['message']
-                  ?? 'API error';
-                throw $this->getException($request->getStatusCode())
-                           ->setMessage(
-                             $errorMessage
-                           );
+        if (self::$nearestNode) {
+            if (self::$nearestNode->isHealthy()
+                || $this->nodeDueForHealthCheck(self::$nearestNode)) {
+                return self::$nearestNode;
+            }
+        }
+        $i = 0;
+        while ($i < count(self::$nodes)) {
+            $i++;
+            $this->nodeIndex = ($this->nodeIndex + 1) % count(self::$nodes);
+            if (self::$nodes[$this->nodeIndex]->isHealthy()
+                || $this->nodeDueForHealthCheck(self::$nodes[$this->nodeIndex])) {
+                return self::$nodes[$this->nodeIndex];
             }
-        } catch (ClientException $exception) {
-            throw $this->getException(
-              $exception->getResponse()->getStatusCode()
-            )->setMessage($exception->getMessage());
         }
 
-        return \GuzzleHttp\json_decode($request->getBody(), true);
+        /**
+         * None of the nodes are marked healthy, but some of them could have become healthy since last health check.
+         * So we will just return the next node.
+         */
+        $this->nodeIndex = ($this->nodeIndex + 1) % count(self::$nodes);
+        return self::$nodes[$this->nodeIndex];
     }
 
     /**
-     * @param   string  $endPoint
+     * @param  int  $httpCode
      *
-     * @return array
-     * @throws \Devloops\Typesence\Exceptions\TypesenseClientError
-     * @throws \GuzzleHttp\Exception\GuzzleException
+     * @return \Devloops\Typesence\Exceptions\TypesenseClientError
      */
-    public function delete(string $endPoint): array
+    public function getException(int $httpCode): TypesenseClientError
     {
-        $url    = $this->config->getMasterNode()->url() . $endPoint;
-        $apiKey = $this->config->getMasterNode()->getApiKey();
-        try {
-            $request = $this->client->delete(
-              $url,
-              [
-                'headers'         => [
-                  self::API_KEY_HEADER_NAME => $apiKey,
-                ],
-                'connect_timeout' => $this->config->getTimeoutSeconds(),
-              ]
-            );
-            if ($request->getStatusCode() !== 200) {
-                $errorMessage =
-                  \GuzzleHttp\json_decode($request->getBody(), true)['message']
-                  ?? 'API error';
-                throw $this->getException($request->getStatusCode())
-                           ->setMessage(
-                             $errorMessage
-                           );
-            }
-        } catch (ClientException $exception) {
-            throw $this->getException(
-              $exception->getResponse()->getStatusCode()
-            )->setMessage($exception->getMessage());
+        switch ($httpCode) {
+            case 400:
+                return new RequestMalformed();
+            case 401:
+                return new RequestUnauthorized();
+            case 404:
+                return new ObjectNotFound();
+            case 409:
+                return new ObjectAlreadyExists();
+            case 422:
+                return new ObjectUnprocessable();
+            case 500:
+                return new ServerError();
+            case 503:
+                return new ServiceUnavailable();
+            default:
+                return new TypesenseClientError();
         }
-        return \GuzzleHttp\json_decode($request->getBody(), true);
     }
 
 }
\ No newline at end of file
diff --git a/src/Collection.php b/src/Collection.php
index ccb1f89..98a8842 100644
--- a/src/Collection.php
+++ b/src/Collection.php
@@ -90,7 +90,6 @@ public function retrieve(): array
     /**
      * @return array
      * @throws \Devloops\Typesence\Exceptions\TypesenseClientError
-     * @throws \GuzzleHttp\Exception\GuzzleException
      */
     public function delete(): array
     {
diff --git a/src/Collections.php b/src/Collections.php
index dda33c5..d1002d3 100644
--- a/src/Collections.php
+++ b/src/Collections.php
@@ -66,7 +66,6 @@ public function __get($collectionName)
      *
      * @return array
      * @throws \Devloops\Typesence\Exceptions\TypesenseClientError
-     * @throws \GuzzleHttp\Exception\GuzzleException
      */
     public function create(array $schema): array
     {
diff --git a/src/Document.php b/src/Document.php
index 13a245c..a5c9293 100644
--- a/src/Document.php
+++ b/src/Document.php
@@ -78,7 +78,6 @@ public function retrieve(): array
     /**
      * @return array
      * @throws \Devloops\Typesence\Exceptions\TypesenseClientError
-     * @throws \GuzzleHttp\Exception\GuzzleException
      */
     public function delete(): array
     {
diff --git a/src/Documents.php b/src/Documents.php
index 3933541..76ca8ce 100644
--- a/src/Documents.php
+++ b/src/Documents.php
@@ -70,13 +70,28 @@ private function endPointPath(string $action = ''): string
      *
      * @return array
      * @throws \Devloops\Typesence\Exceptions\TypesenseClientError
-     * @throws \GuzzleHttp\Exception\GuzzleException
      */
     public function create(array $document): array
     {
         return $this->apiCall->post($this->endPointPath(''), $document);
     }
 
+    /**
+     * @param   array  $documents
+     *
+     * @return array
+     * @throws \Devloops\Typesence\Exceptions\TypesenseClientError
+     */
+    public function create_many(array $documents): array
+    {
+        $documentsStr = [];
+        foreach ($documents as $document) {
+            $documentsStr[] = json_encode($document);
+        }
+        $docsImport = implode("\n", $documentsStr);
+        return $this->apiCall->post($this->endPointPath('import'), $docsImport);
+    }
+
     /**
      * @return array
      * @throws \Devloops\Typesence\Exceptions\TypesenseClientError
diff --git a/src/Exceptions/ServiceUnavailable.php b/src/Exceptions/ServiceUnavailable.php
new file mode 100644
index 0000000..a8bd3be
--- /dev/null
+++ b/src/Exceptions/ServiceUnavailable.php
@@ -0,0 +1,16 @@
+
+ */
+class ServiceUnavailable extends TypesenseClientError
+{
+
+}
\ No newline at end of file
diff --git a/src/Key.php b/src/Key.php
new file mode 100644
index 0000000..904dd7f
--- /dev/null
+++ b/src/Key.php
@@ -0,0 +1,76 @@
+
+ */
+class Key
+{
+
+    /**
+     * @var \Devloops\Typesence\Lib\Configuration
+     */
+    private $congif;
+
+    /**
+     * @var \Devloops\Typesence\ApiCall
+     */
+    private $apiCall;
+
+    /**
+     * @var string
+     */
+    private $keyId;
+
+    /**
+     * Key constructor.
+     *
+     * @param  \Devloops\Typesence\Lib\Configuration  $congif
+     * @param  \Devloops\Typesence\ApiCall  $apiCall
+     * @param  string  $keyId
+     */
+    public function __construct(
+      Configuration $congif,
+      ApiCall $apiCall,
+      string $keyId
+    ) {
+        $this->congif  = $congif;
+        $this->apiCall = $apiCall;
+        $this->keyId   = $keyId;
+    }
+
+    /**
+     * @return string
+     */
+    private function endpointPath(): string
+    {
+        return sprintf('%s/%s', Keys::RESOURCE_PATH, $this->keyId);
+    }
+
+    /**
+     * @return array
+     * @throws \Devloops\Typesence\Exceptions\TypesenseClientError
+     */
+    public function retrieve(): array
+    {
+        return $this->apiCall->get($this->endpointPath(), []);
+    }
+
+    /**
+     * @return array
+     * @throws \Devloops\Typesence\Exceptions\TypesenseClientError
+     */
+    public function delete(): array
+    {
+        return $this->apiCall->delete($this->endpointPath());
+    }
+
+}
\ No newline at end of file
diff --git a/src/Keys.php b/src/Keys.php
new file mode 100644
index 0000000..a57c61e
--- /dev/null
+++ b/src/Keys.php
@@ -0,0 +1,131 @@
+
+ */
+class Keys implements \ArrayAccess
+{
+
+    public const RESOURCE_PATH = '/collections';
+
+    /**
+     * @var \Devloops\Typesence\Lib\Configuration
+     */
+    private $congif;
+
+    /**
+     * @var \Devloops\Typesence\ApiCall
+     */
+    private $apiCall;
+
+    /**
+     * @var array
+     */
+    private $keys = [];
+
+    /**
+     * Keys constructor.
+     *
+     * @param  \Devloops\Typesence\Lib\Configuration  $congif
+     * @param  \Devloops\Typesence\ApiCall  $apiCall
+     */
+    public function __construct(
+      Configuration $congif,
+      ApiCall $apiCall
+    ) {
+        $this->congif  = $congif;
+        $this->apiCall = $apiCall;
+    }
+
+    /**
+     * @param  array  $schema
+     *
+     * @return array
+     * @throws \Devloops\Typesence\Exceptions\TypesenseClientError
+     */
+    public function create(array $schema): array
+    {
+        return $this->apiCall->post(self::RESOURCE_PATH, $schema);
+    }
+
+    /**
+     * @param  string  $searchKey
+     * @param  array  $parameters
+     *
+     * @return string
+     */
+    public function generate_scoped_search_key(
+      string $searchKey,
+      array $parameters
+    ): string {
+        $paramStr     = json_encode($parameters);
+        $digest       =
+          base64_encode(hash_hmac('sha256', utf8_encode($paramStr),
+            utf8_encode($searchKey)));
+        $keyPrefix    = substr($searchKey, 0, 4);
+        $rawScopedKey =
+          sprintf('%s%s%s', utf8_decode($digest), $keyPrefix, $paramStr);
+        return base64_encode(utf8_encode($rawScopedKey));
+    }
+
+    /**
+     * @return array
+     * @throws \Devloops\Typesence\Exceptions\TypesenseClientError
+     */
+    public function retrieve(): array
+    {
+        return $this->apiCall->get(self::RESOURCE_PATH, []);
+    }
+
+    /**
+     * @param  mixed  $offset
+     *
+     * @return bool
+     */
+    public function offsetExists($offset): bool
+    {
+        return isset($this->keys[$offset]);
+    }
+
+    /**
+     * @param  mixed  $offset
+     *
+     * @return \Devloops\Typesence\Key
+     */
+    public function offsetGet($offset): Key
+    {
+        if (!isset($this->keys[$offset])) {
+            $this->keys[$offset] =
+              new Key($this->congif, $this->apiCall, $offset);
+        }
+
+        return $this->keys[$offset];
+    }
+
+    /**
+     * @param  mixed  $offset
+     * @param  mixed  $value
+     */
+    public function offsetSet($offset, $value): void
+    {
+        $this->keys[$offset] = $value;
+    }
+
+    /**
+     * @param  mixed  $offset
+     */
+    public function offsetUnset($offset): void
+    {
+        unset($this->keys[$offset]);
+    }
+
+}
\ No newline at end of file
diff --git a/src/Override.php b/src/Override.php
index 9013bed..a9aaf9a 100644
--- a/src/Override.php
+++ b/src/Override.php
@@ -79,7 +79,6 @@ public function retrieve(): array
     /**
      * @return array
      * @throws \Devloops\Typesence\Exceptions\TypesenseClientError
-     * @throws \GuzzleHttp\Exception\GuzzleException
      */
     public function delete(): array
     {
diff --git a/src/Overrides.php b/src/Overrides.php
index 5d31d50..58c04df 100644
--- a/src/Overrides.php
+++ b/src/Overrides.php
@@ -72,7 +72,6 @@ public function endPointPath(string $overrideId = ''): string
      *
      * @return array
      * @throws \Devloops\Typesence\Exceptions\TypesenseClientError
-     * @throws \GuzzleHttp\Exception\GuzzleException
      */
     public function upsert(string $documentId, array $config): array
     {
diff --git a/src/lib/Configuration.php b/src/lib/Configuration.php
index 46a3ccb..8b161b5 100644
--- a/src/lib/Configuration.php
+++ b/src/lib/Configuration.php
@@ -14,25 +14,40 @@
 class Configuration
 {
 
+    /**
+     * @var \Devloops\Typesence\Lib\Node[]
+     */
+    private $nodes;
+
     /**
      * @var \Devloops\Typesence\Lib\Node
      */
-    private $masterNode;
+    private $nearestNode;
 
     /**
-     * @var array
+     * @var float
      */
-    private $readReplicaNodes = [];
+    private $connectionTimeoutSeconds;
+
+    /**
+     * @var mixed|string
+     */
+    private $apiKey;
 
     /**
      * @var float
      */
-    private $timeoutSeconds;
+    private $numRetries;
+
+    /**
+     * @var float
+     */
+    private $retryIntervalSeconds;
 
     /**
      * Configuration constructor.
      *
-     * @param   array  $config
+     * @param  array  $config
      *
      * @throws \Devloops\Typesence\Exceptions\ConfigError
      */
@@ -40,90 +55,114 @@ public function __construct(array $config)
     {
         $this->validateConfigArray($config);
 
-        $masterNodeArray   = $config['master_node'] ?? [];
-        $replicaNodeArrays = $config['read_replica_nodes'] ?? [];
-
-        $this->masterNode = new Node(
-          $masterNodeArray['host'],
-          $masterNodeArray['port'],
-          $masterNodeArray['protocol'],
-          $masterNodeArray['api_key']
-        );
-
-        foreach ($replicaNodeArrays as $replica_node_array) {
-            $this->readReplicaNodes[] = new Node(
-              $replica_node_array['host'],
-              $replica_node_array['port'],
-              $replica_node_array['protocol'],
-              $replica_node_array['api_key']
-            );
+        $nodes = $config['nodes'] ?? [];
+
+        foreach ($nodes as $node) {
+            $this->nodes[] =
+              new Node($node['host'], $node['port'], $node['path'] ?? '',
+                $node['protocol']);
         }
 
-        $this->timeoutSeconds = (float)($config['timeout_seconds'] ?? 1.0);
-    }
+        $nearestNode = $config['nearest_node'] ?? [];
+        if (!empty($nearestNode)) {
+            $this->nearestNode =
+              new Node($nearestNode['host'], $nearestNode['post'],
+                $nearestNode['path'] ?? '', $nearestNode['protocol']);
+        }
 
+        $this->apiKey                   = $config['api_key'] ?? '';
+        $this->connectionTimeoutSeconds =
+          (float) ($config['connection_timeout_seconds'] ?? 1.0);
+        $this->numRetries               = (float) ($config['num_retries'] ?? 3);
+        $this->retryIntervalSeconds     =
+          (float) ($config['retry_interval_seconds'] ?? 1.0);
+    }
 
     /**
-     * @param   array  $config
+     * @param  array  $config
      *
      * @throws \Devloops\Typesence\Exceptions\ConfigError
      */
     private function validateConfigArray(array $config): void
     {
-        $masterNode = $config['master_node'] ?? false;
-        if (!$masterNode) {
-            throw new ConfigError('`master_node` is not defined.');
+        $nodes = $config['nodes'] ?? false;
+        if (!$nodes) {
+            throw new ConfigError('`nodes` is not defined.');
         }
 
-        if (!$this->validateNodeFields($masterNode)) {
-            throw new ConfigError(
-              '`master_node` must be a dictionary with the following required keys: host, port, protocol, api_key'
-            );
+        $apiKey = $config['api_key'] ?? false;
+        if (!$apiKey) {
+            throw new ConfigError('`api_key` is not defined.');
         }
 
-        $replicaNodes = $config['read_replica_nodes'] ?? [];
-        foreach ($replicaNodes as $replica_node) {
-            if (!$this->validateNodeFields($replica_node)) {
-                throw new ConfigError(
-                  '`read_replica_nodes` entry be a dictionary with the following required keys: host, port, protocol, api_key'
-                );
+        foreach ($nodes as $node) {
+            if (!$this->validateNodeFields($node)) {
+                throw new ConfigError('`node` entry be a dictionary with the following required keys: host, port, protocol, api_key');
             }
         }
+        $nearestNode = $config['nearest_node'] ?? [];
+        if (!empty($nearestNode) && !$this->validateNodeFields($nearestNode)) {
+            throw new ConfigError('`nearest_node` entry be a dictionary with the following required keys: host, port, protocol, api_key');
+        }
     }
 
     /**
-     * @param   array  $node
+     * @param  array  $node
      *
      * @return bool
      */
     public function validateNodeFields(array $node): bool
     {
-        $keys = ['host', 'port', 'protocol', 'api_key'];
+        $keys = ['host', 'port', 'protocol'];
         return !array_diff_key(array_flip($keys), $node);
     }
 
     /**
-     * @return \Devloops\Typesence\Node
+     * @return \Devloops\Typesence\Lib\Node[]
      */
-    public function getMasterNode(): Node
+    public function getNodes(): array
     {
-        return $this->masterNode;
+        return $this->nodes;
     }
 
     /**
-     * @return array
+     * @return \Devloops\Typesence\Lib\Node
+     */
+    public function getNearestNode(): Node
+    {
+        return $this->nearestNode;
+    }
+
+    /**
+     * @return mixed|string
+     */
+    public function getApiKey()
+    {
+        return $this->apiKey;
+    }
+
+    /**
+     * @return float
+     */
+    public function getNumRetries(): float
+    {
+        return $this->numRetries;
+    }
+
+    /**
+     * @return float
      */
-    public function getReadReplicaNodes(): array
+    public function getRetryIntervalSeconds(): float
     {
-        return $this->readReplicaNodes;
+        return $this->retryIntervalSeconds;
     }
 
     /**
      * @return float
      */
-    public function getTimeoutSeconds(): float
+    public function getConnectionTimeoutSeconds(): float
     {
-        return $this->timeoutSeconds;
+        return $this->connectionTimeoutSeconds;
     }
 
 }
\ No newline at end of file
diff --git a/src/lib/Node.php b/src/lib/Node.php
index 4cc4723..207824f 100644
--- a/src/lib/Node.php
+++ b/src/lib/Node.php
@@ -26,27 +26,42 @@ class Node
     /**
      * @var string
      */
-    private $protocol;
+    private $path;
 
     /**
      * @var string
      */
-    private $apiKey;
+    private $protocol;
+
+    /**
+     * @var bool
+     */
+    private $healthy = false;
+
+    /**
+     * @var int
+     */
+    private $lastAccessTs;
 
     /**
      * Node constructor.
      *
-     * @param $host
-     * @param $port
-     * @param $protocol
-     * @param $apiKey
+     * @param  string  $host
+     * @param  string  $port
+     * @param  string  $path
+     * @param  string  $protocol
      */
-    public function __construct($host, $port, $protocol, $apiKey)
-    {
-        $this->host     = $host;
-        $this->port     = $port;
-        $this->protocol = $protocol;
-        $this->apiKey   = $apiKey;
+    public function __construct(
+      string $host,
+      string $port,
+      string $path,
+      string $protocol
+    ) {
+        $this->host         = $host;
+        $this->port         = $port;
+        $this->path         = $path;
+        $this->protocol     = $protocol;
+        $this->lastAccessTs = time();
     }
 
     /**
@@ -54,15 +69,40 @@ public function __construct($host, $port, $protocol, $apiKey)
      */
     public function url(): string
     {
-        return sprintf('%s://%s:%s', $this->protocol, $this->host, $this->port);
+        return sprintf('%s://%s:%s%s', $this->protocol, $this->host,
+          $this->port, $this->path);
     }
 
     /**
-     * @return string
+     * @return bool
+     */
+    public function isHealthy(): bool
+    {
+        return $this->healthy;
+    }
+
+    /**
+     * @param  bool  $healthy
+     */
+    public function setHealthy(bool $healthy): void
+    {
+        $this->healthy = $healthy;
+    }
+
+    /**
+     * @return int
+     */
+    public function getLastAccessTs(): int
+    {
+        return $this->lastAccessTs;
+    }
+
+    /**
+     * @param  int  $lastAccessTs
      */
-    public function getApiKey(): string
+    public function setLastAccessTs(int $lastAccessTs): void
     {
-        return $this->apiKey;
+        $this->lastAccessTs = $lastAccessTs;
     }
 
 }
\ No newline at end of file