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