From 0a9949208a81e643eb681570aa74da8e8b8d2ca9 Mon Sep 17 00:00:00 2001 From: bepsvpt <8221099+bepsvpt@users.noreply.github.com> Date: Sun, 10 Dec 2023 22:04:12 +0800 Subject: [PATCH 01/15] chore: init for 5.0 --- .github/workflows/testing.yml | 98 +++++ .gitignore | 15 +- composer.json | 57 +-- docker-compose.yml | 25 +- examples/README.md | 14 - examples/alias_operations.php | 142 ------- examples/cluster_operations.php | 29 -- examples/collection_operations.php | 232 ----------- examples/curation_operations.php | 148 ------- examples/info_operations.php | 31 -- examples/keys_operations.php | 160 -------- examples/synonym_operations.php | 150 ------- phpcs.xml | 45 --- phpstan.neon | 19 + phpunit.xml | 45 +++ src/Alias.php | 65 --- src/Aliases.php | 124 ------ src/Analytics.php | 25 -- src/AnalyticsRule.php | 30 -- src/AnalyticsRules.php | 39 -- src/ApiCall.php | 369 ------------------ src/Client.php | 180 --------- src/Collection.php | 118 ------ src/Collections.php | 112 ------ src/Debug.php | 41 -- src/Document.php | 90 ----- src/Documents.php | 234 ----------- .../Client/ClientErrorException.php | 12 + .../Client/InvalidPayloadException.php | 10 + .../Client/ResourceAlreadyExistsException.php | 10 + .../Client/ResourceNotFoundException.php | 10 + .../Client/UnauthorizedException.php | 10 + .../Client/UnprocessableEntityException.php | 10 + src/Exceptions/ConfigError.php | 15 - src/Exceptions/HTTPStatus0Error.php | 14 - .../MalformedResponsePayloadException.php | 17 + src/Exceptions/ObjectAlreadyExists.php | 15 - src/Exceptions/ObjectNotFound.php | 15 - src/Exceptions/ObjectUnprocessable.php | 15 - src/Exceptions/RequestMalformed.php | 15 - src/Exceptions/RequestUnauthorized.php | 15 - .../Server/ServerErrorException.php | 12 + .../Server/ServiceUnavailableException.php | 10 + src/Exceptions/ServerError.php | 15 - src/Exceptions/ServiceUnavailable.php | 15 - src/Exceptions/Timeout.php | 15 - src/Exceptions/TypesenseClientError.php | 22 -- src/Exceptions/TypesenseException.php | 12 + src/Exceptions/UnknownHttpException.php | 10 + src/Health.php | 41 -- src/Http.php | 56 +++ src/Key.php | 65 --- src/Keys.php | 129 ------ src/Lib/Configuration.php | 226 ----------- src/Lib/Node.php | 105 ----- src/Metrics.php | 41 -- src/MultiSearch.php | 48 --- src/Objects/Collection.php | 54 +++ src/Objects/CollectionDroppedField.php | 14 + src/Objects/CollectionField.php | 62 +++ src/Objects/Document.php | 10 + src/Objects/GenericDocument.php | 13 + src/Objects/ImportedDocument.php | 20 + src/Objects/TypesenseObject.php | 29 ++ src/Operations.php | 48 --- src/Override.php | 78 ---- src/Overrides.php | 119 ------ src/Presets.php | 107 ----- src/Requests/Collection.php | 142 +++++++ src/Requests/Document.php | 268 +++++++++++++ src/Requests/Request.php | 127 ++++++ src/Synonym.php | 76 ---- src/Synonyms.php | 117 ------ src/Typesense.php | 58 +++ tests/Objects/TestObject.php | 14 + tests/Pest.php | 7 + tests/TestCase.php | 70 ++++ tests/Unit/ArchitectureTest.php | 26 ++ tests/Unit/AuthenticationTest.php | 18 + tests/Unit/CollectionTest.php | 223 +++++++++++ tests/Unit/DocumentTest.php | 276 +++++++++++++ tests/Unit/HttpClientTest.php | 33 ++ tests/Unit/MiscellaneousTest.php | 18 + tests/Unit/ObjectCreationTest.php | 22 ++ 84 files changed, 1905 insertions(+), 3786 deletions(-) create mode 100644 .github/workflows/testing.yml delete mode 100644 examples/README.md delete mode 100644 examples/alias_operations.php delete mode 100644 examples/cluster_operations.php delete mode 100644 examples/collection_operations.php delete mode 100644 examples/curation_operations.php delete mode 100644 examples/info_operations.php delete mode 100644 examples/keys_operations.php delete mode 100644 examples/synonym_operations.php delete mode 100644 phpcs.xml create mode 100644 phpstan.neon create mode 100644 phpunit.xml delete mode 100644 src/Alias.php delete mode 100644 src/Aliases.php delete mode 100644 src/Analytics.php delete mode 100644 src/AnalyticsRule.php delete mode 100644 src/AnalyticsRules.php delete mode 100644 src/ApiCall.php delete mode 100644 src/Client.php delete mode 100644 src/Collection.php delete mode 100644 src/Collections.php delete mode 100644 src/Debug.php delete mode 100644 src/Document.php delete mode 100644 src/Documents.php create mode 100644 src/Exceptions/Client/ClientErrorException.php create mode 100644 src/Exceptions/Client/InvalidPayloadException.php create mode 100644 src/Exceptions/Client/ResourceAlreadyExistsException.php create mode 100644 src/Exceptions/Client/ResourceNotFoundException.php create mode 100644 src/Exceptions/Client/UnauthorizedException.php create mode 100644 src/Exceptions/Client/UnprocessableEntityException.php delete mode 100644 src/Exceptions/ConfigError.php delete mode 100644 src/Exceptions/HTTPStatus0Error.php create mode 100644 src/Exceptions/MalformedResponsePayloadException.php delete mode 100644 src/Exceptions/ObjectAlreadyExists.php delete mode 100644 src/Exceptions/ObjectNotFound.php delete mode 100644 src/Exceptions/ObjectUnprocessable.php delete mode 100644 src/Exceptions/RequestMalformed.php delete mode 100644 src/Exceptions/RequestUnauthorized.php create mode 100644 src/Exceptions/Server/ServerErrorException.php create mode 100644 src/Exceptions/Server/ServiceUnavailableException.php delete mode 100644 src/Exceptions/ServerError.php delete mode 100644 src/Exceptions/ServiceUnavailable.php delete mode 100644 src/Exceptions/Timeout.php delete mode 100644 src/Exceptions/TypesenseClientError.php create mode 100644 src/Exceptions/TypesenseException.php create mode 100644 src/Exceptions/UnknownHttpException.php delete mode 100644 src/Health.php create mode 100644 src/Http.php delete mode 100644 src/Key.php delete mode 100644 src/Keys.php delete mode 100644 src/Lib/Configuration.php delete mode 100644 src/Lib/Node.php delete mode 100644 src/Metrics.php delete mode 100644 src/MultiSearch.php create mode 100644 src/Objects/Collection.php create mode 100644 src/Objects/CollectionDroppedField.php create mode 100644 src/Objects/CollectionField.php create mode 100644 src/Objects/Document.php create mode 100644 src/Objects/GenericDocument.php create mode 100644 src/Objects/ImportedDocument.php create mode 100644 src/Objects/TypesenseObject.php delete mode 100644 src/Operations.php delete mode 100644 src/Override.php delete mode 100644 src/Overrides.php delete mode 100644 src/Presets.php create mode 100644 src/Requests/Collection.php create mode 100644 src/Requests/Document.php create mode 100644 src/Requests/Request.php delete mode 100644 src/Synonym.php delete mode 100644 src/Synonyms.php create mode 100644 src/Typesense.php create mode 100644 tests/Objects/TestObject.php create mode 100644 tests/Pest.php create mode 100644 tests/TestCase.php create mode 100644 tests/Unit/ArchitectureTest.php create mode 100644 tests/Unit/AuthenticationTest.php create mode 100644 tests/Unit/CollectionTest.php create mode 100644 tests/Unit/DocumentTest.php create mode 100644 tests/Unit/HttpClientTest.php create mode 100644 tests/Unit/MiscellaneousTest.php create mode 100644 tests/Unit/ObjectCreationTest.php diff --git a/.github/workflows/testing.yml b/.github/workflows/testing.yml new file mode 100644 index 00000000..1cbac5d8 --- /dev/null +++ b/.github/workflows/testing.yml @@ -0,0 +1,98 @@ +name: Testing + +on: + push: + +jobs: + static_analyze: + name: Static Analyze + + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v4 + + - name: Setup PHP with PECL extension + uses: shivammathur/setup-php@v2 + with: + php-version: 8.3 + coverage: xdebug + + - name: Get composer cache directory + run: echo "COMPOSER_DIR=$(composer config cache-files-dir)" >> $GITHUB_ENV + + - name: Cache dependencies + uses: actions/cache@v3 + with: + path: ${{ env.COMPOSER_DIR }} + key: ${{ runner.os }}-composer-static-analyze-${{ hashFiles('**/composer.json') }} + restore-keys: ${{ runner.os }}-composer-static-analyze- + + - name: Install dependencies + run: composer update --no-progress --no-interaction + + - name: Check runtime dependencies + run: composer check-platform-reqs + + - name: Run composer validate + run: composer validate --strict + + - name: Run composer normalize + run: composer normalize --dry-run + + - name: Run static analysis + run: vendor/bin/phpstan --memory-limit=-1 --verbose + + - name: Run coding style checker + run: vendor/bin/pint -v --test + + - name: Run type coverage check + run: vendor/bin/pest --type-coverage --min=95 + + testing: + name: PHP ${{ matrix.php }} (Testing) + + runs-on: ubuntu-latest + + strategy: + fail-fast: false + + matrix: + php: [ '8.3', '8.2' ] + + services: + typesense: + image: typesense/typesense:0.25.1 + ports: + - 8108:8108/tcp + volumes: + - typesense_data:/data + env: + TYPESENSE_DATA_DIR: /data + TYPESENSE_API_KEY: testing + TYPESENSE_ENABLE_CORS: true + + steps: + - uses: actions/checkout@v4 + + - name: Setup PHP with PECL extension + uses: shivammathur/setup-php@v2 + with: + php-version: ${{ matrix.php }} + coverage: xdebug + + - name: Get composer cache directory + run: echo "COMPOSER_DIR=$(composer config cache-files-dir)" >> $GITHUB_ENV + + - name: Cache dependencies + uses: actions/cache@v3 + with: + path: ${{ env.COMPOSER_DIR }} + key: ${{ runner.os }}-composer-php-${{ matrix.php }}-${{ hashFiles('**/composer.json') }} + restore-keys: ${{ runner.os }}-composer-php-${{ matrix.php }}- + + - name: Install dependencies + run: composer update --no-progress --no-interaction + + - name: Run tests + run: vendor/bin/pest --coverage --min=95 diff --git a/.gitignore b/.gitignore index 8228c840..4d7bb9f5 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,11 @@ -.idea -.tmp -/composer.lock -vendor +/.fleet +/.idea +/.vscode +/coverage +/vendor +.DS_Store +.phpunit.result.cache +clover.xml +composer.phar +composer.lock +Thumbs.db \ No newline at end of file diff --git a/composer.json b/composer.json index 757297e7..26de44af 100644 --- a/composer.json +++ b/composer.json @@ -1,9 +1,8 @@ { "name": "typesense/typesense-php", "description": "PHP client for Typesense Search Server: https://github.com/typesense/typesense", - "type": "library", - "homepage": "https://github.com/typesense/typesense-php", "license": "Apache-2.0", + "type": "library", "authors": [ { "name": "Typesense", @@ -18,34 +17,48 @@ "role": "Developer" } ], + "homepage": "https://github.com/typesense/typesense-php", "support": { - "docs": "https://typesense.org/api", + "issues": "https://github.com/typesense/typesense-php/issues", "source": "https://github.com/typesense/typesense-php", - "issues": "https://github.com/typesense/typesense-php/issues" + "docs": "https://typesense.org/docs/api" + }, + "require": { + "php": "^8.2", + "php-http/discovery": "^1.19", + "psr/http-client": "^1.0", + "psr/http-client-implementation": "^1.0", + "psr/http-factory": "^1.0", + "psr/http-factory-implementation": "^1.0" + }, + "require-dev": { + "ergebnis/composer-normalize": "^2.40", + "guzzlehttp/guzzle": "^7.8", + "laravel/pint": "^1.13", + "pestphp/pest": "^2.28", + "pestphp/pest-plugin-faker": "^2.0", + "pestphp/pest-plugin-type-coverage": "^2.5", + "phpstan/phpstan": "^1.10", + "symfony/http-client": "^7.0" }, "minimum-stability": "stable", + "prefer-stable": true, "autoload": { "psr-4": { "Typesense\\": "src/" } }, - "require": { - "php": ">=7.4", - "ext-json": "*", - "monolog/monolog": "^2.1 || ^3.0 || ^3.3", - "nyholm/psr7": "^1.3", - "php-http/client-common": "^1.0 || ^2.3", - "php-http/discovery": "^1.0", - "php-http/httplug": "^1.0 || ^2.2", - "psr/http-client-implementation": "^1.0", - "psr/http-message": "^1.0 || ^2.0", - "psr/http-factory": "^1.0" - }, - "require-dev": { - "squizlabs/php_codesniffer": "3.*", - "symfony/http-client": "^5.2" + "autoload-dev": { + "psr-4": { + "Typesense\\Tests\\": "tests/" + } }, "config": { + "allow-plugins": { + "ergebnis/composer-normalize": true, + "pestphp/pest-plugin": true, + "php-http/discovery": false + }, "optimize-autoloader": true, "preferred-install": { "*": "dist" @@ -53,11 +66,11 @@ "sort-packages": true }, "scripts": { + "lint": "phpcs -v", + "lint:fix": "phpcbf", "typesenseServer": [ "Composer\\Config::disableProcessTimeout", "docker-compose up" - ], - "lint": "phpcs -v", - "lint:fix": "phpcbf" + ] } } diff --git a/docker-compose.yml b/docker-compose.yml index 41be3e38..c2a87649 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,13 +1,16 @@ -version: '3.5' +version: "3.8" services: - typesense: - image: typesense/typesense:0.21.0.rc20 - environment: - TYPESENSE_DATA_DIR: /data - TYPESENSE_API_KEY: xyz - volumes: - - /tmp/typesense-server-data:/data - ports: - - 8108:8108 - restart: "no" + typesense: + image: typesense/typesense:0.25.1 + container_name: typesense-testing + restart: "on-failure" + ports: + - "8108:8108/tcp" + environment: + TYPESENSE_DATA_DIR: /var/tmp + TYPESENSE_API_KEY: testing + TYPESENSE_ENABLE_CORS: "true" + TYPESENSE_PEERING_ADDRESS: "127.0.0.1" + TYPESENSE_PEERING_PORT: "12345" + TYPESENSE_PEERING_SUBNET: "127.0.0.1/24" diff --git a/examples/README.md b/examples/README.md deleted file mode 100644 index 91e87432..00000000 --- a/examples/README.md +++ /dev/null @@ -1,14 +0,0 @@ -### Running the examples - -Start a local typesense server via docker: - -```shell script -composer run-script typesenseServer -``` - -Then: - -```shell script -cd examples -php .php -``` \ No newline at end of file diff --git a/examples/alias_operations.php b/examples/alias_operations.php deleted file mode 100644 index 47f51e81..00000000 --- a/examples/alias_operations.php +++ /dev/null @@ -1,142 +0,0 @@ - 'xyz', - 'nodes' => [ - [ - 'host' => 'localhost', - 'port' => '8108', - 'protocol' => 'http', - ], - ], - 'client' => new HttplugClient(), - ] - ); - echo '
';
-    try {
-        print_r($client->aliases['books']->delete());
-    } catch (Exception $e) {
-        // Don't error out if the collection was not found
-    }
-    try {
-        print_r($client->collections['books_january']->delete());
-    } catch (Exception $e) {
-        // Don't error out if the collection was not found
-    }
-    echo "--------Create Collection-------\n";
-    print_r(
-        $client->collections->create(
-            [
-                'name' => 'books_january',
-                'fields' => [
-                    [
-                        'name' => 'title',
-                        'type' => 'string',
-                    ],
-                    [
-                        'name' => 'authors',
-                        'type' => 'string[]',
-                    ],
-                    [
-                        'name' => 'authors_facet',
-                        'type' => 'string[]',
-                        'facet' => true,
-                    ],
-                    [
-                        'name' => 'publication_year',
-                        'type' => 'int32',
-                    ],
-                    [
-                        'name' => 'publication_year_facet',
-                        'type' => 'string',
-                        'facet' => true,
-                    ],
-                    [
-                        'name' => 'ratings_count',
-                        'type' => 'int32',
-                    ],
-                    [
-                        'name' => 'average_rating',
-                        'type' => 'float',
-                    ],
-                    [
-                        'name' => 'image_url',
-                        'type' => 'string',
-                    ],
-                ],
-                'default_sorting_field' => 'ratings_count',
-            ]
-        )
-    );
-    echo "--------Create Collection-------\n";
-    echo "\n";
-    echo "--------Create Collection Alias-------\n";
-    print_r(
-        $client->aliases->upsert(
-            'books',
-            [
-                'collection_name' => 'books_january',
-            ]
-        )
-    );
-    echo "--------Create Collection Alias-------\n";
-    echo "\n";
-    echo "--------Create Document on Alias-------\n";
-    print_r(
-        $client->collections['books']->documents->create(
-            [
-                'id' => '1',
-                'original_publication_year' => 2008,
-                'authors' => [
-                    'Suzanne Collins',
-                ],
-                'average_rating' => 4.34,
-                'publication_year' => 2008,
-                'publication_year_facet' => '2008',
-                'authors_facet' => [
-                    'Suzanne Collins',
-                ],
-                'title' => 'The Hunger Games',
-                'image_url' => 'https://images.gr-assets.com/books/1447303603m/2767052.jpg',
-                'ratings_count' => 4780653,
-            ]
-        )
-    );
-    echo "--------Create Document on Alias-------\n";
-    echo "\n";
-    echo "--------Search Document on Alias-------\n";
-    print_r(
-        $client->collections['books']->documents->search(
-            [
-                'q' => 'hunger',
-                'query_by' => 'title',
-                'sort_by' => 'ratings_count:desc',
-            ]
-        )
-    );
-    echo "--------Search Document on Alias-------\n";
-    echo "\n";
-    echo "--------Retrieve All Aliases-------\n";
-    print_r($client->aliases->retrieve());
-    echo "--------Retrieve All Aliases-------\n";
-    echo "\n";
-    echo "--------Retrieve All Alias Documents-------\n";
-    print_r($client->aliases['books']->retrieve());
-    echo "--------Retrieve All Alias Documents-------\n";
-    echo "\n";
-    echo "--------Delete Alias-------\n";
-    print_r($client->aliases['books']->delete());
-    echo "--------Delete Alias-------\n";
-    echo "\n";
-} catch (Exception $e) {
-    echo $e->getMessage();
-}
diff --git a/examples/cluster_operations.php b/examples/cluster_operations.php
deleted file mode 100644
index 98893507..00000000
--- a/examples/cluster_operations.php
+++ /dev/null
@@ -1,29 +0,0 @@
- 'xyz',
-            'nodes' => [
-                [
-                    'host' => 'localhost',
-                    'port' => '8108',
-                    'protocol' => 'http',
-                ],
-            ],
-            'client' => new HttplugClient(),
-        ]
-    );
-    echo '
';
-
-    print_r($client->operations->perform('snapshot', ['snapshot_path' => '/tmp/snapshot']));
-} catch (Exception $e) {
-    echo $e->getMessage();
-}
diff --git a/examples/collection_operations.php b/examples/collection_operations.php
deleted file mode 100644
index 7f892c0e..00000000
--- a/examples/collection_operations.php
+++ /dev/null
@@ -1,232 +0,0 @@
- 'xyz',
-            'nodes' => [
-                [
-                    'host' => 'localhost',
-                    'port' => '8108',
-                    'protocol' => 'http',
-                ],
-            ],
-            'client' => new HttplugClient(),
-        ]
-    );
-    echo '
';
-
-    try {
-        print_r($client->collections['books']->delete());
-    } catch (Exception $e) {
-        // Don't error out if the collection was not found
-    }
-
-    echo "--------Create Collection-------\n";
-    print_r(
-        $client->collections->create(
-            [
-                'name' => 'books',
-                'fields' => [
-                    [
-                        'name' => 'title',
-                        'type' => 'string',
-                    ],
-                    [
-                        'name' => 'authors',
-                        'type' => 'string[]',
-                    ],
-                    [
-                        'name' => 'authors_facet',
-                        'type' => 'string[]',
-                        'facet' => true,
-                    ],
-                    [
-                        'name' => 'publication_year',
-                        'type' => 'int32',
-                    ],
-                    [
-                        'name' => 'publication_year_facet',
-                        'type' => 'string',
-                        'facet' => true,
-                    ],
-                    [
-                        'name' => 'ratings_count',
-                        'type' => 'int32',
-                    ],
-                    [
-                        'name' => 'average_rating',
-                        'type' => 'float',
-                    ],
-                    [
-                        'name' => 'image_url',
-                        'type' => 'string',
-                    ],
-                ],
-                'default_sorting_field' => 'ratings_count',
-            ]
-        )
-    );
-    echo "--------Create Collection-------\n";
-    echo "\n";
-    echo "--------Retrieve Collection-------\n";
-    print_r($client->collections['books']->retrieve());
-    echo "--------Retrieve Collection-------\n";
-    echo "\n";
-    echo "--------Retrieve All Collections-------\n";
-    print_r($client->collections->retrieve());
-    echo "--------Retrieve All Collections-------\n";
-    echo "\n";
-    echo "--------Create Document-------\n";
-    print_r(
-        $client->collections['books']->documents->create(
-            [
-                'id' => '1',
-                'original_publication_year' => 2008,
-                'authors' => [
-                    'Suzanne Collins',
-                ],
-                'average_rating' => 4.34,
-                'publication_year' => 2008,
-                'publication_year_facet' => '2008',
-                'authors_facet' => [
-                    'Suzanne Collins',
-                ],
-                'title' => 'The Hunger Games',
-                'image_url' => 'https://images.gr-assets.com/books/1447303603m/2767052.jpg',
-                'ratings_count' => 4780653,
-            ]
-        )
-    );
-    echo "--------Create Document-------\n";
-    echo "\n";
-
-    echo "--------Upsert Document-------\n";
-    print_r(
-        $client->collections['books']->documents->upsert(
-            [
-                'id' => '1',
-                'original_publication_year' => 2008,
-                'authors' => [
-                    'Suzanne Collins',
-                ],
-                'average_rating' => 4.6,
-                'publication_year' => "2008",
-                'publication_year_facet' => '2008',
-                'authors_facet' => [
-                    'Suzanne Collins',
-                ],
-                'title' => 'The Hunger Games',
-                'image_url' => 'https://images.gr-assets.com/books/1447303603m/2767052.jpg',
-                'ratings_count' => 4780653,
-            ],
-            [
-                'dirty_values' => 'coerce_or_reject',
-            ]
-        )
-    );
-    echo "--------Upsert Document-------\n";
-    echo "\n";
-
-    echo "--------Export Documents-------\n";
-    $exportedDocStrs = $client->collections['books']->documents->export(["exclude_fields" => "authors_facet"]);
-    print_r($exportedDocStrs);
-    echo "--------Export Documents-------\n";
-    echo "\n";
-    echo "--------Update Single Document-------\n";
-    print_r($client->collections['books']->documents['1']->update([
-        'average_rating' => 4.5,
-    ]));
-    echo "--------Update Single Document-------\n";
-    echo "\n";
-    echo "--------Fetch Single Document-------\n";
-    print_r($client->collections['books']->documents['1']->retrieve());
-    echo "--------Fetch Single Document-------\n";
-    echo "\n";
-    echo "--------Search Document-------\n";
-    print_r(
-        $client->collections['books']->documents->search(
-            [
-                'q' => 'hunger',
-                'query_by' => 'title',
-                'sort_by' => 'ratings_count:desc',
-            ]
-        )
-    );
-    echo "--------Search Document-------\n";
-    echo "\n";
-    echo "--------Multi search-------\n";
-    print_r(
-        $client->multiSearch->perform(
-            [
-                'searches' => [
-                    [
-                        'q' => 'hunger',
-                        'sort_by' => 'ratings_count:desc',
-                    ],
-                    [
-                        'q' => 'game',
-                        'sort_by' => 'ratings_count:asc',
-                    ]
-                ]
-            ],
-            [
-                'query_by' => 'title',
-                'collection' => 'books'
-            ]
-        )
-    );
-    echo "--------Multi Search-------\n";
-    echo "\n";
-    echo "--------Delete Document-------\n";
-    print_r($client->collections['books']->documents['1']->delete());
-    echo "--------Delete Document-------\n";
-    echo "\n";
-    echo "--------Import Documents-------\n";
-    $docsToImport = [];
-    $exportedDocStrsArray = explode('\n', $exportedDocStrs);
-    foreach ($exportedDocStrsArray as $exportedDocStr) {
-        $docsToImport[] = json_decode($exportedDocStr, true);
-    }
-    $importRes =
-        $client->collections['books']->documents->import($docsToImport);
-    print_r($importRes);
-
-    // Or if you have documents in JSONL format, and want to save the overhead of parsing JSON,
-    // you can also pass in a JSONL string of documents
-    // $client->collections['books']->documents->import($exportedDocStrsArray);
-    echo "--------Import Documents-------\n";
-    echo "\n";
-    echo "--------Upsert Documents-------\n";
-    $upsertRes =
-        $client->collections['books']->documents->import($docsToImport, [
-            'action' => 'upsert'
-        ]);
-    print_r($upsertRes);
-    echo "--------Upsert Documents-------\n";
-    echo "\n";
-    echo "--------Update Documents-------\n";
-    $upsertRes =
-        $client->collections['books']->documents->import($docsToImport, [
-            'action' => 'update'
-        ]);
-    print_r($upsertRes);
-    echo "--------Upsert Documents-------\n";
-    echo "\n";
-    echo "--------Bulk Delete Documents-------\n";
-    print_r($client->collections['books']->documents->delete(['filter_by' => 'publication_year:=2008']));
-    echo "--------Bulk Delete Documents-------\n";
-    echo "\n";
-    echo "--------Delete Collection-------\n";
-    print_r($client->collections['books']->delete());
-    echo "--------Delete Collection-------\n";
-} catch (Exception $e) {
-    echo $e->getMessage();
-}
diff --git a/examples/curation_operations.php b/examples/curation_operations.php
deleted file mode 100644
index 13c3874c..00000000
--- a/examples/curation_operations.php
+++ /dev/null
@@ -1,148 +0,0 @@
- 'xyz',
-            'nodes' => [
-                [
-                    'host' => 'localhost',
-                    'port' => '8108',
-                    'protocol' => 'http',
-                ],
-            ],
-            'client' => new HttplugClient(),
-        ]
-    );
-    echo '
';
-    try {
-        print_r($client->collections['books']->delete());
-    } catch (Exception $e) {
-        // Don't error out if the collection was not found
-    }
-    echo "--------Create Collection-------\n";
-    print_r(
-        $client->collections->create(
-            [
-                'name' => 'books',
-                'fields' => [
-                    [
-                        'name' => 'title',
-                        'type' => 'string',
-                    ],
-                    [
-                        'name' => 'authors',
-                        'type' => 'string[]',
-                    ],
-                    [
-                        'name' => 'authors_facet',
-                        'type' => 'string[]',
-                        'facet' => true,
-                    ],
-                    [
-                        'name' => 'publication_year',
-                        'type' => 'int32',
-                    ],
-                    [
-                        'name' => 'publication_year_facet',
-                        'type' => 'string',
-                        'facet' => true,
-                    ],
-                    [
-                        'name' => 'ratings_count',
-                        'type' => 'int32',
-                    ],
-                    [
-                        'name' => 'average_rating',
-                        'type' => 'float',
-                    ],
-                    [
-                        'name' => 'image_url',
-                        'type' => 'string',
-                    ],
-                ],
-                'default_sorting_field' => 'ratings_count',
-            ]
-        )
-    );
-    echo "--------Create Collection-------\n";
-    echo "\n";
-    echo "--------Create or Update Override-------\n";
-    print_r(
-        $client->collections['books']->overrides->upsert(
-            'hermione-exact',
-            [
-                'rule' => [
-                    'query' => 'hermione',
-                    'match' => 'exact',
-                ],
-                'includes' => [
-                    [
-                        'id' => '1',
-                        'position' => 1,
-                    ],
-                ],
-            ]
-        )
-    );
-    echo "--------Create or Update Override-------\n";
-    echo "\n";
-    echo "--------Get All Overrides-------\n";
-    print_r($client->collections['books']->overrides->retrieve());
-    echo "--------Get All Overrides-------\n";
-    echo "\n";
-    echo "--------Get Single Override-------\n";
-    print_r(
-        $client->collections['books']->overrides['hermione-exact']->retrieve()
-    );
-    echo "--------Get Single Override-------\n";
-    echo "\n";
-    echo "--------Create Document-------\n";
-    print_r(
-        $client->collections['books']->documents->create(
-            [
-                'id' => '1',
-                'original_publication_year' => 2008,
-                'authors' => [
-                    'Suzanne Collins',
-                ],
-                'average_rating' => 4.34,
-                'publication_year' => 2008,
-                'publication_year_facet' => '2008',
-                'authors_facet' => [
-                    'Suzanne Collins',
-                ],
-                'title' => 'The Hunger Games',
-                'image_url' => 'https://images.gr-assets.com/books/1447303603m/2767052.jpg',
-                'ratings_count' => 4780653,
-            ]
-        )
-    );
-    echo "--------Create Document-------\n";
-    echo "\n";
-    echo "--------Search Document-------\n";
-    print_r(
-        $client->collections['books']->documents->search(
-            [
-                'q' => 'hermione',
-                'query_by' => 'title',
-                'sort_by' => 'ratings_count:desc',
-            ]
-        )
-    );
-    echo "--------Search Document-------\n";
-    echo "\n";
-    echo "--------Delete Override-------\n";
-    print_r(
-        $client->collections['books']->getOverrides()['hermione-exact']->delete()
-    );
-    echo "--------Delete Override-------\n";
-    echo "\n";
-} catch (Exception $e) {
-    echo $e->getMessage();
-}
diff --git a/examples/info_operations.php b/examples/info_operations.php
deleted file mode 100644
index ba00a3e8..00000000
--- a/examples/info_operations.php
+++ /dev/null
@@ -1,31 +0,0 @@
- 'xyz',
-            'nodes' => [
-                [
-                    'host' => 'localhost',
-                    'port' => '8108',
-                    'protocol' => 'http',
-                ],
-            ],
-            'client' => new HttplugClient(),
-        ]
-    );
-    echo '
';
-
-    print_r($client->debug->retrieve());
-    print_r($client->metrics->retrieve());
-    print_r($client->health->retrieve());
-} catch (Exception $e) {
-    echo $e->getMessage();
-}
diff --git a/examples/keys_operations.php b/examples/keys_operations.php
deleted file mode 100644
index b4dcc9c8..00000000
--- a/examples/keys_operations.php
+++ /dev/null
@@ -1,160 +0,0 @@
- 'xyz',
-            'nodes' => [
-                [
-                    'host' => 'localhost',
-                    'port' => '8108',
-                    'protocol' => 'http',
-                ],
-            ],
-            'client' => new HttplugClient(),
-        ]
-    );
-    echo '
';
-    try {
-        print_r($client->collections['users']->delete());
-    } catch (Exception $e) {
-        // Don't error out if the collection was not found
-    }
-    echo "--------Create Collection-------\n";
-    print_r(
-        $client->collections->create(
-            [
-                'name' => 'users',
-                'fields' => [
-                    [
-                        'name' => 'company_id',
-                        'type' => 'int32',
-                        'facet' => false
-                    ],
-                    [
-                        'name' => 'user_name',
-                        'type' => 'string',
-                        'facet' => false
-                    ],
-                    [
-                        'name' => 'login_count',
-                        'type' => 'int32',
-                        'facet' => false
-                    ],
-                    [
-                        'name' => 'country',
-                        'type' => 'string',
-                        'facet' => true
-                    ]
-                ],
-                'default_sorting_field' => 'company_id'
-            ]
-        )
-    );
-    echo "--------Create Collection-------\n";
-    echo "\n";
-    echo "--------Create Documents-------\n";
-    print_r(
-        $client->collections['users']->documents->createMany([
-            [
-                'company_id' => 124,
-                'user_name' => 'Hilary Bradford',
-                'login_count' => 10,
-                'country' => 'USA'
-            ],
-            [
-                'company_id' => 124,
-                'user_name' => 'Nile Carty',
-                'login_count' => 100,
-                'country' => 'USA'
-            ],
-            [
-                'company_id' => 126,
-                'user_name' => 'Tahlia Maxwell',
-                'login_count' => 1,
-                'country' => 'France'
-            ],
-            [
-                'company_id' => 126,
-                'user_name' => 'Karl Roy',
-                'login_count' => 2,
-                'country' => 'Germany'
-            ]
-        ])
-    );
-    echo "--------Create Documents-------\n";
-    echo "\n";
-    echo "--------Create a search only API key-------\n";
-    $searchOnlyApiKeyResponse = $client->keys->create([
-        'description' => 'Search-only key.',
-        'actions' => ['documents:search'],
-        'collections' => ['*']
-    ]);
-    print_r($searchOnlyApiKeyResponse);
-    echo "--------Create a search only API key-------\n";
-    echo "\n";
-    echo "--------Get All Keys-------\n";
-    print_r($client->keys->retrieve());
-    echo "--------Get All Keys-------\n";
-    echo "\n";
-    echo "--------Get Single Key-------\n";
-    print_r(
-        $client->keys[$searchOnlyApiKeyResponse['id']]->retrieve()
-    );
-    echo "--------Get Single Key-------\n";
-    echo "\n";
-    echo "--------Generate Scoped API Key-------\n";
-    $scopedAPIKey = $client->keys->generateScopedSearchKey($searchOnlyApiKeyResponse['value'], ['filter_by' => 'company_id:124']);
-    print_r($scopedAPIKey);
-    echo "\n";
-    echo "--------Generate Scoped API Key-------\n";
-    echo "\n";
-    echo "--------Search Documents with scoped Key-------\n";
-    $scopedClient = new Client(
-        [
-            'api_key' => $scopedAPIKey,
-            'nodes' => [
-                [
-                    'host' => 'localhost',
-                    'port' => '8108',
-                    'protocol' => 'http',
-                ],
-            ]
-        ]
-    );
-
-    print_r(
-        $scopedClient->collections['users']->documents->search(
-            [
-                'q' => 'Hilary',
-                'query_by' => 'user_name'
-            ]
-        )
-    );
-    echo "--------Search Documents with scoped Key-------\n";
-    echo "\n";
-    echo "--------Search for document outside of scope for scoped Key-------\n";
-    print_r(
-        $scopedClient->collections['users']->documents->search(
-            [
-                'q' => 'Maxwell',
-                'query_by' => 'user_name'
-            ]
-        )
-    );
-    echo "--------Search for document outside of scope for scoped Key-------\n";
-    echo "\n";
-    echo "--------Delete Key-------\n";
-    print_r(
-        $client->keys[$searchOnlyApiKeyResponse['id']]->delete()
-    );
-    echo "--------Delete Key-------\n";
-    echo "\n";
-} catch (Exception $e) {
-    echo $e->getMessage();
-}
diff --git a/examples/synonym_operations.php b/examples/synonym_operations.php
deleted file mode 100644
index a883e8e5..00000000
--- a/examples/synonym_operations.php
+++ /dev/null
@@ -1,150 +0,0 @@
- 'xyz',
-            'nodes' => [
-                [
-                    'host' => 'localhost',
-                    'port' => '8108',
-                    'protocol' => 'http',
-                ],
-            ],
-            'client' => new HttplugClient(),
-        ]
-    );
-    echo '
';
-    try {
-        print_r($client->collections['books']->delete());
-    } catch (Exception $e) {
-        // Don't error out if the collection was not found
-    }
-    echo "--------Create Collection-------\n";
-    print_r(
-        $client->collections->create(
-            [
-                'name' => 'books',
-                'fields' => [
-                    [
-                        'name' => 'title',
-                        'type' => 'string',
-                    ],
-                    [
-                        'name' => 'authors',
-                        'type' => 'string[]',
-                        'facet' => true
-                    ],
-                    [
-                        'name' => 'publication_year',
-                        'type' => 'int32',
-                        'facet' => true,
-                    ],
-                    [
-                        'name' => 'ratings_count',
-                        'type' => 'int32',
-                    ],
-                    [
-                        'name' => 'average_rating',
-                        'type' => 'float',
-                    ],
-                    [
-                        'name' => 'image_url',
-                        'type' => 'string',
-                    ],
-                ],
-                'default_sorting_field' => 'ratings_count',
-            ]
-        )
-    );
-    echo "--------Create Collection-------\n";
-    echo "\n";
-    echo "--------Upsert Synonym-------\n";
-    print_r(
-        $client->collections['books']->synonyms->upsert(
-            'synonym-set-1',
-            [
-                'synonyms' => ['Hunger', 'Katniss'],
-            ]
-        )
-    );
-    echo "--------Upsert Synonym-------\n";
-    echo "\n";
-    echo "--------Get All Synonyms-------\n";
-    print_r($client->collections['books']->synonyms->retrieve());
-    echo "--------Get All Synonyms-------\n";
-    echo "\n";
-    echo "--------Get Single Synonym-------\n";
-    print_r(
-        $client->collections['books']->synonyms['synonym-set-1']->retrieve()
-    );
-    echo "--------Get Single Synonym-------\n";
-    echo "\n";
-    echo "--------Create Document-------\n";
-    print_r(
-        $client->collections['books']->documents->create(
-            [
-                'id' => '1',
-                'original_publication_year' => 2008,
-                'authors' => [
-                    'Suzanne Collins',
-                ],
-                'average_rating' => 4.34,
-                'publication_year' => 2008,
-                'title' => 'The Hunger Games',
-                'image_url' => 'https://images.gr-assets.com/books/1447303603m/2767052.jpg',
-                'ratings_count' => 4780653,
-            ]
-        )
-    );
-    echo "--------Create Document-------\n";
-    echo "\n";
-    echo "--------Search Document, using a synonym-------\n";
-    print_r(
-        $client->collections['books']->documents->search(
-            [
-                'q' => 'Katniss',
-                'query_by' => 'title'
-            ]
-        )
-    );
-    echo "--------Search Document, using a synonym-------\n";
-    echo "\n";
-    echo "--------Upsert 1-way synonym-------\n";
-    print_r(
-        $client->collections['books']->synonyms->upsert(
-            'synonym-set-1',
-            [
-                'root' => 'Katniss',
-                'synonyms' => ['Hunger', 'Peeta'],
-            ]
-        )
-    );
-    echo "--------Upsert 1-way synonym-------\n";
-    echo "\n";
-    echo "--------Search Document, using a synonym-------\n";
-    // Won't return any results
-    print_r(
-        $client->collections['books']->documents->search(
-            [
-                'q' => 'Peeta',
-                'query_by' => 'title'
-            ]
-        )
-    );
-    echo "--------Search Document, using a synonym-------\n";
-    echo "\n";
-    echo "--------Delete Synonym-------\n";
-    print_r(
-        $client->collections['books']->getSynonyms()['synonym-set-1']->delete()
-    );
-    echo "--------Delete Synonym-------\n";
-    echo "\n";
-} catch (Exception $e) {
-    echo $e->getMessage();
-}
diff --git a/phpcs.xml b/phpcs.xml
deleted file mode 100644
index e7ff5dab..00000000
--- a/phpcs.xml
+++ /dev/null
@@ -1,45 +0,0 @@
-
-
-    The coding standard for PHP_CodeSniffer itself.
-
-    src
-    examples
-
-    */src/Standards/*/Tests/*\.(inc|css|js)$
-    */tests/Core/*/*\.(inc|css|js)$
-
-    
-    
-    
-    
-
-    
-    
-        error
-    
-
-    
-    
-
-    
-    
-        
-            
-            
-        
-    
-
-    
-    
-        
-            
-                
-                
-                
-                
-                
-            
-        
-    
-
\ No newline at end of file
diff --git a/phpstan.neon b/phpstan.neon
new file mode 100644
index 00000000..fb46dc56
--- /dev/null
+++ b/phpstan.neon
@@ -0,0 +1,19 @@
+parameters:
+  level: max
+
+  parallel:
+    maximumNumberOfProcesses: 4
+
+  paths:
+    - src
+    - tests
+
+  ignoreErrors:
+    -
+      message: '#Undefined variable\: \$this#'
+      paths:
+        - tests/*
+    -
+      message: '#Call to an undefined method Pest\\PendingCalls\\TestCall\|Pest\\Support\\HigherOrderTapProxy\:\:.+\(\)\.#'
+      paths:
+        - tests/*
diff --git a/phpunit.xml b/phpunit.xml
new file mode 100644
index 00000000..c8a2b8d9
--- /dev/null
+++ b/phpunit.xml
@@ -0,0 +1,45 @@
+
+
+  
+    
+      src
+    
+  
+  
+    
+      tests
+    
+  
+  
+    
+      
+      
+    
+  
+  
+    
+    
+    
+    
+    
+    
+  
+
diff --git a/src/Alias.php b/src/Alias.php
deleted file mode 100644
index 5dd76189..00000000
--- a/src/Alias.php
+++ /dev/null
@@ -1,65 +0,0 @@
-
- */
-class Alias
-{
-
-    /**
-     * @var string
-     */
-    private string $name;
-
-    /**
-     * @var ApiCall
-     */
-    private ApiCall $apiCall;
-
-    /**
-     * Alias constructor.
-     *
-     * @param string $name
-     * @param ApiCall $apiCall
-     */
-    public function __construct(string $name, ApiCall $apiCall)
-    {
-        $this->name    = $name;
-        $this->apiCall = $apiCall;
-    }
-
-    /**
-     * @return string
-     */
-    public function endPointPath(): string
-    {
-        return sprintf('%s/%s', Aliases::RESOURCE_PATH, $this->name);
-    }
-
-    /**
-     * @return array
-     * @throws TypesenseClientError|HttpClientException
-     */
-    public function retrieve(): array
-    {
-        return $this->apiCall->get($this->endPointPath(), []);
-    }
-
-    /**
-     * @return array
-     * @throws TypesenseClientError|HttpClientException
-     */
-    public function delete(): array
-    {
-        return $this->apiCall->delete($this->endPointPath());
-    }
-}
diff --git a/src/Aliases.php b/src/Aliases.php
deleted file mode 100644
index d1c76cf1..00000000
--- a/src/Aliases.php
+++ /dev/null
@@ -1,124 +0,0 @@
-
- */
-class Aliases implements \ArrayAccess
-{
-
-    public const RESOURCE_PATH = '/aliases';
-
-    /**
-     * @var ApiCall
-     */
-    private ApiCall $apiCall;
-
-    /**
-     * @var array
-     */
-    private array $aliases = [];
-
-    /**
-     * Aliases constructor.
-     *
-     * @param ApiCall $apiCall
-     */
-    public function __construct(ApiCall $apiCall)
-    {
-        $this->apiCall = $apiCall;
-    }
-
-    /**
-     * @param string $aliasName
-     *
-     * @return string
-     */
-    public function endPointPath(string $aliasName): string
-    {
-        return sprintf('%s/%s', static::RESOURCE_PATH, $aliasName);
-    }
-
-    /**
-     * @param string $name
-     * @param array $mapping
-     *
-     * @return array
-     * @throws TypesenseClientError|HttpClientException
-     */
-    public function upsert(string $name, array $mapping): array
-    {
-        return $this->apiCall->put($this->endPointPath($name), $mapping);
-    }
-
-    /**
-     * @return array
-     * @throws TypesenseClientError|HttpClientException
-     */
-    public function retrieve(): array
-    {
-        return $this->apiCall->get(static::RESOURCE_PATH, []);
-    }
-
-    /**
-     * @param $name
-     *
-     * @return mixed
-     */
-    public function __get($name)
-    {
-        if (isset($this->{$name})) {
-            return $this->{$name};
-        }
-
-        if (!isset($this->aliases[$name])) {
-            $this->aliases[$name] = new Alias($name, $this->apiCall);
-        }
-
-        return $this->aliases[$name];
-    }
-
-    /**
-     * @inheritDoc
-     */
-    public function offsetExists($offset): bool
-    {
-        return isset($this->aliases[$offset]);
-    }
-
-    /**
-     * @inheritDoc
-     */
-    public function offsetGet($offset): Alias
-    {
-        if (!isset($this->aliases[$offset])) {
-            $this->aliases[$offset] = new Alias($offset, $this->apiCall);
-        }
-
-        return $this->aliases[$offset];
-    }
-
-    /**
-     * @inheritDoc
-     */
-    public function offsetSet($offset, $value): void
-    {
-        $this->aliases[$offset] = $value;
-    }
-
-    /**
-     * @inheritDoc
-     */
-    public function offsetUnset($offset): void
-    {
-        unset($this->aliases[$offset]);
-    }
-}
diff --git a/src/Analytics.php b/src/Analytics.php
deleted file mode 100644
index 0adf4fd2..00000000
--- a/src/Analytics.php
+++ /dev/null
@@ -1,25 +0,0 @@
-apiCall = $apiCall;
-    }
-
-    public function rules()
-    {
-        if (!isset($this->rules)) {
-            $this->rules = new AnalyticsRules($this->apiCall);
-        }
-        return $this->rules;
-    }
-}
diff --git a/src/AnalyticsRule.php b/src/AnalyticsRule.php
deleted file mode 100644
index bc8755f6..00000000
--- a/src/AnalyticsRule.php
+++ /dev/null
@@ -1,30 +0,0 @@
-ruleName = $ruleName;
-        $this->apiCall = $apiCall;
-    }
-
-    public function retrieve()
-    {
-        return $this->apiCall->get($this->endpointPath(), []);
-    }
-
-    public function delete()
-    {
-        return $this->apiCall->delete($this->endpointPath());
-    }
-
-    private function endpointPath()
-    {
-        return AnalyticsRules::RESOURCE_PATH . '/' . $this->ruleName;
-    }
-}
diff --git a/src/AnalyticsRules.php b/src/AnalyticsRules.php
deleted file mode 100644
index 412aea0d..00000000
--- a/src/AnalyticsRules.php
+++ /dev/null
@@ -1,39 +0,0 @@
-apiCall = $apiCall;
-    }
-
-    public function __get($ruleName)
-    {
-        if (!isset($this->analyticsRules[$ruleName])) {
-            $this->analyticsRules[$ruleName] = new AnalyticsRule($ruleName, $this->apiCall);
-        }
-        return $this->analyticsRules[$ruleName];
-    }
-
-    public function upsert($ruleName, $params)
-    {
-        return $this->apiCall->put($this->endpoint_path($ruleName), $params);
-    }
-
-    public function retrieve()
-    {
-        return $this->apiCall->get($this->endpoint_path(), []);
-    }
-
-    private function endpoint_path($operation = null)
-    {
-        return self::RESOURCE_PATH . ($operation === null ? '' : "/$operation");
-    }
-}
diff --git a/src/ApiCall.php b/src/ApiCall.php
deleted file mode 100644
index 435ff107..00000000
--- a/src/ApiCall.php
+++ /dev/null
@@ -1,369 +0,0 @@
-
- */
-class ApiCall
-{
-
-    private const API_KEY_HEADER_NAME = 'X-TYPESENSE-API-KEY';
-
-    /**
-     * @var ClientInterface
-     */
-    private ClientInterface $client;
-
-    /**
-     * @var Configuration
-     */
-    private Configuration $config;
-
-    /**
-     * @var array|Node[]
-     */
-    private static array $nodes;
-
-    /**
-     * @var Node|null
-     */
-    private static ?Node $nearestNode;
-
-    /**
-     * @var int
-     */
-    private int $nodeIndex;
-
-    /**
-     * @var LoggerInterface
-     */
-    public LoggerInterface $logger;
-
-    /**
-     * ApiCall constructor.
-     *
-     * @param Configuration $config
-     */
-    public function __construct(Configuration $config)
-    {
-        $this->config        = $config;
-        $this->logger        = $config->getLogger();
-        $this->client        = $config->getClient();
-        static::$nodes       = $this->config->getNodes();
-        static::$nearestNode = $this->config->getNearestNode();
-        $this->nodeIndex     = 0;
-        $this->initializeNodes();
-    }
-
-    /**
-     *  Initialize Nodes
-     */
-    private function initializeNodes(): void
-    {
-        if (static::$nearestNode !== null) {
-            $this->setNodeHealthCheck(static::$nearestNode, true);
-        }
-
-        foreach (static::$nodes as &$node) {
-            $this->setNodeHealthCheck($node, true);
-        }
-    }
-
-    /**
-     * @param string $endPoint
-     * @param array $params
-     * @param bool $asJson
-     *
-     * @return string|array
-     * @throws TypesenseClientError
-     * @throws Exception|HttpClientException
-     */
-    public function get(string $endPoint, array $params, bool $asJson = true)
-    {
-        return $this->makeRequest('get', $endPoint, $asJson, [
-            'query' => $params ?? [],
-        ]);
-    }
-
-    /**
-     * @param string $endPoint
-     * @param mixed $body
-     *
-     * @param bool $asJson
-     * @param array $queryParameters
-     *
-     * @return array|string
-     * @throws TypesenseClientError
-     * @throws HttpClientException
-     */
-    public function post(string $endPoint, $body, bool $asJson = true, array $queryParameters = [])
-    {
-        return $this->makeRequest('post', $endPoint, $asJson, [
-            'data' => $body ?? [],
-            'query' => $queryParameters ?? []
-        ]);
-    }
-
-    /**
-     * @param string $endPoint
-     * @param array $body
-     *
-     * @param bool $asJson
-     * @param array $queryParameters
-     *
-     * @return array
-     * @throws TypesenseClientError|HttpClientException
-     */
-    public function put(string $endPoint, array $body, bool $asJson = true, array $queryParameters = []): array
-    {
-        return $this->makeRequest('put', $endPoint, $asJson, [
-            'data' => $body ?? [],
-            'query' => $queryParameters ?? []
-        ]);
-    }
-
-    /**
-     * @param string $endPoint
-     * @param array $body
-     *
-     * @param bool $asJson
-     * @param array $queryParameters
-     *
-     * @return array
-     * @throws TypesenseClientError|HttpClientException
-     */
-    public function patch(string $endPoint, array $body, bool $asJson = true, array $queryParameters = []): array
-    {
-        return $this->makeRequest('patch', $endPoint, $asJson, [
-            'data' => $body ?? [],
-            'query' => $queryParameters ?? []
-        ]);
-    }
-
-    /**
-     * @param string $endPoint
-     *
-     * @param bool $asJson
-     * @param array $queryParameters
-     *
-     * @return array
-     * @throws TypesenseClientError|HttpClientException
-     */
-    public function delete(string $endPoint, bool $asJson = true, array $queryParameters = []): array
-    {
-        return $this->makeRequest('delete', $endPoint, $asJson, [
-            'query' => $queryParameters ?? []
-        ]);
-    }
-
-    /**
-     * Makes the actual http request, along with retries
-     *
-     * @param string $method
-     * @param string $endPoint
-     * @param bool $asJson
-     * @param array $options
-     *
-     * @return string|array
-     * @throws TypesenseClientError|HttpClientException
-     * @throws Exception
-     */
-    private function makeRequest(string $method, string $endPoint, bool $asJson, array $options)
-    {
-        $numRetries    = 0;
-        $lastException = null;
-        while ($numRetries < $this->config->getNumRetries() + 1) {
-            $numRetries++;
-            $node = $this->getNode();
-
-            try {
-                $url   = $node->url() . $endPoint;
-                $reqOp = $this->getRequestOptions();
-                if (isset($options['data'])) {
-                    if (is_string($options['data']) || $options['data'] instanceof StreamInterface) {
-                        $reqOp['body'] = $options['data'];
-                    } else {
-                        $reqOp['body'] = \json_encode($options['data']);
-                    }
-                }
-
-                if (isset($options['query'])) {
-                    foreach ($options['query'] as $key => $value) :
-                        if (is_bool($value)) {
-                            $options['query'][$key] = ($value) ? 'true' : 'false';
-                        }
-                    endforeach;
-                    $reqOp['query'] = http_build_query($options['query']);
-                }
-
-                $response = $this->client->send(
-                    \strtoupper($method),
-                    $url . '?' . ($reqOp['query'] ?? ''),
-                    $reqOp['headers'] ?? [],
-                    $reqOp['body'] ?? null
-                );
-
-                $statusCode = $response->getStatusCode();
-                if (0 < $statusCode && $statusCode < 500) {
-                    $this->setNodeHealthCheck($node, true);
-                }
-
-                if (!(200 <= $statusCode && $statusCode < 300)) {
-                    $errorMessage = json_decode($response->getBody()
-                            ->getContents(), true, 512, JSON_THROW_ON_ERROR)['message'] ?? 'API error.';
-                    throw $this->getException($statusCode)
-                        ->setMessage($errorMessage);
-                }
-
-                return $asJson ? json_decode($response->getBody()
-                    ->getContents(), true, 512, JSON_THROW_ON_ERROR) : $response->getBody()
-                    ->getContents();
-            } catch (HttpException $exception) {
-                if (
-                    $exception->getResponse()
-                        ->getStatusCode() === 408
-                ) {
-                    continue;
-                }
-                $this->setNodeHealthCheck($node, false);
-                throw $this->getException($exception->getResponse()
-                    ->getStatusCode())
-                    ->setMessage($exception->getMessage());
-            } catch (TypesenseClientError | HttpClientException $exception) {
-                $this->setNodeHealthCheck($node, false);
-                throw $exception;
-            } catch (Exception $exception) {
-                $this->setNodeHealthCheck($node, false);
-                $lastException = $exception;
-                sleep($this->config->getRetryIntervalSeconds());
-            }
-        }
-
-        if ($lastException) {
-            throw $lastException;
-        }
-    }
-
-    /**
-     * @return array
-     */
-    private function getRequestOptions(): array
-    {
-        return [
-            'headers' => [
-                static::API_KEY_HEADER_NAME => $this->config->getApiKey(),
-            ]
-        ];
-    }
-
-    /**
-     * @param Node $node
-     *
-     * @return bool
-     */
-    private function nodeDueForHealthCheck(Node $node): bool
-    {
-        $currentTimestamp = time();
-        return ($currentTimestamp - $node->getLastAccessTs()) > $this->config->getHealthCheckIntervalSeconds();
-    }
-
-    /**
-     * @param 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 Node
-     */
-    public function getNode(): Lib\Node
-    {
-        if (static::$nearestNode !== null) {
-            if (static::$nearestNode->isHealthy() || $this->nodeDueForHealthCheck(static::$nearestNode)) {
-                return static::$nearestNode;
-            }
-        }
-        $i = 0;
-        while ($i < count(static::$nodes)) {
-            $i++;
-            $node            = static::$nodes[$this->nodeIndex];
-            $this->nodeIndex = ($this->nodeIndex + 1) % count(static::$nodes);
-            if ($node->isHealthy() || $this->nodeDueForHealthCheck($node)) {
-                return $node;
-            }
-        }
-
-        /**
-         * 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.
-         */
-        return static::$nodes[$this->nodeIndex];
-    }
-
-    /**
-     * @param int $httpCode
-     *
-     * @return TypesenseClientError
-     */
-    public function getException(int $httpCode): TypesenseClientError
-    {
-        switch ($httpCode) {
-            case 0:
-                return new HTTPStatus0Error();
-            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 LoggerInterface
-     */
-    public function getLogger()
-    {
-        return $this->logger;
-    }
-}
diff --git a/src/Client.php b/src/Client.php
deleted file mode 100644
index d64be61f..00000000
--- a/src/Client.php
+++ /dev/null
@@ -1,180 +0,0 @@
-
- */
-class Client
-{
-    /**
-     * @var Configuration
-     */
-    private Configuration $config;
-
-    /**
-     * @var Collections
-     */
-    public Collections $collections;
-
-    /**
-     * @var Aliases
-     */
-    public Aliases $aliases;
-
-    /**
-     * @var Keys
-     */
-    public Keys $keys;
-
-    /**
-     * @var Debug
-     */
-    public Debug $debug;
-
-    /**
-     * @var Metrics
-     */
-    public Metrics $metrics;
-
-    /**
-     * @var Health
-     */
-    public Health $health;
-
-    /**
-     * @var Operations
-     */
-    public Operations $operations;
-
-    /**
-     * @var MultiSearch
-     */
-    public MultiSearch $multiSearch;
-
-    /**
-     * @var Presets
-     */
-    public Presets $presets;
-
-    /**
-     * @var Analytics
-     */
-    public Analytics $analytics;
-
-    /**
-     * @var ApiCall
-     */
-    private ApiCall $apiCall;
-
-    /**
-     * Client constructor.
-     *
-     * @param array $config
-     *
-     * @throws ConfigError
-     */
-    public function __construct(array $config)
-    {
-        $this->config  = new Configuration($config);
-        $this->apiCall = new ApiCall($this->config);
-
-        $this->collections = new Collections($this->apiCall);
-        $this->aliases     = new Aliases($this->apiCall);
-        $this->keys        = new Keys($this->apiCall);
-        $this->debug       = new Debug($this->apiCall);
-        $this->metrics     = new Metrics($this->apiCall);
-        $this->health      = new Health($this->apiCall);
-        $this->operations  = new Operations($this->apiCall);
-        $this->multiSearch = new MultiSearch($this->apiCall);
-        $this->presets     = new Presets($this->apiCall);
-        $this->analytics   = new Analytics($this->apiCall);
-    }
-
-    /**
-     * @return Collections
-     */
-    public function getCollections(): Collections
-    {
-        return $this->collections;
-    }
-
-    /**
-     * @return Aliases
-     */
-    public function getAliases(): Aliases
-    {
-        return $this->aliases;
-    }
-
-    /**
-     * @return Keys
-     */
-    public function getKeys(): Keys
-    {
-        return $this->keys;
-    }
-
-    /**
-     * @return Debug
-     */
-    public function getDebug(): Debug
-    {
-        return $this->debug;
-    }
-
-    /**
-     * @return Metrics
-     */
-    public function getMetrics(): Metrics
-    {
-        return $this->metrics;
-    }
-
-    /**
-     * @return Health
-     */
-    public function getHealth(): Health
-    {
-        return $this->health;
-    }
-
-    /**
-     * @return Operations
-     */
-    public function getOperations(): Operations
-    {
-        return $this->operations;
-    }
-
-    /**
-     * @return MultiSearch
-     */
-    public function getMultiSearch(): MultiSearch
-    {
-        return $this->multiSearch;
-    }
-
-    /**
-     * @return Presets
-     */
-    public function getPresets(): Presets
-    {
-        return $this->presets;
-    }
-
-    /**
-     * @return Analytics
-     */
-    public function getAnalytics(): Analytics
-    {
-        return $this->analytics;
-    }
-}
diff --git a/src/Collection.php b/src/Collection.php
deleted file mode 100644
index 91748e32..00000000
--- a/src/Collection.php
+++ /dev/null
@@ -1,118 +0,0 @@
-
- */
-class Collection
-{
-
-    /**
-     * @var string
-     */
-    private string $name;
-
-    /**
-     * @var ApiCall
-     */
-    private ApiCall $apiCall;
-
-    /**
-     * @var Documents
-     */
-    public Documents $documents;
-
-    /**
-     * @var Overrides
-     */
-    public Overrides $overrides;
-
-    /**
-     * @var Synonyms
-     */
-    public Synonyms $synonyms;
-
-    /**
-     * Collection constructor.
-     *
-     * @param string $name
-     * @param ApiCall $apiCall
-     */
-    public function __construct(string $name, ApiCall $apiCall)
-    {
-        $this->name      = $name;
-        $this->apiCall   = $apiCall;
-        $this->documents = new Documents($name, $this->apiCall);
-        $this->overrides = new Overrides($name, $this->apiCall);
-        $this->synonyms  = new Synonyms($name, $this->apiCall);
-    }
-
-    /**
-     * @return string
-     */
-    public function endPointPath(): string
-    {
-        return sprintf('%s/%s', Collections::RESOURCE_PATH, $this->name);
-    }
-
-    /**
-     * @return Documents
-     */
-    public function getDocuments(): Documents
-    {
-        return $this->documents;
-    }
-
-    /**
-     * @return Overrides
-     */
-    public function getOverrides(): Overrides
-    {
-        return $this->overrides;
-    }
-
-    /**
-     * @return Synonyms
-     */
-    public function getSynonyms(): Synonyms
-    {
-        return $this->synonyms;
-    }
-
-    /**
-     * @return array
-     * @throws TypesenseClientError|HttpClientException
-     */
-    public function retrieve(): array
-    {
-        return $this->apiCall->get($this->endPointPath(), []);
-    }
-
-    /**
-     * @param array $schema
-     *
-     * @return array
-     * @throws TypesenseClientError|HttpClientException
-     */
-    public function update(array $schema): array
-    {
-        return $this->apiCall->patch($this->endPointPath(), $schema);
-    }
-
-    /**
-     * @return array
-     * @throws TypesenseClientError|HttpClientException
-     */
-    public function delete(): array
-    {
-        return $this->apiCall->delete($this->endPointPath());
-    }
-}
diff --git a/src/Collections.php b/src/Collections.php
deleted file mode 100644
index 9385aa38..00000000
--- a/src/Collections.php
+++ /dev/null
@@ -1,112 +0,0 @@
-
- */
-class Collections implements \ArrayAccess
-{
-
-    public const RESOURCE_PATH = '/collections';
-
-    /**
-     * @var ApiCall
-     */
-    private ApiCall $apiCall;
-
-    /**
-     * @var array
-     */
-    private array $collections = [];
-
-    /**
-     * Collections constructor.
-     *
-     * @param ApiCall $apiCall
-     */
-    public function __construct(ApiCall $apiCall)
-    {
-        $this->apiCall = $apiCall;
-    }
-
-    /**
-     * @param $collectionName
-     *
-     * @return mixed
-     */
-    public function __get($collectionName)
-    {
-        if (isset($this->{$collectionName})) {
-            return $this->{$collectionName};
-        }
-        if (!isset($this->collections[$collectionName])) {
-            $this->collections[$collectionName] = new Collection($collectionName, $this->apiCall);
-        }
-
-        return $this->collections[$collectionName];
-    }
-
-    /**
-     * @param array $schema
-     *
-     * @return array
-     * @throws TypesenseClientError|HttpClientException
-     */
-    public function create(array $schema): array
-    {
-        return $this->apiCall->post(static::RESOURCE_PATH, $schema);
-    }
-
-    /**
-     * @return array
-     * @throws TypesenseClientError|HttpClientException
-     */
-    public function retrieve(): array
-    {
-        return $this->apiCall->get(static::RESOURCE_PATH, []);
-    }
-
-    /**
-     * @inheritDoc
-     */
-    public function offsetExists($offset): bool
-    {
-        return isset($this->collections[$offset]);
-    }
-
-    /**
-     * @inheritDoc
-     */
-    public function offsetGet($offset): Collection
-    {
-        if (!isset($this->collections[$offset])) {
-            $this->collections[$offset] = new Collection($offset, $this->apiCall);
-        }
-
-        return $this->collections[$offset];
-    }
-
-    /**
-     * @inheritDoc
-     */
-    public function offsetSet($offset, $value): void
-    {
-        $this->collections[$offset] = $value;
-    }
-
-    /**
-     * @inheritDoc
-     */
-    public function offsetUnset($offset): void
-    {
-        unset($this->collections[$offset]);
-    }
-}
diff --git a/src/Debug.php b/src/Debug.php
deleted file mode 100644
index c6987387..00000000
--- a/src/Debug.php
+++ /dev/null
@@ -1,41 +0,0 @@
-apiCall = $apiCall;
-    }
-
-    /**
-     * @return array
-     * @throws TypesenseClientError|HttpClientException
-     */
-    public function retrieve(): array
-    {
-        return $this->apiCall->get(Debug::RESOURCE_PATH, []);
-    }
-}
diff --git a/src/Document.php b/src/Document.php
deleted file mode 100644
index 617bf9ee..00000000
--- a/src/Document.php
+++ /dev/null
@@ -1,90 +0,0 @@
-
- */
-class Document
-{
-
-    /**
-     * @var string
-     */
-    private string $collectionName;
-
-    /**
-     * @var string
-     */
-    private string $documentId;
-
-    /**
-     * @var ApiCall
-     */
-    private ApiCall $apiCall;
-
-    /**
-     * Document constructor.
-     *
-     * @param string $collectionName
-     * @param string $documentId
-     * @param ApiCall $apiCall
-     */
-    public function __construct(string $collectionName, string $documentId, ApiCall $apiCall)
-    {
-        $this->collectionName = $collectionName;
-        $this->documentId     = $documentId;
-        $this->apiCall        = $apiCall;
-    }
-
-    /**
-     * @return string
-     */
-    private function endpointPath(): string
-    {
-        return sprintf(
-            '%s/%s/%s/%s',
-            Collections::RESOURCE_PATH,
-            $this->collectionName,
-            Documents::RESOURCE_PATH,
-            $this->documentId
-        );
-    }
-
-    /**
-     * @return array
-     * @throws TypesenseClientError|HttpClientException
-     */
-    public function retrieve(): array
-    {
-        return $this->apiCall->get($this->endpointPath(), []);
-    }
-
-    /**
-     * @param array $partialDocument
-     * @param array $options
-     *
-     * @return array
-     * @throws TypesenseClientError|HttpClientException
-     */
-    public function update(array $partialDocument, array $options = []): array
-    {
-        return $this->apiCall->patch($this->endpointPath(), $partialDocument, true, $options);
-    }
-
-    /**
-     * @return array
-     * @throws TypesenseClientError|HttpClientException
-     */
-    public function delete(): array
-    {
-        return $this->apiCall->delete($this->endpointPath());
-    }
-}
diff --git a/src/Documents.php b/src/Documents.php
deleted file mode 100644
index 39c3bc6e..00000000
--- a/src/Documents.php
+++ /dev/null
@@ -1,234 +0,0 @@
-
- */
-class Documents implements \ArrayAccess
-{
-
-    public const RESOURCE_PATH = 'documents';
-
-    /**
-     * @var string
-     */
-    private string $collectionName;
-
-    /**
-     * @var ApiCall
-     */
-    private ApiCall $apiCall;
-
-    /**
-     * @var array
-     */
-    private array $documents = [];
-
-    /**
-     * Documents constructor.
-     *
-     * @param string $collectionName
-     * @param ApiCall $apiCall
-     */
-    public function __construct(string $collectionName, ApiCall $apiCall)
-    {
-        $this->collectionName = $collectionName;
-        $this->apiCall        = $apiCall;
-    }
-
-    /**
-     * @param string $action
-     *
-     * @return string
-     */
-    private function endPointPath(string $action = ''): string
-    {
-        return sprintf(
-            '%s/%s/%s/%s',
-            Collections::RESOURCE_PATH,
-            $this->collectionName,
-            static::RESOURCE_PATH,
-            $action
-        );
-    }
-
-    /**
-     * @param array $document
-     * @param array $options
-     *
-     * @return array
-     * @throws TypesenseClientError|HttpClientException
-     */
-    public function create(array $document, array $options = []): array
-    {
-        return $this->apiCall->post($this->endPointPath(''), $document, true, $options);
-    }
-
-    /**
-     * @param array $document
-     * @param array $options
-     *
-     * @return array
-     * @throws TypesenseClientError|HttpClientException
-     */
-    public function upsert(array $document, array $options = []): array
-    {
-        return $this->apiCall->post(
-            $this->endPointPath(''),
-            $document,
-            true,
-            array_merge($options, ['action' => 'upsert'])
-        );
-    }
-
-    /**
-     * @param array $document
-     * @param array $options
-     *
-     * @return array
-     * @throws TypesenseClientError|HttpClientException
-     */
-    public function update(array $document, array $options = []): array
-    {
-        return $this->apiCall->post(
-            $this->endPointPath(''),
-            $document,
-            true,
-            array_merge($options, ['action' => 'update'])
-        );
-    }
-
-    /**
-     * @param array $documents
-     * @param array $options
-     *
-     * @return array
-     * @throws TypesenseClientError|HttpClientException|\JsonException
-     */
-    public function createMany(array $documents, array $options = []): array
-    {
-        $this->apiCall->getLogger()->warning(
-            "createMany is deprecated and will be removed in a future version. " .
-            "Use import instead, which now takes both an array of documents or a JSONL string of documents"
-        );
-        return $this->import($documents, $options);
-    }
-
-    /**
-     * @param string|array $documents
-     * @param array $options
-     *
-     * @return string|array
-     * @throws TypesenseClientError
-     * @throws \JsonException|HttpClientException
-     */
-    public function import($documents, array $options = [])
-    {
-        if (is_array($documents)) {
-            $documentsInJSONLFormat = implode(
-                "\n",
-                array_map(
-                    static fn(array $document) => json_encode($document, JSON_THROW_ON_ERROR),
-                    $documents
-                )
-            );
-        } else {
-            $documentsInJSONLFormat = $documents;
-        }
-        $resultsInJSONLFormat = $this->apiCall->post(
-            $this->endPointPath('import'),
-            $documentsInJSONLFormat,
-            false,
-            $options
-        );
-
-        if (is_array($documents)) {
-            return array_map(static function ($item) {
-                return json_decode($item, true, 512, JSON_THROW_ON_ERROR);
-            }, explode("\n", $resultsInJSONLFormat));
-        } else {
-            return $resultsInJSONLFormat;
-        }
-    }
-
-    /**
-     * @param array $queryParams
-     *
-     * @return string
-     * @throws TypesenseClientError|HttpClientException
-     */
-    public function export(array $queryParams = []): string
-    {
-        return $this->apiCall->get($this->endPointPath('export'), $queryParams, false);
-    }
-
-    /**
-     * @param array $queryParams
-     *
-     * @return array
-     * @throws TypesenseClientError|HttpClientException
-     */
-    public function delete(array $queryParams = []): array
-    {
-        return $this->apiCall->delete($this->endPointPath(), true, $queryParams);
-    }
-
-    /**
-     * @param array $searchParams
-     *
-     * @return array
-     * @throws TypesenseClientError|HttpClientException
-     */
-    public function search(array $searchParams): array
-    {
-        return $this->apiCall->get($this->endPointPath('search'), $searchParams);
-    }
-
-    /**
-     * @param mixed $documentId
-     *
-     * @return bool
-     */
-    public function offsetExists($documentId): bool
-    {
-        return isset($this->documents[$documentId]);
-    }
-
-    /**
-     * @inheritDoc
-     */
-    public function offsetGet($documentId): Document
-    {
-        if (!isset($this->documents[$documentId])) {
-            $this->documents[$documentId] = new Document($this->collectionName, $documentId, $this->apiCall);
-        }
-
-        return $this->documents[$documentId];
-    }
-
-    /**
-     * @inheritDoc
-     */
-    public function offsetUnset($documentId): void
-    {
-        if (isset($this->documents[$documentId])) {
-            unset($this->documents[$documentId]);
-        }
-    }
-
-    /**
-     * @inheritDoc
-     */
-    public function offsetSet($offset, $value): void
-    {
-        $this->documents[$offset] = $value;
-    }
-}
diff --git a/src/Exceptions/Client/ClientErrorException.php b/src/Exceptions/Client/ClientErrorException.php
new file mode 100644
index 00000000..37a9e92e
--- /dev/null
+++ b/src/Exceptions/Client/ClientErrorException.php
@@ -0,0 +1,12 @@
+
- */
-class ConfigError extends TypesenseClientError
-{
-
-}
diff --git a/src/Exceptions/HTTPStatus0Error.php b/src/Exceptions/HTTPStatus0Error.php
deleted file mode 100644
index e2f2752c..00000000
--- a/src/Exceptions/HTTPStatus0Error.php
+++ /dev/null
@@ -1,14 +0,0 @@
-
- */
-class HTTPStatus0Error extends TypesenseClientError
-{
-
-}
diff --git a/src/Exceptions/MalformedResponsePayloadException.php b/src/Exceptions/MalformedResponsePayloadException.php
new file mode 100644
index 00000000..4465b787
--- /dev/null
+++ b/src/Exceptions/MalformedResponsePayloadException.php
@@ -0,0 +1,17 @@
+
- */
-class ObjectAlreadyExists extends TypesenseClientError
-{
-
-}
diff --git a/src/Exceptions/ObjectNotFound.php b/src/Exceptions/ObjectNotFound.php
deleted file mode 100644
index 7cae4b82..00000000
--- a/src/Exceptions/ObjectNotFound.php
+++ /dev/null
@@ -1,15 +0,0 @@
-
- */
-class ObjectNotFound extends TypesenseClientError
-{
-
-}
diff --git a/src/Exceptions/ObjectUnprocessable.php b/src/Exceptions/ObjectUnprocessable.php
deleted file mode 100644
index 89a2f922..00000000
--- a/src/Exceptions/ObjectUnprocessable.php
+++ /dev/null
@@ -1,15 +0,0 @@
-
- */
-class ObjectUnprocessable extends TypesenseClientError
-{
-
-}
diff --git a/src/Exceptions/RequestMalformed.php b/src/Exceptions/RequestMalformed.php
deleted file mode 100644
index 2a7c42b7..00000000
--- a/src/Exceptions/RequestMalformed.php
+++ /dev/null
@@ -1,15 +0,0 @@
-
- */
-class RequestMalformed extends TypesenseClientError
-{
-
-}
diff --git a/src/Exceptions/RequestUnauthorized.php b/src/Exceptions/RequestUnauthorized.php
deleted file mode 100644
index 47ec3533..00000000
--- a/src/Exceptions/RequestUnauthorized.php
+++ /dev/null
@@ -1,15 +0,0 @@
-
- */
-class RequestUnauthorized extends TypesenseClientError
-{
-
-}
diff --git a/src/Exceptions/Server/ServerErrorException.php b/src/Exceptions/Server/ServerErrorException.php
new file mode 100644
index 00000000..bc3b0ccd
--- /dev/null
+++ b/src/Exceptions/Server/ServerErrorException.php
@@ -0,0 +1,12 @@
+
- */
-class ServerError extends TypesenseClientError
-{
-
-}
diff --git a/src/Exceptions/ServiceUnavailable.php b/src/Exceptions/ServiceUnavailable.php
deleted file mode 100644
index 1b32bcb1..00000000
--- a/src/Exceptions/ServiceUnavailable.php
+++ /dev/null
@@ -1,15 +0,0 @@
-
- */
-class ServiceUnavailable extends TypesenseClientError
-{
-
-}
diff --git a/src/Exceptions/Timeout.php b/src/Exceptions/Timeout.php
deleted file mode 100644
index 359eb338..00000000
--- a/src/Exceptions/Timeout.php
+++ /dev/null
@@ -1,15 +0,0 @@
-
- */
-class Timeout extends TypesenseClientError
-{
-
-}
diff --git a/src/Exceptions/TypesenseClientError.php b/src/Exceptions/TypesenseClientError.php
deleted file mode 100644
index 2f20c2a8..00000000
--- a/src/Exceptions/TypesenseClientError.php
+++ /dev/null
@@ -1,22 +0,0 @@
-
- */
-class TypesenseClientError extends Exception
-{
-
-    public function setMessage(string $message): TypesenseClientError
-    {
-        $this->message = $message;
-        return $this;
-    }
-}
diff --git a/src/Exceptions/TypesenseException.php b/src/Exceptions/TypesenseException.php
new file mode 100644
index 00000000..13959fcd
--- /dev/null
+++ b/src/Exceptions/TypesenseException.php
@@ -0,0 +1,12 @@
+apiCall = $apiCall;
-    }
-
-    /**
-     * @return array
-     * @throws TypesenseClientError|HttpClientException
-     */
-    public function retrieve(): array
-    {
-        return $this->apiCall->get(Health::RESOURCE_PATH, []);
-    }
-}
diff --git a/src/Http.php b/src/Http.php
new file mode 100644
index 00000000..a90530c0
--- /dev/null
+++ b/src/Http.php
@@ -0,0 +1,56 @@
+client = new Psr18Client($config['http'] ?? null);
+    }
+
+    /**
+     * @param  'GET'|'HEAD'|'POST'|'PATCH'|'PUT'|'DELETE'  $method
+     */
+    public function request(string $method, string $path, string $body = ''): ResponseInterface
+    {
+        $request = $this->client
+            ->createRequest($method, $this->uri($path))
+            ->withHeader('Content-Type', 'application/json')
+            ->withHeader('X-TYPESENSE-API-KEY', $this->config['apiKey']);
+
+        if (! empty($body)) {
+            $request = $request->withBody(
+                $this->client->createStream($body),
+            );
+        }
+
+        return $this->client->sendRequest($request);
+    }
+
+    /**
+     * Form a complete request URL.
+     */
+    public function uri(string $path): string
+    {
+        return sprintf(
+            '%s/%s',
+            rtrim($this->config['url'], '/'),
+            ltrim($path, '/'),
+        );
+    }
+}
diff --git a/src/Key.php b/src/Key.php
deleted file mode 100644
index af7a9b1f..00000000
--- a/src/Key.php
+++ /dev/null
@@ -1,65 +0,0 @@
-
- */
-class Key
-{
-
-    /**
-     * @var ApiCall
-     */
-    private ApiCall $apiCall;
-
-    /**
-     * @var string
-     */
-    private string $keyId;
-
-    /**
-     * Key constructor.
-     *
-     * @param string $keyId
-     * @param ApiCall $apiCall
-     */
-    public function __construct(string $keyId, ApiCall $apiCall)
-    {
-        $this->keyId   = $keyId;
-        $this->apiCall = $apiCall;
-    }
-
-    /**
-     * @return string
-     */
-    private function endpointPath(): string
-    {
-        return sprintf('%s/%s', Keys::RESOURCE_PATH, $this->keyId);
-    }
-
-    /**
-     * @return array
-     * @throws TypesenseClientError|HttpClientException
-     */
-    public function retrieve(): array
-    {
-        return $this->apiCall->get($this->endpointPath(), []);
-    }
-
-    /**
-     * @return array
-     * @throws TypesenseClientError|HttpClientException
-     */
-    public function delete(): array
-    {
-        return $this->apiCall->delete($this->endpointPath());
-    }
-}
diff --git a/src/Keys.php b/src/Keys.php
deleted file mode 100644
index 740d9098..00000000
--- a/src/Keys.php
+++ /dev/null
@@ -1,129 +0,0 @@
-
- */
-class Keys implements \ArrayAccess
-{
-
-    public const RESOURCE_PATH = '/keys';
-
-    /**
-     * @var ApiCall
-     */
-    private ApiCall $apiCall;
-
-    /**
-     * @var array
-     */
-    private array $keys = [];
-
-    /**
-     * Keys constructor.
-     *
-     * @param ApiCall $apiCall
-     */
-    public function __construct(ApiCall $apiCall)
-    {
-        $this->apiCall = $apiCall;
-    }
-
-    /**
-     * @param array $schema
-     *
-     * @return array
-     * @throws TypesenseClientError|HttpClientException
-     */
-    public function create(array $schema): array
-    {
-        return $this->apiCall->post(static::RESOURCE_PATH, $schema);
-    }
-
-    /**
-     * @param string $searchKey
-     * @param array $parameters
-     *
-     * @return string
-     * @throws \JsonException
-     */
-    public function generateScopedSearchKey(
-        string $searchKey,
-        array $parameters
-    ): string {
-        $paramStr = json_encode($parameters, JSON_THROW_ON_ERROR);
-        $digest = base64_encode(
-            hash_hmac(
-                'sha256',
-                mb_convert_encoding($paramStr, 'UTF-8', 'ISO-8859-1'),
-                mb_convert_encoding($searchKey, 'UTF-8', 'ISO-8859-1'),
-                true)
-        );
-        $keyPrefix = substr($searchKey, 0, 4);
-        $rawScopedKey = sprintf(
-            '%s%s%s',
-            mb_convert_encoding($digest, 'ISO-8859-1', 'UTF-8'),
-            $keyPrefix,
-            $paramStr
-        );
-        return base64_encode(mb_convert_encoding($rawScopedKey, 'UTF-8', 'ISO-8859-1'));
-    }
-
-    /**
-     * @return array
-     * @throws TypesenseClientError|HttpClientException
-     */
-    public function retrieve(): array
-    {
-        return $this->apiCall->get(static::RESOURCE_PATH, []);
-    }
-
-    /**
-     * @param mixed $offset
-     *
-     * @return bool
-     */
-    public function offsetExists($offset): bool
-    {
-        return isset($this->keys[$offset]);
-    }
-
-    /**
-     * @param mixed $offset
-     *
-     * @return \Typesense\Key
-     */
-    public function offsetGet($offset): Key
-    {
-        if (!isset($this->keys[$offset])) {
-            $this->keys[$offset] = new Key($offset, $this->apiCall);
-        }
-
-        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]);
-    }
-}
diff --git a/src/Lib/Configuration.php b/src/Lib/Configuration.php
deleted file mode 100644
index 54f0e9ea..00000000
--- a/src/Lib/Configuration.php
+++ /dev/null
@@ -1,226 +0,0 @@
-
- */
-class Configuration
-{
-
-    /**
-     * @var Node[]
-     */
-    private array $nodes;
-
-    /**
-     * @var Node|null
-     */
-    private ?Node $nearestNode;
-
-    /**
-     * @var string
-     */
-    private string $apiKey;
-
-    /**
-     * @var float
-     */
-    private float $numRetries;
-
-    /**
-     * @var float
-     */
-    private float $retryIntervalSeconds;
-
-    /**
-     * @var int
-     */
-    private int $healthCheckIntervalSeconds;
-
-    /**
-     * @var LoggerInterface
-     */
-    private LoggerInterface $logger;
-
-    /**
-     * @var null|ClientInterface
-     */
-    private ?ClientInterface $client = null;
-
-    /**
-     * @var int
-     */
-    private int $logLevel;
-
-    /**
-     * Configuration constructor.
-     *
-     * @param array $config
-     *
-     * @throws ConfigError
-     */
-    public function __construct(array $config)
-    {
-        $this->validateConfigArray($config);
-
-        $nodes = $config['nodes'] ?? [];
-
-        foreach ($nodes as $node) {
-            $this->nodes[] = new Node($node['host'], $node['port'], $node['path'] ?? '', $node['protocol']);
-        }
-
-        $nearestNode       = $config['nearest_node'] ?? null;
-        $this->nearestNode = null;
-        if (null !== $nearestNode) {
-            $this->nearestNode =
-                new Node(
-                    $nearestNode['host'],
-                    $nearestNode['port'],
-                    $nearestNode['path'] ?? '',
-                    $nearestNode['protocol']
-                );
-        }
-
-        $this->apiKey = $config['api_key'] ?? '';
-        $this->healthCheckIntervalSeconds = (int)($config['healthcheck_interval_seconds'] ?? 60);
-        $this->numRetries           = (float)($config['num_retries'] ?? 3);
-        $this->retryIntervalSeconds = (float)($config['retry_interval_seconds'] ?? 1.0);
-
-        $this->logLevel = $config['log_level'] ?? Logger::WARNING;
-        $this->logger   = new Logger('typesense');
-        $this->logger->pushHandler(new StreamHandler('php://stdout', $this->logLevel));
-
-        if (true === \array_key_exists('client', $config) && $config['client'] instanceof ClientInterface) {
-            $this->client = $config['client'];
-        }
-    }
-
-    /**
-     * @param array $config
-     *
-     * @throws ConfigError
-     */
-    private function validateConfigArray(array $config): void
-    {
-        $nodes = $config['nodes'] ?? false;
-        if (!$nodes) {
-            throw new ConfigError('`nodes` is not defined.');
-        }
-
-        $apiKey = $config['api_key'] ?? false;
-        if (!$apiKey) {
-            throw new ConfigError('`api_key` is not defined.');
-        }
-
-        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
-     *
-     * @return bool
-     */
-    public function validateNodeFields(array $node): bool
-    {
-        $keys = [
-            'host',
-            'port',
-            'protocol',
-        ];
-        return !array_diff_key(array_flip($keys), $node);
-    }
-
-    /**
-     * @return Node[]
-     */
-    public function getNodes(): array
-    {
-        return $this->nodes;
-    }
-
-    /**
-     * @return 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 getRetryIntervalSeconds(): float
-    {
-        return $this->retryIntervalSeconds;
-    }
-
-    /**
-     * @return float|mixed
-     */
-    public function getHealthCheckIntervalSeconds()
-    {
-        return $this->healthCheckIntervalSeconds;
-    }
-
-    /**
-     * @return LoggerInterface
-     */
-    public function getLogger(): LoggerInterface
-    {
-        return $this->logger;
-    }
-
-    /**
-     * @return ClientInterface
-     */
-    public function getClient(): ClientInterface
-    {
-        return new HttpMethodsClient(
-            $this->client ?? Psr18ClientDiscovery::find(),
-                Psr17FactoryDiscovery::findRequestFactory(),
-                Psr17FactoryDiscovery::findStreamFactory(),
-        );
-    }
-}
diff --git a/src/Lib/Node.php b/src/Lib/Node.php
deleted file mode 100644
index 3500baae..00000000
--- a/src/Lib/Node.php
+++ /dev/null
@@ -1,105 +0,0 @@
-
- */
-class Node
-{
-
-    /**
-     * @var string
-     */
-    private string $host;
-
-    /**
-     * @var string
-     */
-    private string $port;
-
-    /**
-     * @var string
-     */
-    private string $path;
-
-    /**
-     * @var string
-     */
-    private string $protocol;
-
-    /**
-     * @var bool
-     */
-    private bool $healthy = false;
-
-    /**
-     * @var int
-     */
-    private int $lastAccessTs;
-
-    /**
-     * Node constructor.
-     *
-     * @param string $host
-     * @param string $port
-     * @param string $path
-     * @param string $protocol
-     */
-    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();
-    }
-
-    /**
-     * @return string
-     */
-    public function url(): string
-    {
-        return sprintf('%s://%s:%s%s', $this->protocol, $this->host, $this->port, $this->path);
-    }
-
-    /**
-     * @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 setLastAccessTs(int $lastAccessTs): void
-    {
-        $this->lastAccessTs = $lastAccessTs;
-    }
-}
diff --git a/src/Metrics.php b/src/Metrics.php
deleted file mode 100644
index fb3c8b56..00000000
--- a/src/Metrics.php
+++ /dev/null
@@ -1,41 +0,0 @@
-apiCall = $apiCall;
-    }
-
-    /**
-     * @return array
-     * @throws TypesenseClientError|HttpClientException
-     */
-    public function retrieve(): array
-    {
-        return $this->apiCall->get(Metrics::RESOURCE_PATH, []);
-    }
-}
diff --git a/src/MultiSearch.php b/src/MultiSearch.php
deleted file mode 100644
index d24f9e9d..00000000
--- a/src/MultiSearch.php
+++ /dev/null
@@ -1,48 +0,0 @@
-apiCall = $apiCall;
-    }
-
-    /**
-     * @param string $searches
-     * @param array $queryParameters
-     *
-     * @return array
-     * @throws TypesenseClientError|HttpClientException
-     */
-    public function perform(array $searches, array $queryParameters = []): array
-    {
-        return $this->apiCall->post(
-            sprintf('%s', static::RESOURCE_PATH),
-            $searches,
-            true,
-            $queryParameters
-        );
-    }
-}
diff --git a/src/Objects/Collection.php b/src/Objects/Collection.php
new file mode 100644
index 00000000..16ed65e4
--- /dev/null
+++ b/src/Objects/Collection.php
@@ -0,0 +1,54 @@
+
+     */
+    public array $fields;
+
+    public string $name;
+
+    /**
+     * @var non-negative-int
+     */
+    public int $num_documents;
+
+    /**
+     * @var array
+     */
+    public array $symbols_to_index;
+
+    /**
+     * @var array
+     */
+    public array $token_separators;
+
+    /**
+     * {@inheritdoc}
+     */
+    public static function from(stdClass $data): static
+    {
+        $data->fields = array_map(
+            fn (stdClass $data) => CollectionField::from($data),
+            $data->fields,
+        );
+
+        return parent::from($data);
+    }
+}
diff --git a/src/Objects/CollectionDroppedField.php b/src/Objects/CollectionDroppedField.php
new file mode 100644
index 00000000..077a938a
--- /dev/null
+++ b/src/Objects/CollectionDroppedField.php
@@ -0,0 +1,14 @@
+raw) as $key => $value) {
+            $this->{$key} = $value;
+        }
+    }
+
+    /**
+     * Create object instance from raw data.
+     */
+    public static function from(stdClass $data): static
+    {
+        return new static($data);
+    }
+}
diff --git a/src/Operations.php b/src/Operations.php
deleted file mode 100644
index 661c5974..00000000
--- a/src/Operations.php
+++ /dev/null
@@ -1,48 +0,0 @@
-apiCall = $apiCall;
-    }
-
-    /**
-     * @param string $operationName
-     * @param array $queryParameters
-     *
-     * @return array
-     * @throws TypesenseClientError|HttpClientException
-     */
-    public function perform(string $operationName, array $queryParameters = []): array
-    {
-        return $this->apiCall->post(
-            sprintf('%s/%s', static::RESOURCE_PATH, $operationName),
-            null,
-            true,
-            $queryParameters
-        );
-    }
-}
diff --git a/src/Override.php b/src/Override.php
deleted file mode 100644
index d50855fe..00000000
--- a/src/Override.php
+++ /dev/null
@@ -1,78 +0,0 @@
-
- */
-class Override
-{
-
-    /**
-     * @var string
-     */
-    private string $collectionName;
-
-    /**
-     * @var string
-     */
-    private string $overrideId;
-
-    /**
-     * @var ApiCall
-     */
-    private ApiCall $apiCall;
-
-    /**
-     * Override constructor.
-     *
-     * @param string $collectionName
-     * @param string $overrideId
-     * @param ApiCall $apiCall
-     */
-    public function __construct(string $collectionName, string $overrideId, ApiCall $apiCall)
-    {
-        $this->collectionName = $collectionName;
-        $this->overrideId     = $overrideId;
-        $this->apiCall        = $apiCall;
-    }
-
-    /**
-     * @return string
-     */
-    private function endPointPath(): string
-    {
-        return sprintf(
-            '%s/%s/%s/%s',
-            Collections::RESOURCE_PATH,
-            $this->collectionName,
-            Overrides::RESOURCE_PATH,
-            $this->overrideId
-        );
-    }
-
-    /**
-     * @return array
-     * @throws TypesenseClientError|HttpClientException
-     */
-    public function retrieve(): array
-    {
-        return $this->apiCall->get($this->endPointPath(), []);
-    }
-
-    /**
-     * @return array
-     * @throws TypesenseClientError|HttpClientException
-     */
-    public function delete(): array
-    {
-        return $this->apiCall->delete($this->endPointPath());
-    }
-}
diff --git a/src/Overrides.php b/src/Overrides.php
deleted file mode 100644
index a7cc48fa..00000000
--- a/src/Overrides.php
+++ /dev/null
@@ -1,119 +0,0 @@
-
- */
-class Overrides implements \ArrayAccess
-{
-
-    public const RESOURCE_PATH = 'overrides';
-
-    /**
-     * @var ApiCall
-     */
-    private ApiCall $apiCall;
-
-    /**
-     * @var string
-     */
-    private string $collectionName;
-
-    /**
-     * @var array
-     */
-    private array $overrides = [];
-
-    /**
-     * Overrides constructor.
-     *
-     * @param string $collectionName
-     * @param ApiCall $apiCall
-     */
-    public function __construct(string $collectionName, ApiCall $apiCall)
-    {
-        $this->collectionName = $collectionName;
-        $this->apiCall        = $apiCall;
-    }
-
-    /**
-     * @param string $overrideId
-     *
-     * @return string
-     */
-    public function endPointPath(string $overrideId = ''): string
-    {
-        return sprintf(
-            '%s/%s/%s/%s',
-            Collections::RESOURCE_PATH,
-            $this->collectionName,
-            static::RESOURCE_PATH,
-            $overrideId
-        );
-    }
-
-    /**
-     * @param string $overrideId
-     * @param array $config
-     *
-     * @return array
-     * @throws TypesenseClientError|HttpClientException
-     */
-    public function upsert(string $overrideId, array $config): array
-    {
-        return $this->apiCall->put($this->endPointPath($overrideId), $config);
-    }
-
-    /**
-     * @return array
-     * @throws TypesenseClientError|HttpClientException
-     */
-    public function retrieve(): array
-    {
-        return $this->apiCall->get($this->endPointPath(), []);
-    }
-
-    /**
-     * @inheritDoc
-     */
-    public function offsetExists($overrideId): bool
-    {
-        return isset($this->overrides[$overrideId]);
-    }
-
-    /**
-     * @inheritDoc
-     */
-    public function offsetGet($overrideId): Override
-    {
-        if (!isset($this->overrides[$overrideId])) {
-            $this->overrides[$overrideId] = new Override($this->collectionName, $overrideId, $this->apiCall);
-        }
-
-        return $this->overrides[$overrideId];
-    }
-
-    /**
-     * @inheritDoc
-     */
-    public function offsetSet($overrideId, $value): void
-    {
-        $this->overrides[$overrideId] = $value;
-    }
-
-    /**
-     * @inheritDoc
-     */
-    public function offsetUnset($overrideId): void
-    {
-        unset($this->overrides[$overrideId]);
-    }
-}
diff --git a/src/Presets.php b/src/Presets.php
deleted file mode 100644
index b0f56c01..00000000
--- a/src/Presets.php
+++ /dev/null
@@ -1,107 +0,0 @@
-
- */
-class Presets
-{
-    /**
-     * @var ApiCall
-     */
-    private $apiCall;
-
-    public const PRESETS_PATH = '/presets';
-
-    public const MULTI_SEARCH_PATH = '/multi_search';
-
-    /**
-     * Document constructor.
-     *
-     * @param ApiCall $apiCall
-     */
-    public function __construct(ApiCall $apiCall)
-    {
-        $this->apiCall = $apiCall;
-    }
-
-    /**
-     * @param $presetName
-     * @return array|string
-     * @throws HttpClientException
-     * @throws TypesenseClientError
-     */
-    public function searchWithPreset($presetName)
-    {
-        return $this->apiCall->post($this->multiSearchEndpointPath(), [], true, ['preset' => $presetName]);
-    }
-
-    /**
-     * @return array|string
-     * @throws HttpClientException
-     * @throws TypesenseClientError
-     */
-    public function get()
-    {
-        return $this->apiCall->get(static::PRESETS_PATH, []);
-    }
-
-    /**
-     * @param array $options
-     *
-     * @return array
-     * @throws HttpClientException
-     * @throws TypesenseClientError
-     */
-    public function put(array $options = [])
-    {
-        $presetName = $options['preset_name'];
-        $presetsData = $options['preset_data'];
-
-        return $this->apiCall->put($this->endpointPath($presetName), $presetsData);
-    }
-
-    /**
-     * @param $presetName
-     * @return array
-     * @throws HttpClientException
-     * @throws TypesenseClientError
-     */
-    public function delete($presetName)
-    {
-        return $this->apiCall->delete($this->endpointPath($presetName));
-    }
-
-    /**
-     * @param $presetsName
-     * @return string
-     */
-    private function endpointPath($presetsName)
-    {
-        return sprintf(
-            '%s/%s',
-            static::PRESETS_PATH,
-            $presetsName
-        );
-    }
-
-    /**
-     * @param $presetsName
-     * @return string
-     */
-    private function multiSearchEndpointPath()
-    {
-        return sprintf(
-            '%s',
-            static::MULTI_SEARCH_PATH
-        );
-    }
-}
diff --git a/src/Requests/Collection.php b/src/Requests/Collection.php
new file mode 100644
index 00000000..6b8b2d30
--- /dev/null
+++ b/src/Requests/Collection.php
@@ -0,0 +1,142 @@
+,
+     *     enable_nested_fields?: bool,
+     *     token_separators?: array,
+     *     symbols_to_index?: array,
+     *     default_sorting_field?: string,
+     * } $payload
+     *
+     * @throws InvalidPayloadException
+     * @throws ResourceAlreadyExistsException
+     *
+     * @see https://typesense.org/docs/latest/api/collections.html#create-a-collection
+     */
+    public function create(array $payload): CollectionObject
+    {
+        $data = $this->send('POST', '/collections', $payload);
+
+        return CollectionObject::from($data);
+    }
+
+    /**
+     * @throws InvalidPayloadException
+     *
+     * @see https://typesense.org/docs/latest/api/collections.html#cloning-a-collection-schema
+     */
+    public function clone(string $source, string $name): CollectionObject
+    {
+        $query = http_build_query([
+            'src_name' => $source,
+        ]);
+
+        $path = sprintf('/collections?%s', $query);
+
+        $payload = [
+            'name' => $name,
+        ];
+
+        $data = $this->send('POST', $path, $payload);
+
+        return CollectionObject::from($data);
+    }
+
+    /**
+     * @throws ResourceNotFoundException
+     *
+     * @see https://typesense.org/docs/latest/api/collections.html#retrieve-a-collection
+     */
+    public function retrieve(string $name): CollectionObject
+    {
+        $path = sprintf('/collections/%s', $name);
+
+        $data = $this->send('GET', $path);
+
+        return CollectionObject::from($data);
+    }
+
+    /**
+     * @return array
+     *
+     * @see https://typesense.org/docs/latest/api/collections.html#list-all-collections
+     */
+    public function list(): array
+    {
+        $data = $this->send('GET', '/collections', expectArray: true);
+
+        return array_map(
+            fn (stdClass $datum) => CollectionObject::from($datum),
+            $data,
+        );
+    }
+
+    /**
+     * @throws ResourceNotFoundException
+     *
+     * @see https://typesense.org/docs/latest/api/collections.html#drop-a-collection
+     */
+    public function drop(string $name): CollectionObject
+    {
+        $path = sprintf('/collections/%s', $name);
+
+        $data = $this->send('DELETE', $path);
+
+        return CollectionObject::from($data);
+    }
+
+    /**
+     * @param  array  $fields
+     * @return array
+     *
+     * @throws ResourceNotFoundException
+     * @throws InvalidPayloadException
+     *
+     * @see https://typesense.org/docs/latest/api/collections.html#update-or-alter-a-collection
+     */
+    public function update(string $name, array $fields): array
+    {
+        $path = sprintf('/collections/%s', $name);
+
+        $data = $this->send('PATCH', $path, ['fields' => $fields]);
+
+        return array_map(function (stdClass $field) {
+            if (isset($field->drop)) {
+                return CollectionDroppedField::from($field);
+            }
+
+            return CollectionField::from($field);
+        }, $data->fields);
+    }
+}
diff --git a/src/Requests/Document.php b/src/Requests/Document.php
new file mode 100644
index 00000000..8ded46c6
--- /dev/null
+++ b/src/Requests/Document.php
@@ -0,0 +1,268 @@
+  $payload
+     * @param  class-string|null  $document
+     * @param  'create'|'upsert'  $action
+     * @return ($document is class-string ? T : GenericDocument)
+     */
+    public function index(
+        string $collection,
+        array $payload,
+        ?string $document = null,
+        string $action = 'create',
+    ): DocumentObject {
+        $query = http_build_query([
+            'action' => $action,
+        ]);
+
+        $path = sprintf(
+            '/collections/%s/documents?%s',
+            $collection,
+            $query,
+        );
+
+        $data = $this->send('POST', $path, $payload);
+
+        return $this->toDocument($data, $document);
+    }
+
+    /**
+     * @template T of DocumentObject
+     *
+     * @param  array  $payload
+     * @param  class-string|null  $document
+     * @return ($document is class-string ? T : GenericDocument)
+     */
+    public function upsert(
+        string $collection,
+        array $payload,
+        ?string $document = null,
+    ): DocumentObject {
+        return $this->index($collection, $payload, $document, 'upsert');
+    }
+
+    /**
+     * @param  array>  $payloads
+     * @param  'create'|'upsert'|'update'|'emplace'  $action
+     * @param  'coerce_or_reject'|'coerce_or_drop'|'drop'|'reject'  $dirty_values
+     * @return array
+     */
+    public function import(
+        string $collection,
+        array $payloads,
+        string $action = 'create',
+        bool $return_id = false,
+        bool $return_doc = false,
+        string $dirty_values = 'coerce_or_reject',
+        int $batch_size = 40,
+    ): array {
+        $query = http_build_query([
+            'action' => $action,
+            'return_id' => $return_id ? 'true' : 'false',
+            'return_doc' => $return_doc ? 'true' : 'false',
+            'dirty_values' => $dirty_values,
+            'batch_size' => $batch_size,
+        ]);
+
+        $path = sprintf(
+            '/collections/%s/documents/import?%s',
+            $collection,
+            $query,
+        );
+
+        $data = $this->send('POST', $path, $payloads, true, true);
+
+        return array_map(
+            fn (stdClass $datum) => ImportedDocument::from($datum),
+            $data,
+        );
+    }
+
+    /**
+     * @template T of DocumentObject
+     *
+     * @param  class-string|null  $document
+     * @return ($document is class-string ? T : GenericDocument)
+     */
+    public function retrieve(
+        string $collection,
+        string $id,
+        ?string $document = null,
+    ): DocumentObject {
+        $path = sprintf(
+            '/collections/%s/documents/%s',
+            $collection,
+            $id,
+        );
+
+        $data = $this->send('GET', $path);
+
+        return $this->toDocument($data, $document);
+    }
+
+    /**
+     * @template T of DocumentObject
+     *
+     * @param  array  $payload
+     * @param  class-string|null  $document
+     * @return ($document is class-string ? T : GenericDocument)
+     */
+    public function update(
+        string $collection,
+        string $id,
+        array $payload,
+        ?string $document = null,
+    ): DocumentObject {
+        $path = sprintf(
+            '/collections/%s/documents/%s',
+            $collection,
+            $id,
+        );
+
+        $data = $this->send('PATCH', $path, $payload);
+
+        return $this->toDocument($data, $document);
+    }
+
+    /**
+     * @param  array  $payload
+     * @return int The number of total updated documents.
+     */
+    public function updateByQuery(
+        string $collection,
+        string $filter_by,
+        array $payload,
+    ): int {
+        $query = http_build_query([
+            'filter_by' => $filter_by,
+        ]);
+
+        $path = sprintf(
+            '/collections/%s/documents?%s',
+            $collection,
+            $query,
+        );
+
+        $data = $this->send('PATCH', $path, $payload);
+
+        return $data->num_updated;
+    }
+
+    /**
+     * @template T of DocumentObject
+     *
+     * @param  class-string|null  $document
+     * @return ($document is class-string ? T : GenericDocument)
+     */
+    public function delete(
+        string $collection,
+        string $id,
+        ?string $document = null,
+    ): DocumentObject {
+        $path = sprintf(
+            '/collections/%s/documents/%s',
+            $collection,
+            $id,
+        );
+
+        $data = $this->send('DELETE', $path);
+
+        return $this->toDocument($data, $document);
+    }
+
+    /**
+     * @return int The number of total deleted documents.
+     */
+    public function deleteByQuery(
+        string $collection,
+        string $filter_by,
+        int $batch_size = 100,
+    ): int {
+        $query = http_build_query([
+            'filter_by' => $filter_by,
+            'batch_size' => $batch_size,
+        ]);
+
+        $path = sprintf(
+            '/collections/%s/documents?%s',
+            $collection,
+            $query,
+        );
+
+        $data = $this->send('DELETE', $path);
+
+        return $data->num_deleted;
+    }
+
+    /**
+     * @template T of DocumentObject
+     *
+     * @param  class-string|null  $document
+     * @return ($document is class-string ? array : array)
+     */
+    public function export(
+        string $collection,
+        string $filter_by = '',
+        string $include_fields = '',
+        string $exclude_fields = '',
+        ?string $document = null,
+    ): array {
+        $query = http_build_query(
+            array_filter(
+                compact('filter_by', 'include_fields', 'exclude_fields'),
+            ),
+        );
+
+        $path = sprintf(
+            '/collections/%s/documents/export?%s',
+            $collection,
+            $query,
+        );
+
+        $data = $this->send(
+            'GET',
+            $path,
+            expectArray: true,
+            ndjson: true,
+        );
+
+        return array_map(
+            fn (stdClass $datum) => $this->toDocument($datum, $document),
+            $data,
+        );
+    }
+
+    /**
+     * @template T of DocumentObject
+     *
+     * @param  class-string|null  $document
+     * @return ($document is class-string ? T : GenericDocument)
+     */
+    public function toDocument(
+        stdClass $data,
+        ?string $document = null,
+    ): DocumentObject {
+        if (
+            $document === null ||
+            ! is_subclass_of($document, DocumentObject::class)
+        ) {
+            return GenericDocument::from($data);
+        }
+
+        return new $document($data);
+    }
+}
diff --git a/src/Requests/Request.php b/src/Requests/Request.php
new file mode 100644
index 00000000..7aa0ca47
--- /dev/null
+++ b/src/Requests/Request.php
@@ -0,0 +1,127 @@
+> : array)  $body
+     * @return ($expectArray is false ? stdClass : array)
+     *
+     * @throws UnauthorizedException
+     */
+    public function send(
+        string $method,
+        string $path,
+        array $body = [],
+        bool $expectArray = false,
+        bool $ndjson = false,
+    ): stdClass|array {
+        if (! $ndjson) {
+            $form = json_encode($body) ?: '';
+        } else {
+            $form = array_map(
+                fn (array $payload) => json_encode($payload),
+                $body,
+            );
+
+            $form = implode(PHP_EOL, array_values(array_filter($form)));
+        }
+
+        $response = $this->http->request($method, $path, $form);
+
+        $contents = $response->getBody()->getContents();
+
+        $context = json_decode($contents, false);
+
+        $status = $response->getStatusCode();
+
+        if (! ($status >= 200 && $status < 300)) {
+            if (! ($context instanceof stdClass) || ! is_string($context->message)) {
+                throw new MalformedResponsePayloadException($contents);
+            }
+
+            $this->toException($status, $context->message);
+        }
+
+        if ($ndjson) {
+            return array_map(
+                function (string $data) {
+                    $result = json_decode($data, false);
+
+                    return $result instanceof stdClass ? $result : new stdClass();
+                },
+                explode(PHP_EOL, $contents),
+            );
+        }
+
+        if ($expectArray) {
+            if (! is_array($context)) {
+                throw new MalformedResponsePayloadException($contents);
+            }
+
+            return $context;
+        }
+
+        if (! ($context instanceof stdClass)) {
+            throw new MalformedResponsePayloadException($contents);
+        }
+
+        return $context;
+    }
+
+    /**
+     * Throw exception by the status code.
+     */
+    public function toException(int $status, string $message): void
+    {
+        if ($status === 400) {
+            throw new InvalidPayloadException($message);
+        }
+
+        if ($status === 401) {
+            throw new UnauthorizedException('Missing API key or the API key is invalid.');
+        }
+
+        if ($status === 404) {
+            throw new ResourceNotFoundException($message);
+        }
+
+        if ($status === 409) {
+            throw new ResourceAlreadyExistsException($message);
+        }
+
+        if ($status === 422) {
+            throw new UnprocessableEntityException($message);
+        }
+
+        if ($status === 503) {
+            throw new ServiceUnavailableException();
+        }
+
+        throw new UnknownHttpException();
+    }
+}
diff --git a/src/Synonym.php b/src/Synonym.php
deleted file mode 100644
index 5ce46c15..00000000
--- a/src/Synonym.php
+++ /dev/null
@@ -1,76 +0,0 @@
-collectionName = $collectionName;
-        $this->synonymId      = $synonymId;
-        $this->apiCall        = $apiCall;
-    }
-
-    /**
-     * @return string
-     */
-    private function endPointPath(): string
-    {
-        return sprintf(
-            '%s/%s/%s/%s',
-            Collections::RESOURCE_PATH,
-            $this->collectionName,
-            synonyms::RESOURCE_PATH,
-            $this->synonymId
-        );
-    }
-
-    /**
-     * @return array
-     * @throws TypesenseClientError|HttpClientException
-     */
-    public function retrieve(): array
-    {
-        return $this->apiCall->get($this->endPointPath(), []);
-    }
-
-    /**
-     * @return array
-     * @throws TypesenseClientError|HttpClientException
-     */
-    public function delete(): array
-    {
-        return $this->apiCall->delete($this->endPointPath());
-    }
-}
diff --git a/src/Synonyms.php b/src/Synonyms.php
deleted file mode 100644
index f48f2144..00000000
--- a/src/Synonyms.php
+++ /dev/null
@@ -1,117 +0,0 @@
-collectionName = $collectionName;
-        $this->apiCall        = $apiCall;
-    }
-
-    /**
-     * @param string $synonymId
-     *
-     * @return string
-     */
-    public function endPointPath(string $synonymId = ''): string
-    {
-        return sprintf(
-            '%s/%s/%s/%s',
-            Collections::RESOURCE_PATH,
-            $this->collectionName,
-            static::RESOURCE_PATH,
-            $synonymId
-        );
-    }
-
-    /**
-     * @param string $synonymId
-     * @param array $config
-     *
-     * @return array
-     * @throws TypesenseClientError|HttpClientException
-     */
-    public function upsert(string $synonymId, array $config): array
-    {
-        return $this->apiCall->put($this->endPointPath($synonymId), $config);
-    }
-
-    /**
-     * @return array
-     * @throws TypesenseClientError|HttpClientException
-     */
-    public function retrieve(): array
-    {
-        return $this->apiCall->get($this->endPointPath(), []);
-    }
-
-    /**
-     * @inheritDoc
-     */
-    public function offsetExists($synonymId): bool
-    {
-        return isset($this->synonyms[$synonymId]);
-    }
-
-    /**
-     * @inheritDoc
-     */
-    public function offsetGet($synonymId): Synonym
-    {
-        if (!isset($this->synonyms[$synonymId])) {
-            $this->synonyms[$synonymId] = new Synonym($this->collectionName, $synonymId, $this->apiCall);
-        }
-
-        return $this->synonyms[$synonymId];
-    }
-
-    /**
-     * @inheritDoc
-     */
-    public function offsetSet($synonymId, $value): void
-    {
-        $this->synonyms[$synonymId] = $value;
-    }
-
-    /**
-     * @inheritDoc
-     */
-    public function offsetUnset($synonymId): void
-    {
-        unset($this->synonyms[$synonymId]);
-    }
-}
diff --git a/src/Typesense.php b/src/Typesense.php
new file mode 100644
index 00000000..0f3bbf1f
--- /dev/null
+++ b/src/Typesense.php
@@ -0,0 +1,58 @@
+http = new Http($config);
+
+        $this->collection = new Collection($this->http);
+
+        $this->document = new Document($this->http);
+    }
+
+    public function setUrl(string $url): static
+    {
+        $this->http->config['url'] = $url;
+
+        return $this;
+    }
+
+    public function setApiKey(string $key): static
+    {
+        $this->http->config['apiKey'] = $key;
+
+        return $this;
+    }
+
+    public function setHttp(ClientInterface $http): static
+    {
+        $this->http->config['http'] = $http;
+
+        return $this;
+    }
+}
diff --git a/tests/Objects/TestObject.php b/tests/Objects/TestObject.php
new file mode 100644
index 00000000..f7020abe
--- /dev/null
+++ b/tests/Objects/TestObject.php
@@ -0,0 +1,14 @@
+in('Unit');
diff --git a/tests/TestCase.php b/tests/TestCase.php
new file mode 100644
index 00000000..80d36c1f
--- /dev/null
+++ b/tests/TestCase.php
@@ -0,0 +1,70 @@
+typesense = new Typesense([
+            'url' => 'http://localhost:8108',
+            'apiKey' => 'testing',
+        ]);
+
+        $this->createTestingCollection();
+    }
+
+    /**
+     * {@inheritdoc}
+     */
+    protected function tearDown(): void
+    {
+        $this->typesense->collection->drop($this->collectionName);
+
+        parent::tearDown();
+    }
+
+    /**
+     * Create testing collection.
+     */
+    protected function createTestingCollection(): void
+    {
+        $this->typesense->collection->create([
+            'name' => $this->collectionName,
+            'fields' => [
+                [
+                    'name' => 'name',
+                    'type' => 'string',
+                ],
+                [
+                    'name' => 'description',
+                    'type' => 'string',
+                ],
+            ],
+        ]);
+    }
+
+    /**
+     * Generate random slug.
+     */
+    public function slug(): string
+    {
+        return fake()->unique()->slug(variableNbWords: false);
+    }
+}
diff --git a/tests/Unit/ArchitectureTest.php b/tests/Unit/ArchitectureTest.php
new file mode 100644
index 00000000..d17b62a4
--- /dev/null
+++ b/tests/Unit/ArchitectureTest.php
@@ -0,0 +1,26 @@
+expect(['dd', 'dump', 'ray', 'var_dump', 'echo', 'print_r'])
+    ->not()
+    ->toBeUsed();
+
+test('strict typing must be enforced in the code')
+    ->expect('Typesense')
+    ->toUseStrictTypes();
+
+test('the code should not utilize the "final" keyword')
+    ->expect('Typesense')
+    ->not()
+    ->toBeFinal();
+
+test('all exception classes should extend "TypesenseException"')
+    ->expect('Typesense\Exceptions')
+    ->classes()
+    ->toExtend(TypesenseException::class);
diff --git a/tests/Unit/AuthenticationTest.php b/tests/Unit/AuthenticationTest.php
new file mode 100644
index 00000000..a8d23948
--- /dev/null
+++ b/tests/Unit/AuthenticationTest.php
@@ -0,0 +1,18 @@
+typesense->http->config['apiKey'];
+
+    $this->typesense->setApiKey($this->slug());
+
+    expect(fn () => $this->typesense->collection->list())
+        ->toThrow(UnauthorizedException::class);
+
+    $this->typesense->setApiKey($origin);
+});
diff --git a/tests/Unit/CollectionTest.php b/tests/Unit/CollectionTest.php
new file mode 100644
index 00000000..e1c12599
--- /dev/null
+++ b/tests/Unit/CollectionTest.php
@@ -0,0 +1,223 @@
+slug();
+
+    $fieldName = $this->slug();
+
+    $fieldType = 'string';
+
+    $collection = $this->typesense->collection->create([
+        'name' => $collectionName,
+        'fields' => [
+            [
+                'name' => $fieldName,
+                'type' => $fieldType,
+            ],
+        ],
+    ]);
+
+    expect($collection->created_at)->toBeInt();
+
+    expect($collection->default_sorting_field)->toBeEmpty();
+
+    expect($collection->enable_nested_fields)->toBeFalse();
+
+    expect($collection->name)->toBe($collectionName);
+
+    expect($collection->num_documents)->toBe(0);
+
+    expect($collection->symbols_to_index)->toBeEmpty();
+
+    expect($collection->token_separators)->toBeEmpty();
+
+    expect($collection->fields)->toHaveCount(1);
+
+    $field = $collection->fields[0];
+
+    expect($field->facet)->toBeFalse();
+
+    expect($field->index)->toBeTrue();
+
+    expect($field->infix)->toBeFalse();
+
+    expect($field->locale)->toBeEmpty();
+
+    expect($field->name)->toBe($fieldName);
+
+    expect($field->optional)->toBeFalse();
+
+    expect($field->sort)->toBeFalse();
+
+    expect($field->type)->toBe($fieldType);
+});
+
+test('it can not create collection if the name already exists', function () {
+    $this->typesense->collection->create([
+        'name' => $this->collectionName,
+        'fields' => [
+            [
+                'name' => $this->slug(),
+                'type' => 'string',
+            ],
+        ],
+    ]);
+})->throws(ResourceAlreadyExistsException::class);
+
+test('it can not create collection if the name is empty', function () {
+    $this->typesense->collection->create([
+        'name' => '',
+    ]);
+})->throws(InvalidPayloadException::class);
+
+test('it can not create collection if the fields is empty', function () {
+    $this->typesense->collection->create([
+        'name' => $this->slug(),
+        'fields' => [],
+    ]);
+})->throws(InvalidPayloadException::class);
+
+test('it can not create collection if the fields are invalid', function () {
+    $this->typesense->collection->create([
+        'name' => $this->slug(),
+        'fields' => [
+            [
+                'name' => '',
+                'type' => 'string',
+            ],
+        ],
+    ]);
+})->throws(InvalidPayloadException::class);
+
+test('it can clone collection', function () {
+    $target = $this->slug();
+
+    $collection = $this->typesense
+        ->collection
+        ->clone($this->collectionName, $target);
+
+    expect($collection->name)->toBe($target);
+});
+
+test('it can not clone collection if source collection does not exist', function () {
+    $this->typesense
+        ->collection
+        ->clone($this->slug(), $this->slug());
+})->throws(InvalidPayloadException::class);
+
+test('it can not clone collection if target collection already exists', function () {
+    $this->typesense
+        ->collection
+        ->clone($this->collectionName, $this->collectionName);
+})->throws(InvalidPayloadException::class);
+
+test('it can retrieve collection', function () {
+    $collection = $this->typesense
+        ->collection
+        ->retrieve($this->collectionName);
+
+    expect($collection->name)->toBe($this->collectionName);
+});
+
+test('it can not retrieve a non-existent collection', function () {
+    $this->typesense
+        ->collection
+        ->retrieve($this->slug());
+})->throws(ResourceNotFoundException::class);
+
+test('it can list collection', function () {
+    $collections = $this->typesense
+        ->collection
+        ->list();
+
+    expect($collections)
+        ->toBeArray()
+        ->toBeGreaterThanOrEqual(1);
+});
+
+test('it can drop collection', function () {
+    $name = $this->slug();
+
+    $this->typesense->collection->create([
+        'name' => $name,
+        'fields' => [
+            [
+                'name' => $this->slug(),
+                'type' => 'string',
+            ],
+        ],
+    ]);
+
+    $collection = $this->typesense->collection->drop($name);
+
+    expect($collection->name)->toBe($name);
+});
+
+test('it can not drop a non-existent collection', function () {
+    $this->typesense->collection->drop($this->slug());
+})->throws(ResourceNotFoundException::class);
+
+test('it can update collection', function () {
+    $name = $this->slug();
+
+    $fields = $this->typesense->collection->update($this->collectionName, [
+        [
+            'name' => $name,
+            'type' => 'int32',
+        ],
+    ]);
+
+    expect($fields)->toHaveCount(1);
+
+    expect($fields[0]->name)->toBe($name);
+
+    expect($fields[0]->type)->toBe('int32');
+
+    $fields = $this->typesense->collection->update($this->collectionName, [
+        [
+            'name' => $name,
+            'drop' => true,
+        ],
+    ]);
+
+    expect($fields)->toHaveCount(1);
+
+    expect($fields[0]->name)->toBe($name);
+
+    expect($fields[0]->drop)->toBeTrue();
+});
+
+test('it can not update a non-existent collection', function () {
+    $this->typesense->collection->update($this->slug(), [
+        [
+            'name' => $this->slug(),
+            'type' => 'int32',
+        ],
+    ]);
+})->throws(ResourceNotFoundException::class);
+
+test('it can not update an existing collection field', function () {
+    $this->typesense->collection->update($this->collectionName, [
+        [
+            'name' => 'name',
+            'type' => 'int32',
+        ],
+    ]);
+})->throws(InvalidPayloadException::class);
+
+test('it can not set drop to "false" for an existing collection field', function () {
+    $this->typesense->collection->update($this->collectionName, [
+        [
+            'name' => 'name',
+            'drop' => false,
+        ],
+    ]);
+})->throws(InvalidPayloadException::class);
diff --git a/tests/Unit/DocumentTest.php b/tests/Unit/DocumentTest.php
new file mode 100644
index 00000000..ea9a1f7b
--- /dev/null
+++ b/tests/Unit/DocumentTest.php
@@ -0,0 +1,276 @@
+slug();
+
+    $description = $this->slug();
+
+    $document = $this->typesense->document->index(
+        'testing',
+        [
+            'name' => $name,
+            'description' => $description,
+        ],
+        TestObject::class,
+    );
+
+    expect($document->name)->toBe($name);
+
+    expect($document->description)->toBe($description);
+});
+
+test('it can upsert document', function () {
+    $name = $this->slug();
+
+    $description = $this->slug();
+
+    $document = $this->typesense->document->index(
+        'testing',
+        [
+            'name' => $name,
+            'description' => $description,
+        ],
+        TestObject::class,
+    );
+
+    expect($document->name)->toBe($name);
+
+    expect($document->description)->toBe($description);
+
+    $name = $this->slug();
+
+    $description = $this->slug();
+
+    $document = $this->typesense->document->upsert(
+        'testing',
+        [
+            'id' => $document->id,
+            'name' => $name,
+            'description' => $description,
+        ],
+        TestObject::class,
+    );
+
+    expect($document->name)->toBe($name);
+
+    expect($document->description)->toBe($description);
+});
+
+test('it can import document', function () {
+    $document = $this->typesense->document->index(
+        'testing',
+        [
+            'name' => $this->slug(),
+            'description' => $this->slug(),
+        ],
+        TestObject::class,
+    );
+
+    $documents = [
+        [
+            'id' => $document->id,
+            'name' => $this->slug(),
+            'description' => $this->slug(),
+        ],
+        [
+            'name' => $this->slug(),
+            'description' => $this->slug(),
+        ],
+        [
+            'name' => $this->slug(),
+            'description' => $this->slug(),
+        ],
+    ];
+
+    $results = $this->typesense->document->import(
+        'testing',
+        $documents,
+        return_id: true,
+        return_doc: true,
+    );
+
+    expect($results)->toHaveCount(count($documents));
+
+    expect($results[0]->success)->toBeFalse();
+
+    expect($results[1]->success)->toBeTrue();
+
+    expect($results[2]->success)->toBeTrue();
+});
+
+test('it can retrieve document', function () {
+    $name = $this->slug();
+
+    $description = $this->slug();
+
+    $document = $this->typesense->document->index(
+        'testing',
+        [
+            'name' => $name,
+            'description' => $description,
+        ],
+        TestObject::class,
+    );
+
+    $document = $this->typesense->document->retrieve('testing', $document->id);
+
+    expect($document->name)->toBe($name);
+
+    expect($document->description)->toBe($description);
+});
+
+test('it can update document', function () {
+    $name = $this->slug();
+
+    $description = $this->slug();
+
+    $document = $this->typesense->document->index(
+        'testing',
+        [
+            'name' => $name,
+            'description' => $description,
+        ],
+        TestObject::class,
+    );
+
+    $description = $this->slug();
+
+    expect($document->description)->not()->toBe($description);
+
+    $document = $this->typesense->document->update(
+        'testing',
+        $document->id,
+        [
+            'description' => $description,
+        ],
+    );
+
+    expect($document->name)->toBe($name);
+
+    expect($document->description)->toBe($description);
+});
+
+test('it can update documents by query', function () {
+    $documents = [
+        [
+            'id' => '1',
+            'name' => $this->slug(),
+            'description' => $this->slug(),
+        ],
+        [
+            'id' => '2',
+            'name' => $this->slug(),
+            'description' => $this->slug(),
+        ],
+        [
+            'id' => '3',
+            'name' => $this->slug(),
+            'description' => $this->slug(),
+        ],
+    ];
+
+    $imported = $this->typesense->document->import(
+        'testing',
+        $documents,
+    );
+
+    expect($imported)->toHaveCount(count($documents));
+
+    $description = $this->slug();
+
+    $updated = $this->typesense->document->updateByQuery(
+        'testing',
+        'id:[2,3]',
+        [
+            'description' => $description,
+        ],
+    );
+
+    expect($updated)->toBe(2);
+});
+
+test('it can delete document', function () {
+    $document = $this->typesense->document->index(
+        'testing',
+        [
+            'name' => $this->slug(),
+            'description' => $this->slug(),
+        ],
+        TestObject::class,
+    );
+
+    $id = $document->id;
+
+    $document = $this->typesense->document->delete('testing', $id);
+
+    expect($document->id)->toBe($id);
+});
+
+test('it can delete documents by query', function () {
+    $documents = [
+        [
+            'id' => '1',
+            'name' => $this->slug(),
+            'description' => $this->slug(),
+        ],
+        [
+            'id' => '2',
+            'name' => $this->slug(),
+            'description' => $this->slug(),
+        ],
+        [
+            'id' => '3',
+            'name' => $this->slug(),
+            'description' => $this->slug(),
+        ],
+    ];
+
+    $imported = $this->typesense->document->import(
+        'testing',
+        $documents,
+    );
+
+    expect($imported)->toHaveCount(count($documents));
+
+    $deleted = $this->typesense->document->deleteByQuery(
+        'testing',
+        'id:[2,3]',
+    );
+
+    expect($deleted)->toBe(2);
+});
+
+test('it can export documents', function () {
+    $documents = [
+        [
+            'id' => '1',
+            'name' => $this->slug(),
+            'description' => $this->slug(),
+        ],
+        [
+            'id' => '2',
+            'name' => $this->slug(),
+            'description' => $this->slug(),
+        ],
+    ];
+
+    $imported = $this->typesense->document->import(
+        'testing',
+        $documents,
+    );
+
+    expect($imported)->toHaveCount(count($documents));
+
+    $exports = $this->typesense->document->export(
+        'testing',
+        document: TestObject::class,
+    );
+
+    expect($exports)->toHaveCount(count($documents));
+});
diff --git a/tests/Unit/HttpClientTest.php b/tests/Unit/HttpClientTest.php
new file mode 100644
index 00000000..27b6401e
--- /dev/null
+++ b/tests/Unit/HttpClientTest.php
@@ -0,0 +1,33 @@
+typesense->setHttp(
+        new Psr18Client(),
+    );
+
+    $collection = $this->typesense->collection->retrieve(
+        $this->collectionName,
+    );
+
+    expect($collection)->toBeInstanceOf(Collection::class);
+});
+
+test('it can use guzzle http client', function () {
+    $this->typesense->setHttp(
+        new Client(),
+    );
+
+    $collection = $this->typesense->collection->retrieve(
+        $this->collectionName,
+    );
+
+    expect($collection)->toBeInstanceOf(Collection::class);
+});
diff --git a/tests/Unit/MiscellaneousTest.php b/tests/Unit/MiscellaneousTest.php
new file mode 100644
index 00000000..a54b8d3f
--- /dev/null
+++ b/tests/Unit/MiscellaneousTest.php
@@ -0,0 +1,18 @@
+typesense->http->config['url'];
+
+    $this->typesense->setUrl('http://0.0.0.0:1');
+
+    expect(
+        fn () => $this->typesense->collection->retrieve('testing'),
+    )
+        ->toThrow('0.0.0.0');
+
+    $this->typesense->setUrl($origin);
+});
diff --git a/tests/Unit/ObjectCreationTest.php b/tests/Unit/ObjectCreationTest.php
new file mode 100644
index 00000000..fd7088dd
--- /dev/null
+++ b/tests/Unit/ObjectCreationTest.php
@@ -0,0 +1,22 @@
+name = 'Hello';
+
+    $data->description = 'World';
+
+    $object = TestObject::from($data);
+
+    expect($object->name)->toBe($data->name);
+
+    expect($object->description)->toBe($data->description);
+});

From 155dace16a4317b57c8461caccab284fdfd2a8ad Mon Sep 17 00:00:00 2001
From: bepsvpt <8221099+bepsvpt@users.noreply.github.com>
Date: Sun, 24 Mar 2024 09:04:26 +0800
Subject: [PATCH 02/15] chore: update CI workflows

---
 .github/workflows/testing.yml | 12 +++++++-----
 1 file changed, 7 insertions(+), 5 deletions(-)

diff --git a/.github/workflows/testing.yml b/.github/workflows/testing.yml
index 1cbac5d8..845bc8c0 100644
--- a/.github/workflows/testing.yml
+++ b/.github/workflows/testing.yml
@@ -2,6 +2,8 @@ name: Testing
 
 on:
   push:
+  schedule:
+    - cron: '0 0 * * 1' # run tests on every week Monday
 
 jobs:
   static_analyze:
@@ -22,7 +24,7 @@ jobs:
         run: echo "COMPOSER_DIR=$(composer config cache-files-dir)" >> $GITHUB_ENV
 
       - name: Cache dependencies
-        uses: actions/cache@v3
+        uses: actions/cache@v4
         with:
           path: ${{ env.COMPOSER_DIR }}
           key: ${{ runner.os }}-composer-static-analyze-${{ hashFiles('**/composer.json') }}
@@ -47,10 +49,10 @@ jobs:
         run: vendor/bin/pint -v --test
 
       - name: Run type coverage check
-        run: vendor/bin/pest --type-coverage --min=95
+        run: vendor/bin/pest --memory-limit=-1 --type-coverage --min=95
 
   testing:
-    name: PHP ${{ matrix.php }} (Testing)
+    name: PHP ${{ matrix.php }}
 
     runs-on: ubuntu-latest
 
@@ -62,7 +64,7 @@ jobs:
 
     services:
       typesense:
-        image: typesense/typesense:0.25.1
+        image: typesense/typesense:0.25.2
         ports:
             - 8108:8108/tcp
         volumes:
@@ -85,7 +87,7 @@ jobs:
         run: echo "COMPOSER_DIR=$(composer config cache-files-dir)" >> $GITHUB_ENV
 
       - name: Cache dependencies
-        uses: actions/cache@v3
+        uses: actions/cache@v4
         with:
           path: ${{ env.COMPOSER_DIR }}
           key: ${{ runner.os }}-composer-php-${{ matrix.php }}-${{ hashFiles('**/composer.json') }}

From e95a211d1d68bd73bef95aaef475773c81ea05e7 Mon Sep 17 00:00:00 2001
From: bepsvpt <8221099+bepsvpt@users.noreply.github.com>
Date: Sun, 24 Mar 2024 09:04:47 +0800
Subject: [PATCH 03/15] chore: update docker-compose configuration

---
 docker-compose.yml | 8 ++++----
 1 file changed, 4 insertions(+), 4 deletions(-)

diff --git a/docker-compose.yml b/docker-compose.yml
index c2a87649..9260c431 100644
--- a/docker-compose.yml
+++ b/docker-compose.yml
@@ -1,15 +1,15 @@
-version: "3.8"
+version: "3"
 
 services:
   typesense:
-    image: typesense/typesense:0.25.1
+    image: typesense/typesense:0.25.2
     container_name: typesense-testing
     restart: "on-failure"
     ports:
       - "8108:8108/tcp"
     environment:
-      TYPESENSE_DATA_DIR: /var/tmp
-      TYPESENSE_API_KEY: testing
+      TYPESENSE_DATA_DIR: "/var/tmp"
+      TYPESENSE_API_KEY: "testing"
       TYPESENSE_ENABLE_CORS: "true"
       TYPESENSE_PEERING_ADDRESS: "127.0.0.1"
       TYPESENSE_PEERING_PORT: "12345"

From 469e7ae08aad576d930c2355e2e00d0d9d3a3d7e Mon Sep 17 00:00:00 2001
From: bepsvpt <8221099+bepsvpt@users.noreply.github.com>
Date: Sun, 24 Mar 2024 09:05:03 +0800
Subject: [PATCH 04/15] chore: add editorconfig and gitattributes

---
 .editorconfig  | 15 +++++++++++++++
 .gitattributes | 14 ++++++++++++++
 2 files changed, 29 insertions(+)
 create mode 100644 .editorconfig
 create mode 100644 .gitattributes

diff --git a/.editorconfig b/.editorconfig
new file mode 100644
index 00000000..5175c803
--- /dev/null
+++ b/.editorconfig
@@ -0,0 +1,15 @@
+root = true
+
+[*]
+charset = utf-8
+end_of_line = lf
+indent_size = 2
+indent_style = space
+insert_final_newline = true
+trim_trailing_whitespace = true
+
+[*.md]
+trim_trailing_whitespace = false
+
+[*.php]
+indent_size = 4
diff --git a/.gitattributes b/.gitattributes
new file mode 100644
index 00000000..1f459efa
--- /dev/null
+++ b/.gitattributes
@@ -0,0 +1,14 @@
+* text=auto eol=lf
+
+*.md diff=markdown
+*.php diff=php
+
+/.github export-ignore
+/tests export-ignore
+.editorconfig export-ignore
+.gitattributes export-ignore
+.gitignore export-ignore
+phpstan.neon export-ignore
+phpunit.xml export-ignore
+pint.json export-ignore
+testbench.yaml export-ignore

From 9f343a9e0cb12b966574ba2d1dc1a396ef5473fe Mon Sep 17 00:00:00 2001
From: bepsvpt <8221099+bepsvpt@users.noreply.github.com>
Date: Sun, 24 Mar 2024 09:05:14 +0800
Subject: [PATCH 05/15] chore: update composer.json

---
 composer.json | 21 ++++++++++-----------
 1 file changed, 10 insertions(+), 11 deletions(-)

diff --git a/composer.json b/composer.json
index 26de44af..3d4f9943 100644
--- a/composer.json
+++ b/composer.json
@@ -15,6 +15,10 @@
       "email": "abdullah@devloops.net",
       "homepage": "https://www.devloops.net",
       "role": "Developer"
+    },
+    {
+      "name": "bepsvpt",
+      "email": "6ibrl@cpp.tw"
     }
   ],
   "homepage": "https://github.com/typesense/typesense-php",
@@ -32,12 +36,12 @@
     "psr/http-factory-implementation": "^1.0"
   },
   "require-dev": {
-    "ergebnis/composer-normalize": "^2.40",
+    "ergebnis/composer-normalize": "^2.42",
     "guzzlehttp/guzzle": "^7.8",
-    "laravel/pint": "^1.13",
-    "pestphp/pest": "^2.28",
+    "laravel/pint": "^1.14",
+    "pestphp/pest": "^2.34",
     "pestphp/pest-plugin-faker": "^2.0",
-    "pestphp/pest-plugin-type-coverage": "^2.5",
+    "pestphp/pest-plugin-type-coverage": "^2.8",
     "phpstan/phpstan": "^1.10",
     "symfony/http-client": "^7.0"
   },
@@ -60,17 +64,12 @@
       "php-http/discovery": false
     },
     "optimize-autoloader": true,
-    "preferred-install": {
-      "*": "dist"
-    },
     "sort-packages": true
   },
   "scripts": {
-    "lint": "phpcs -v",
-    "lint:fix": "phpcbf",
-    "typesenseServer": [
+    "typesense-server": [
       "Composer\\Config::disableProcessTimeout",
-      "docker-compose up"
+      "docker-compose pull && docker-compose up"
     ]
   }
 }

From f7c23f36ba6040a7f728c6dac93e83b90e202935 Mon Sep 17 00:00:00 2001
From: bepsvpt <8221099+bepsvpt@users.noreply.github.com>
Date: Sun, 24 Mar 2024 09:05:28 +0800
Subject: [PATCH 06/15] chore: update phpunit configuration

---
 phpunit.xml | 7 -------
 1 file changed, 7 deletions(-)

diff --git a/phpunit.xml b/phpunit.xml
index c8a2b8d9..b8cda39a 100644
--- a/phpunit.xml
+++ b/phpunit.xml
@@ -7,11 +7,6 @@
   colors="true"
   columns="max"
   executionOrder="random"
-  enforceTimeLimit="true"
-  defaultTimeLimit="2"
-  timeoutForSmallTests="1"
-  timeoutForMediumTests="3"
-  timeoutForLargeTests="5"
   displayDetailsOnTestsThatTriggerWarnings="true"
   failOnDeprecation="true"
   failOnWarning="true"
@@ -30,7 +25,6 @@
   
   
     
-      
       
     
   
@@ -39,7 +33,6 @@
     
     
     
-    
     
   
 

From 7de69a9defa67abe15dc54d742596d439dac21bb Mon Sep 17 00:00:00 2001
From: bepsvpt <8221099+bepsvpt@users.noreply.github.com>
Date: Sun, 24 Mar 2024 09:52:30 +0800
Subject: [PATCH 07/15] feat: support api keys endpoint

---
 src/Objects/Key.php    |  31 ++++++++++++
 src/Requests/Key.php   |  79 ++++++++++++++++++++++++++++++
 tests/Unit/KeyTest.php | 106 +++++++++++++++++++++++++++++++++++++++++
 3 files changed, 216 insertions(+)
 create mode 100644 src/Objects/Key.php
 create mode 100644 src/Requests/Key.php
 create mode 100644 tests/Unit/KeyTest.php

diff --git a/src/Objects/Key.php b/src/Objects/Key.php
new file mode 100644
index 00000000..ac489509
--- /dev/null
+++ b/src/Objects/Key.php
@@ -0,0 +1,31 @@
+
+     */
+    public array $actions;
+
+    /**
+     * @var non-empty-array
+     */
+    public array $collections;
+
+    public string $description;
+
+    public int $expires_at;
+
+    public int $id;
+
+    public ?string $value = null;
+
+    public ?string $value_prefix = null;
+}
diff --git a/src/Requests/Key.php b/src/Requests/Key.php
new file mode 100644
index 00000000..537e3d6d
--- /dev/null
+++ b/src/Requests/Key.php
@@ -0,0 +1,79 @@
+,
+     *     collections: array,
+     *     description: string,
+     *     value?: string,
+     *     expires_at?: int,
+     * } $payload
+     *
+     * @throws InvalidPayloadException
+     *
+     * @see https://typesense.org/docs/latest/api/api-keys.html#create-an-api-key
+     */
+    public function create(array $payload): KeyObject
+    {
+        $data = $this->send('POST', '/keys', $payload);
+
+        return KeyObject::from($data);
+    }
+
+    /**
+     * @throws ResourceNotFoundException
+     *
+     * @see https://typesense.org/docs/latest/api/api-keys.html#retrieve-an-api-key
+     */
+    public function retrieve(int $id): KeyObject
+    {
+        $path = sprintf('/keys/%d', $id);
+
+        $data = $this->send('GET', $path);
+
+        return KeyObject::from($data);
+    }
+
+    /**
+     * @return array
+     *
+     * @see https://typesense.org/docs/latest/api/api-keys.html#list-all-keys
+     */
+    public function list(): array
+    {
+        $data = $this->send('GET', '/keys');
+
+        return array_map(
+            fn (stdClass $datum) => KeyObject::from($datum),
+            $data->keys,
+        );
+    }
+
+    /**
+     * @throws ResourceNotFoundException
+     *
+     * @see https://typesense.org/docs/latest/api/api-keys.html#delete-api-key
+     */
+    public function delete(int $id): bool
+    {
+        $path = sprintf('/keys/%d', $id);
+
+        $data = $this->send('DELETE', $path);
+
+        return $data->id === $id;
+    }
+}
diff --git a/tests/Unit/KeyTest.php b/tests/Unit/KeyTest.php
new file mode 100644
index 00000000..daa2a65e
--- /dev/null
+++ b/tests/Unit/KeyTest.php
@@ -0,0 +1,106 @@
+typesense->key->create([
+        'actions' => ['collections:*'],
+        'collections' => ['*'],
+        'description' => $description = $this->slug(),
+        'expires_at' => $expires_at = time() + 100,
+    ]);
+
+    expect($key->actions)->toBe(['collections:*']);
+
+    expect($key->collections)->toBe(['*']);
+
+    expect($key->description)->toBe($description);
+
+    expect($key->expires_at)->toBe($expires_at);
+
+    expect($key->id)->toBeInt();
+
+    expect($key->value)->toBeString();
+
+    expect($key->value_prefix)->toBeNull();
+});
+
+test('it can not create an api key without actions', function () {
+    $this->typesense->key->create([
+        'actions' => [],
+        'collections' => ['*'],
+        'description' => $this->slug(),
+    ]);
+})->throws(InvalidPayloadException::class);
+
+test('it can not create an api key without collections', function () {
+    $this->typesense->key->create([
+        'actions' => ['*'],
+        'collections' => [],
+        'description' => $this->slug(),
+    ]);
+})->throws(InvalidPayloadException::class);
+
+test('it can create an api key without description', function () {
+    $this->typesense->key->create([
+        'actions' => ['*'],
+        'collections' => ['*'],
+    ]);
+})->throws(InvalidPayloadException::class);
+
+test('it can retrieve an existing key', function () {
+    $key = $this->typesense->key->create([
+        'actions' => ['*'],
+        'collections' => ['*'],
+        'description' => $this->slug(),
+    ]);
+
+    $nKey = $this->typesense->key->retrieve($key->id);
+
+    expect($nKey->id)->toBe($key->id);
+
+    expect($nKey->expires_at)->toBe($key->expires_at);
+});
+
+test('it can not retrieve a non-existent key', function () {
+    $this->typesense->key->retrieve(
+        mt_rand(1000000000, 2147483647),
+    );
+})->throws(ResourceNotFoundException::class);
+
+test('it can list all api keys', function () {
+    $key = $this->typesense->key->create([
+        'actions' => ['*'],
+        'collections' => ['*'],
+        'description' => $this->slug(),
+    ]);
+
+    $keys = $this->typesense->key->list();
+
+    $ids = array_column($keys, 'id');
+
+    expect($key->id)->toBeIn($ids);
+});
+
+test('it can delete an existing key', function () {
+    $key = $this->typesense->key->create([
+        'actions' => ['*'],
+        'collections' => ['*'],
+        'description' => $this->slug(),
+    ]);
+
+    $deleted = $this->typesense->key->delete($key->id);
+
+    expect($deleted)->toBeTrue();
+});
+
+test('it can not delete a non-existent key', function () {
+    $this->typesense->key->delete(
+        mt_rand(1000000000, 2147483647),
+    );
+})->throws(ResourceNotFoundException::class);

From 8b615fb54b028fad5eaa6d529a977324975e67de Mon Sep 17 00:00:00 2001
From: bepsvpt <8221099+bepsvpt@users.noreply.github.com>
Date: Sun, 24 Mar 2024 10:38:24 +0800
Subject: [PATCH 08/15] feat: support alias endpoint

---
 src/Objects/Alias.php    | 12 +++++
 src/Requests/Alias.php   | 74 +++++++++++++++++++++++++++++++
 src/Requests/Request.php |  2 +-
 tests/Unit/AliasTest.php | 94 ++++++++++++++++++++++++++++++++++++++++
 4 files changed, 181 insertions(+), 1 deletion(-)
 create mode 100644 src/Objects/Alias.php
 create mode 100644 src/Requests/Alias.php
 create mode 100644 tests/Unit/AliasTest.php

diff --git a/src/Objects/Alias.php b/src/Objects/Alias.php
new file mode 100644
index 00000000..191485e2
--- /dev/null
+++ b/src/Objects/Alias.php
@@ -0,0 +1,12 @@
+send('PUT', $path, $payload);
+
+        return AliasObject::from($data);
+    }
+
+    /**
+     * @throws ResourceNotFoundException
+     *
+     * @see https://typesense.org/docs/latest/api/collection-alias.html#retrieve-an-alias
+     */
+    public function retrieve(string $name): AliasObject
+    {
+        $path = sprintf('/aliases/%s', $name);
+
+        $data = $this->send('GET', $path);
+
+        return AliasObject::from($data);
+    }
+
+    /**
+     * @return array
+     *
+     * @see https://typesense.org/docs/latest/api/collection-alias.html#list-all-aliases
+     */
+    public function list(): array
+    {
+        $data = $this->send('GET', '/aliases');
+
+        return array_map(
+            fn (stdClass $datum) => AliasObject::from($datum),
+            $data->aliases,
+        );
+    }
+
+    /**
+     * @throws ResourceNotFoundException
+     *
+     * @see https://typesense.org/docs/latest/api/collection-alias.html#delete-an-alias
+     */
+    public function delete(string $name): bool
+    {
+        $path = sprintf('/aliases/%s', $name);
+
+        $data = $this->send('DELETE', $path);
+
+        return $data->name === $name;
+    }
+}
diff --git a/src/Requests/Request.php b/src/Requests/Request.php
index 7aa0ca47..733b33b4 100644
--- a/src/Requests/Request.php
+++ b/src/Requests/Request.php
@@ -27,7 +27,7 @@ public function __construct(
     }
 
     /**
-     * @param  'GET'|'POST'|'PATCH'|'DELETE'  $method
+     * @param  'GET'|'POST'|'PATCH'|'PUT'|'DELETE'  $method
      * @param  ($ndjson is true ? array> : array)  $body
      * @return ($expectArray is false ? stdClass : array)
      *
diff --git a/tests/Unit/AliasTest.php b/tests/Unit/AliasTest.php
new file mode 100644
index 00000000..eafe2f37
--- /dev/null
+++ b/tests/Unit/AliasTest.php
@@ -0,0 +1,94 @@
+typesense->alias->upsert(
+        $name = $this->slug(),
+        [
+            'collection_name' => $this->collectionName,
+        ],
+    );
+
+    expect($alias->name)->toBe($name);
+
+    expect($alias->collection_name)->toBe($this->collectionName);
+});
+
+test('it can create an alias to a non-existent collection', function () {
+    $alias = $this->typesense->alias->upsert(
+        $name = $this->slug(),
+        [
+            'collection_name' => $collection = $this->slug(),
+        ],
+    );
+
+    expect($alias->name)->toBe($name);
+
+    expect($alias->collection_name)->toBe($collection);
+});
+
+test('it can not create an alias without collection name', function () {
+    $this->typesense->alias->upsert(
+        $this->slug(),
+        [],
+    );
+})->throws(InvalidPayloadException::class);
+
+test('it can retrieve an existing alias', function () {
+    $alias = $this->typesense->alias->upsert(
+        $this->slug(),
+        [
+            'collection_name' => $this->collectionName,
+        ],
+    );
+
+    $nAlias = $this->typesense->alias->retrieve($alias->name);
+
+    expect($nAlias->name)->toBe($alias->name);
+});
+
+test('it can not retrieve a non-existent alias', function () {
+    $this->typesense->alias->retrieve(
+        $this->slug(),
+    );
+})->throws(ResourceNotFoundException::class);
+
+test('it can list all aliases', function () {
+    $alias = $this->typesense->alias->upsert(
+        $this->slug(),
+        [
+            'collection_name' => $this->collectionName,
+        ],
+    );
+
+    $aliases = $this->typesense->alias->list();
+
+    $names = array_column($aliases, 'name');
+
+    expect($alias->name)->toBeIn($names);
+});
+
+test('it can delete an existing alias', function () {
+    $alias = $this->typesense->alias->upsert(
+        $this->slug(),
+        [
+            'collection_name' => $this->collectionName,
+        ],
+    );
+
+    $deleted = $this->typesense->alias->delete($alias->name);
+
+    expect($deleted)->toBeTrue();
+});
+
+test('it can not delete a non-existent alias', function () {
+    $this->typesense->alias->delete(
+        $this->slug(),
+    );
+})->throws(ResourceNotFoundException::class);

From 6cc76a1a8d3b86f5c7c369dbd6204cf8b57a3f5f Mon Sep 17 00:00:00 2001
From: bepsvpt <8221099+bepsvpt@users.noreply.github.com>
Date: Sun, 24 Mar 2024 16:41:58 +0800
Subject: [PATCH 09/15] feat: support curation endpoint

---
 src/Objects/Curation.php    |  61 +++++++++++++++++++
 src/Requests/Curation.php   |  99 +++++++++++++++++++++++++++++++
 tests/Unit/CurationTest.php | 113 ++++++++++++++++++++++++++++++++++++
 3 files changed, 273 insertions(+)
 create mode 100644 src/Objects/Curation.php
 create mode 100644 src/Requests/Curation.php
 create mode 100644 tests/Unit/CurationTest.php

diff --git a/src/Objects/Curation.php b/src/Objects/Curation.php
new file mode 100644
index 00000000..b9d970e6
--- /dev/null
+++ b/src/Objects/Curation.php
@@ -0,0 +1,61 @@
+
+     */
+    public array $excludes = [];
+
+    /**
+     * @var array
+     */
+    public array $includes = [];
+
+    public ?string $filter_by = null;
+
+    public ?string $sort_by = null;
+
+    public ?string $replace_query = null;
+
+    public bool $remove_matched_tokens = true;
+
+    public bool $filter_curated_hits = false;
+
+    public ?int $effective_from_ts = null;
+
+    public ?int $effective_to_ts = null;
+
+    public bool $stop_processing = true;
+}
diff --git a/src/Requests/Curation.php b/src/Requests/Curation.php
new file mode 100644
index 00000000..c28346b1
--- /dev/null
+++ b/src/Requests/Curation.php
@@ -0,0 +1,99 @@
+,
+     *     excludes?: array,
+     *     filter_by?: string,
+     *     sort_by?: string,
+     *     replace_query?: string,
+     *     remove_matched_tokens?: bool,
+     *     filter_curated_hits?: bool,
+     *     effective_from_ts?: int,
+     *     effective_to_ts?: int,
+     *     stop_processing?: bool,
+     * } $payload
+     *
+     * @throws InvalidPayloadException
+     *
+     * @see https://typesense.org/docs/latest/api/curation.html#create-or-update-an-override
+     */
+    public function upsert(string $collection, string $id, array $payload): CurationObject
+    {
+        $path = sprintf('/collections/%s/overrides/%s', $collection, $id);
+
+        $data = $this->send('PUT', $path, $payload);
+
+        return CurationObject::from($data);
+    }
+
+    /**
+     * Fetch an individual override associated with a collection.
+     *
+     * @throws ResourceNotFoundException
+     *
+     * @see https://typesense.org/docs/latest/api/curation.html#retrieve-an-override
+     */
+    public function retrieve(string $collection, string $id): CurationObject
+    {
+        $path = sprintf('/collections/%s/overrides/%s', $collection, $id);
+
+        $data = $this->send('GET', $path);
+
+        return CurationObject::from($data);
+    }
+
+    /**
+     * Listing all overrides associated with a given collection.
+     *
+     * @return array
+     *
+     * @see https://typesense.org/docs/latest/api/curation.html#list-all-overrides
+     */
+    public function list(string $collection): array
+    {
+        $path = sprintf('/collections/%s/overrides', $collection);
+
+        $data = $this->send('GET', $path);
+
+        return array_map(
+            fn (stdClass $datum) => CurationObject::from($datum),
+            $data->overrides,
+        );
+    }
+
+    /**
+     * Deleting an override associated with a collection.
+     *
+     * @throws ResourceNotFoundException
+     *
+     * @see https://typesense.org/docs/latest/api/curation.html#delete-an-override
+     */
+    public function delete(string $collection, string $id): bool
+    {
+        $path = sprintf('/collections/%s/overrides/%s', $collection, $id);
+
+        $data = $this->send('DELETE', $path);
+
+        return $data->id === $id;
+    }
+}
diff --git a/tests/Unit/CurationTest.php b/tests/Unit/CurationTest.php
new file mode 100644
index 00000000..917e4371
--- /dev/null
+++ b/tests/Unit/CurationTest.php
@@ -0,0 +1,113 @@
+typesense->curation->upsert(
+        $this->collectionName,
+        $id = $this->slug(),
+        [
+            'rule' => [
+                'query' => 'apple',
+                'match' => 'contains',
+            ],
+            'remove_matched_tokens' => true,
+        ],
+    );
+
+    expect($curation->id)->toBe($id);
+});
+
+test('it can not create a curation without required fields', function () {
+    $this->typesense->curation->upsert(
+        $this->collectionName,
+        $this->slug(),
+        [
+            'rule' => [
+                'query' => 'apple',
+                'match' => 'contains',
+            ],
+        ],
+    );
+})->throws(InvalidPayloadException::class);
+
+test('it can retrieve an existing curation', function () {
+    $this->typesense->curation->upsert(
+        $this->collectionName,
+        $id = $this->slug(),
+        [
+            'rule' => [
+                'query' => 'apple',
+                'match' => 'contains',
+            ],
+            'remove_matched_tokens' => true,
+        ],
+    );
+
+    $curation = $this->typesense->curation->retrieve(
+        $this->collectionName,
+        $id,
+    );
+
+    expect($curation->id)->toBe($id);
+});
+
+test('it can not retrieve a non--existent curation', function () {
+    $this->typesense->curation->retrieve(
+        $this->collectionName,
+        $this->slug(),
+    );
+})->throws(ResourceNotFoundException::class);
+
+test('it can list all curations', function () {
+    $curation = $this->typesense->curation->upsert(
+        $this->collectionName,
+        $this->slug(),
+        [
+            'rule' => [
+                'query' => 'apple',
+                'match' => 'contains',
+            ],
+            'remove_matched_tokens' => true,
+        ],
+    );
+
+    $curations = $this->typesense->curation->list($this->collectionName);
+
+    $ids = array_column($curations, 'id');
+
+    expect($curation->id)->toBeIn($ids);
+});
+
+test('it can delete an existing curation', function () {
+    $curation = $this->typesense->curation->upsert(
+        $this->collectionName,
+        $this->slug(),
+        [
+            'rule' => [
+                'query' => 'apple',
+                'match' => 'contains',
+            ],
+            'remove_matched_tokens' => true,
+        ],
+    );
+
+    $deleted = $this->typesense->curation->delete(
+        $this->collectionName,
+        $curation->id,
+    );
+
+    expect($deleted)->toBeTrue();
+});
+
+test('it can not delete a non-existent curation', function () {
+    $this->typesense->curation->delete(
+        $this->collectionName,
+        $this->slug(),
+    );
+})->throws(ResourceNotFoundException::class);

From fe9cf5c83326d74af20bfdb5003447afef1eedfc Mon Sep 17 00:00:00 2001
From: bepsvpt <8221099+bepsvpt@users.noreply.github.com>
Date: Sun, 24 Mar 2024 17:14:06 +0800
Subject: [PATCH 10/15] feat: support analytic endpoint

---
 src/Objects/Analytic.php    |  27 ++++++++
 src/Requests/Analytic.php   |  84 ++++++++++++++++++++++++
 tests/Unit/AnalyticTest.php | 125 ++++++++++++++++++++++++++++++++++++
 3 files changed, 236 insertions(+)
 create mode 100644 src/Objects/Analytic.php
 create mode 100644 src/Requests/Analytic.php
 create mode 100644 tests/Unit/AnalyticTest.php

diff --git a/src/Objects/Analytic.php b/src/Objects/Analytic.php
new file mode 100644
index 00000000..71e7606f
--- /dev/null
+++ b/src/Objects/Analytic.php
@@ -0,0 +1,27 @@
+,
+ *     },
+ *     destination: array{
+ *         collection: string,
+ *     },
+ *     limit: int,
+ * }
+ */
+class Analytic extends TypesenseObject
+{
+    public string $name;
+
+    public string $type;
+
+    public stdClass $params;
+}
diff --git a/src/Requests/Analytic.php b/src/Requests/Analytic.php
new file mode 100644
index 00000000..96d99977
--- /dev/null
+++ b/src/Requests/Analytic.php
@@ -0,0 +1,84 @@
+send('POST', '/collections', [
+            'name' => $name,
+            'fields' => [
+                ['name' => 'q', 'type' => 'string'],
+                ['name' => 'count', 'type' => 'int32'],
+            ],
+        ]);
+
+        return $data->name === $name;
+    }
+
+    /**
+     * @param array{
+     *     name: string,
+     *     type: 'popular_queries',
+     *     params: RulePayload,
+     * } $payload
+     *
+     * @throws InvalidPayloadException
+     *
+     * @see https://typesense.org/docs/latest/api/analytics-query-suggestions.html#create-an-analytics-rule
+     */
+    public function create(array $payload): AnalyticObject
+    {
+        $data = $this->send('POST', '/analytics/rules', $payload);
+
+        return AnalyticObject::from($data);
+    }
+
+    /**
+     * @return array
+     *
+     * @see https://typesense.org/docs/latest/api/analytics-query-suggestions.html#list-all-rules
+     */
+    public function list(): array
+    {
+        $data = $this->send('GET', '/analytics/rules');
+
+        return array_map(
+            fn (stdClass $datum) => AnalyticObject::from($datum),
+            $data->rules,
+        );
+    }
+
+    /**
+     * @throws ResourceNotFoundException
+     *
+     * @see https://typesense.org/docs/latest/api/analytics-query-suggestions.html#remove-a-rule
+     */
+    public function delete(string $name): bool
+    {
+        $path = sprintf('/analytics/rules/%s', $name);
+
+        $data = $this->send('DELETE', $path);
+
+        return $data->name === $name;
+    }
+}
diff --git a/tests/Unit/AnalyticTest.php b/tests/Unit/AnalyticTest.php
new file mode 100644
index 00000000..8fd67ff7
--- /dev/null
+++ b/tests/Unit/AnalyticTest.php
@@ -0,0 +1,125 @@
+typesense->analytic->setup(
+        $this->slug(),
+    );
+
+    expect($created)->toBeTrue();
+});
+
+test('it can not use setup to create aggregate collection with existing collection name', function () {
+    $this->typesense->analytic->setup(
+        $name = $this->slug(),
+    );
+
+    $this->typesense->analytic->setup(
+        $name,
+    );
+})->throws(ResourceAlreadyExistsException::class);
+
+test('it can create an analytic rule', function () {
+    $this->typesense->analytic->setup(
+        $collection = $this->slug(),
+    );
+
+    $rule = $this->typesense->analytic->create([
+        'name' => $name = $this->slug(),
+        'type' => 'popular_queries',
+        'params' => [
+            'source' => [
+                'collections' => ['*'],
+            ],
+            'destination' => [
+                'collection' => $collection,
+            ],
+            'limit' => 50,
+        ],
+    ]);
+
+    expect($rule->name)->toBe($name);
+});
+
+test('it can not create an analytic rule with invalid type', function () {
+    $this->typesense->analytic->setup(
+        $collection = $this->slug(),
+    );
+
+    $this->typesense->analytic->create([
+        'name' => $this->slug(),
+        'type' => $this->slug(),
+        'params' => [
+            'source' => [
+                'collections' => ['*'],
+            ],
+            'destination' => [
+                'collection' => $collection,
+            ],
+            'limit' => 50,
+        ],
+    ]);
+})->throws(InvalidPayloadException::class);
+
+test('it can list all analytic rules', function () {
+    $this->typesense->analytic->setup(
+        $collection = $this->slug(),
+    );
+
+    $analytic = $this->typesense->analytic->create([
+        'name' => $this->slug(),
+        'type' => 'popular_queries',
+        'params' => [
+            'source' => [
+                'collections' => ['*'],
+            ],
+            'destination' => [
+                'collection' => $collection,
+            ],
+            'limit' => 50,
+        ],
+    ]);
+
+    $analytics = $this->typesense->analytic->list();
+
+    $names = array_column($analytics, 'name');
+
+    expect($analytic->name)->toBeIn($names);
+});
+
+test('it can delete an existing analytic rule', function () {
+    $this->typesense->analytic->setup(
+        $collection = $this->slug(),
+    );
+
+    $analytic = $this->typesense->analytic->create([
+        'name' => $this->slug(),
+        'type' => 'popular_queries',
+        'params' => [
+            'source' => [
+                'collections' => ['*'],
+            ],
+            'destination' => [
+                'collection' => $collection,
+            ],
+            'limit' => 50,
+        ],
+    ]);
+
+    $deleted = $this->typesense->analytic->delete($analytic->name);
+
+    expect($deleted)->toBeTrue();
+});
+
+test('it can not delete a non-existent analytic rule', function () {
+    $this->typesense->analytic->delete(
+        $this->slug(),
+    );
+})->throws(ResourceNotFoundException::class);

From decc2cf9b3cf08f85188d5e4fd06f007848adb72 Mon Sep 17 00:00:00 2001
From: bepsvpt <8221099+bepsvpt@users.noreply.github.com>
Date: Sun, 24 Mar 2024 17:41:43 +0800
Subject: [PATCH 11/15] feat: support synonym endpoint

---
 src/Objects/Synonym.php    |  24 +++++++++
 src/Requests/Synonym.php   |  84 ++++++++++++++++++++++++++++++
 tests/Unit/SynonymTest.php | 104 +++++++++++++++++++++++++++++++++++++
 3 files changed, 212 insertions(+)
 create mode 100644 src/Objects/Synonym.php
 create mode 100644 src/Requests/Synonym.php
 create mode 100644 tests/Unit/SynonymTest.php

diff --git a/src/Objects/Synonym.php b/src/Objects/Synonym.php
new file mode 100644
index 00000000..95305c20
--- /dev/null
+++ b/src/Objects/Synonym.php
@@ -0,0 +1,24 @@
+
+     */
+    public array $synonyms;
+
+    public string $root = '';
+
+    public string $locale = '';
+
+    /**
+     * @var array
+     */
+    public array $symbols_to_index = [];
+}
diff --git a/src/Requests/Synonym.php b/src/Requests/Synonym.php
new file mode 100644
index 00000000..0fdd96e1
--- /dev/null
+++ b/src/Requests/Synonym.php
@@ -0,0 +1,84 @@
+,
+     *     root?: string,
+     *     locale?: string,
+     *     symbols_to_index?: array,
+     * } $payload
+     *
+     * @see https://typesense.org/docs/latest/api/synonyms.html#create-or-update-a-synonym
+     */
+    public function upsert(string $collection, string $id, array $payload): SynonymObject
+    {
+        $path = sprintf('/collections/%s/synonyms/%s', $collection, $id);
+
+        $data = $this->send('PUT', $path, $payload);
+
+        return SynonymObject::from($data);
+    }
+
+    /**
+     * Retrieve a single synonym.
+     *
+     * @throws ResourceNotFoundException
+     *
+     * @see https://typesense.org/docs/latest/api/synonyms.html#retrieve-a-synonym
+     */
+    public function retrieve(string $collection, string $id): SynonymObject
+    {
+        $path = sprintf('/collections/%s/synonyms/%s', $collection, $id);
+
+        $data = $this->send('GET', $path);
+
+        return SynonymObject::from($data);
+    }
+
+    /**
+     * List all synonyms associated with a given collection.
+     *
+     * @return array
+     *
+     * @see https://typesense.org/docs/latest/api/synonyms.html#list-all-synonyms
+     */
+    public function list(string $collection): array
+    {
+        $path = sprintf('/collections/%s/synonyms', $collection);
+
+        $data = $this->send('GET', $path);
+
+        return array_map(
+            fn (stdClass $datum) => SynonymObject::from($datum),
+            $data->aliases,
+        );
+    }
+
+    /**
+     * Delete a synonym associated with a collection.
+     *
+     * @throws ResourceNotFoundException
+     *
+     * @see https://typesense.org/docs/latest/api/synonyms.html#delete-a-synonym
+     */
+    public function delete(string $collection, string $id): bool
+    {
+        $path = sprintf('/collections/%s/synonyms/%s', $collection, $id);
+
+        $data = $this->send('DELETE', $path);
+
+        return $data->id === $id;
+    }
+}
diff --git a/tests/Unit/SynonymTest.php b/tests/Unit/SynonymTest.php
new file mode 100644
index 00000000..b7637a80
--- /dev/null
+++ b/tests/Unit/SynonymTest.php
@@ -0,0 +1,104 @@
+typesense->synonym->upsert(
+        $this->collectionName,
+        $id = $this->slug(),
+        [
+            'synonyms' => $synonyms = ['apple', 'banana', 'car'],
+        ],
+    );
+
+    expect($synonym->id)->toBe($id);
+
+    expect($synonym->synonyms)->toBe($synonyms);
+
+    expect($synonym->root)->toBe('');
+});
+
+test('it can create a one-way synonym', function () {
+    $synonym = $this->typesense->synonym->upsert(
+        $this->collectionName,
+        $id = $this->slug(),
+        [
+            'root' => $root = 'dog',
+            'synonyms' => $synonyms = ['apple', 'banana', 'car'],
+        ],
+    );
+
+    expect($synonym->id)->toBe($id);
+
+    expect($synonym->synonyms)->toBe($synonyms);
+
+    expect($synonym->root)->toBe($root);
+});
+
+test('it can retrieve an existing synonym', function () {
+    $this->typesense->synonym->upsert(
+        $this->collectionName,
+        $id = $this->slug(),
+        [
+            'synonyms' => [$this->slug()],
+        ],
+    );
+
+    $synonym = $this->typesense->synonym->retrieve(
+        $this->collectionName,
+        $id,
+    );
+
+    expect($synonym->id)->toBe($id);
+});
+
+test('it can not retrieve a non-existent synonym', function () {
+    $this->typesense->synonym->retrieve(
+        $this->collectionName,
+        $this->slug(),
+    );
+})->throws(ResourceNotFoundException::class);
+
+test('it can list all synonyms', function () {
+    $synonym = $this->typesense->synonym->upsert(
+        $this->collectionName,
+        $this->slug(),
+        [
+            'synonyms' => [$this->slug()],
+        ],
+    );
+
+    $synonyms = $this->typesense->synonym->list($this->collectionName);
+
+    $ids = array_column($synonyms, 'id');
+
+    expect($synonym->id)->toBeIn($ids);
+});
+
+test('it can delete an existing synonym', function () {
+    $synonym = $this->typesense->synonym->upsert(
+        $this->collectionName,
+        $this->slug(),
+        [
+            'synonyms' => [$this->slug()],
+        ],
+    );
+
+    $deleted = $this->typesense->synonym->delete(
+        $this->collectionName,
+        $synonym->id,
+    );
+
+    expect($deleted)->toBeTrue();
+});
+
+test('it can not delete a non-existent synonym', function () {
+    $this->typesense->synonym->delete(
+        $this->collectionName,
+        $this->slug(),
+    );
+})->throws(ResourceNotFoundException::class);

From 25fbce3784e7d8904a7fda08b1789c0a6dbb5739 Mon Sep 17 00:00:00 2001
From: bepsvpt <8221099+bepsvpt@users.noreply.github.com>
Date: Sun, 24 Mar 2024 18:17:13 +0800
Subject: [PATCH 12/15] feat: support cluster endpoint

---
 src/Objects/Metric.php     | 84 ++++++++++++++++++++++++++++++++
 src/Objects/Stat.php       | 36 ++++++++++++++
 src/Requests/Cluster.php   | 99 ++++++++++++++++++++++++++++++++++++++
 tests/Unit/ClusterTest.php | 45 +++++++++++++++++
 4 files changed, 264 insertions(+)
 create mode 100644 src/Objects/Metric.php
 create mode 100644 src/Objects/Stat.php
 create mode 100644 src/Requests/Cluster.php
 create mode 100644 tests/Unit/ClusterTest.php

diff --git a/src/Objects/Metric.php b/src/Objects/Metric.php
new file mode 100644
index 00000000..eb0514a8
--- /dev/null
+++ b/src/Objects/Metric.php
@@ -0,0 +1,84 @@
+send('POST', $path);
+
+        return $data->success;
+    }
+
+    /**
+     * Compacting the on-disk database.
+     *
+     * @see https://typesense.org/docs/latest/api/cluster-operations.html#compacting-the-on-disk-database
+     */
+    public function compact(): bool
+    {
+        $data = $this->send('POST', '/operations/db/compact');
+
+        return $data->success;
+    }
+
+    /**
+     * Triggers a follower node to initiate the raft voting process, which triggers leader re-election.
+     *
+     * @see https://typesense.org/docs/latest/api/cluster-operations.html#re-elect-leader
+     */
+    public function reElectLeader(): bool
+    {
+        $data = $this->send('POST', '/operations/vote');
+
+        return $data->success;
+    }
+
+    /**
+     * Enable logging of requests that take over a defined threshold of time.
+     *
+     * @see https://typesense.org/docs/latest/api/cluster-operations.html#toggle-slow-request-log
+     */
+    public function updateSlowRequestLog(int $ms): bool
+    {
+        $data = $this->send('POST', '/config', [
+            'log-slow-requests-time-ms' => $ms,
+        ]);
+
+        return $data->success;
+    }
+
+    /**
+     * Get current RAM, CPU, Disk & Network usage metrics.
+     *
+     * @see https://typesense.org/docs/latest/api/cluster-operations.html#cluster-metrics
+     */
+    public function metrics(): Metric
+    {
+        $data = $this->send('GET', '/metrics.json');
+
+        return Metric::from($data);
+    }
+
+    /**
+     * Get stats about API endpoints.
+     *
+     * @see https://typesense.org/docs/latest/api/cluster-operations.html#api-stats
+     */
+    public function stats(): Stat
+    {
+        $data = $this->send('GET', '/stats.json');
+
+        return Stat::from($data);
+    }
+
+    /**
+     * Get health information about a Typesense node.
+     *
+     * @see https://typesense.org/docs/latest/api/cluster-operations.html#health
+     */
+    public function health(): bool
+    {
+        $data = $this->send('GET', '/health');
+
+        return $data->ok;
+    }
+}
diff --git a/tests/Unit/ClusterTest.php b/tests/Unit/ClusterTest.php
new file mode 100644
index 00000000..5b12c39d
--- /dev/null
+++ b/tests/Unit/ClusterTest.php
@@ -0,0 +1,45 @@
+slug());
+
+    $success = $this->typesense->cluster->snapshot(
+        $path,
+    );
+
+    expect($success)->toBeTrue();
+});
+
+test('it can compact the database', function () {
+    $success = $this->typesense->cluster->compact();
+
+    expect($success)->toBeTrue();
+});
+
+test('it can update slow request log', function () {
+    $success = $this->typesense->cluster->updateSlowRequestLog(100);
+
+    expect($success)->toBeTrue();
+});
+
+test('it can get metrics', function () {
+    $metric = $this->typesense->cluster->metrics();
+
+    expect($metric->typesense_memory_retained_bytes)->toBeGreaterThanOrEqual(0);
+});
+
+test('it can get stats', function () {
+    $stats = $this->typesense->cluster->stats();
+
+    expect($stats->delete_latency_ms)->toBeGreaterThanOrEqual(0);
+});
+
+test('it can get health', function () {
+    $health = $this->typesense->cluster->health();
+
+    expect($health)->toBeTrue();
+});

From 4e2087aaf57e89ca6d7630cce44998b0da0d282d Mon Sep 17 00:00:00 2001
From: bepsvpt <8221099+bepsvpt@users.noreply.github.com>
Date: Sun, 24 Mar 2024 18:17:45 +0800
Subject: [PATCH 13/15] chore: update typesense for endpoints

---
 src/Typesense.php | 36 +++++++++++++++++++++++++++++++++---
 1 file changed, 33 insertions(+), 3 deletions(-)

diff --git a/src/Typesense.php b/src/Typesense.php
index 0f3bbf1f..0ebe2cd7 100644
--- a/src/Typesense.php
+++ b/src/Typesense.php
@@ -5,8 +5,14 @@
 namespace Typesense;
 
 use Psr\Http\Client\ClientInterface;
+use Typesense\Requests\Alias;
+use Typesense\Requests\Analytic;
+use Typesense\Requests\Cluster;
 use Typesense\Requests\Collection;
+use Typesense\Requests\Curation;
 use Typesense\Requests\Document;
+use Typesense\Requests\Key;
+use Typesense\Requests\Synonym;
 
 /**
  * @phpstan-type TypesenseConfiguration array{
@@ -17,11 +23,23 @@
  */
 class Typesense
 {
-    public Http $http;
+    public readonly Http $http;
 
-    public Collection $collection;
+    public readonly Collection $collection;
 
-    public Document $document;
+    public readonly Document $document;
+
+    public readonly Analytic $analytic;
+
+    public readonly Key $key;
+
+    public readonly Curation $curation;
+
+    public readonly Alias $alias;
+
+    public readonly Synonym $synonym;
+
+    public readonly Cluster $cluster;
 
     /**
      * @param  TypesenseConfiguration  $config
@@ -33,6 +51,18 @@ public function __construct(array $config)
         $this->collection = new Collection($this->http);
 
         $this->document = new Document($this->http);
+
+        $this->analytic = new Analytic($this->http);
+
+        $this->key = new Key($this->http);
+
+        $this->curation = new Curation($this->http);
+
+        $this->alias = new Alias($this->http);
+
+        $this->synonym = new Synonym($this->http);
+
+        $this->cluster = new Cluster($this->http);
     }
 
     public function setUrl(string $url): static

From cdd9edd6cae4bdd030e5c188539d3bd28670ec60 Mon Sep 17 00:00:00 2001
From: bepsvpt <8221099+bepsvpt@users.noreply.github.com>
Date: Sun, 24 Mar 2024 18:22:57 +0800
Subject: [PATCH 14/15] fix: update synonyms list endpoint property accessing

---
 src/Requests/Synonym.php | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/src/Requests/Synonym.php b/src/Requests/Synonym.php
index 0fdd96e1..9733e42b 100644
--- a/src/Requests/Synonym.php
+++ b/src/Requests/Synonym.php
@@ -62,7 +62,7 @@ public function list(string $collection): array
 
         return array_map(
             fn (stdClass $datum) => SynonymObject::from($datum),
-            $data->aliases,
+            $data->synonyms,
         );
     }
 

From 1461df78dd67de515d6a1072ce8c188179b76b70 Mon Sep 17 00:00:00 2001
From: bepsvpt <8221099+bepsvpt@users.noreply.github.com>
Date: Sun, 24 Mar 2024 18:23:09 +0800
Subject: [PATCH 15/15] fix: update stat property types

---
 src/Objects/Stat.php | 20 ++++++++++----------
 1 file changed, 10 insertions(+), 10 deletions(-)

diff --git a/src/Objects/Stat.php b/src/Objects/Stat.php
index 02c47387..99fd2a0b 100644
--- a/src/Objects/Stat.php
+++ b/src/Objects/Stat.php
@@ -8,29 +8,29 @@
 
 class Stat extends TypesenseObject
 {
-    public int $delete_latency_ms;
+    public float $delete_latency_ms;
 
-    public int $delete_requests_per_second;
+    public float $delete_requests_per_second;
 
-    public int $import_latency_ms;
+    public float $import_latency_ms;
 
-    public int $import_requests_per_second;
+    public float $import_requests_per_second;
 
     public stdClass $latency_ms;
 
-    public int $overloaded_requests_per_second;
+    public float $overloaded_requests_per_second;
 
-    public int $pending_write_batches;
+    public float $pending_write_batches;
 
     public stdClass $requests_per_second;
 
-    public int $search_latency_ms;
+    public float $search_latency_ms;
 
-    public int $search_requests_per_second;
+    public float $search_requests_per_second;
 
     public float $total_requests_per_second;
 
-    public int $write_latency_ms;
+    public float $write_latency_ms;
 
-    public int $write_requests_per_second;
+    public float $write_requests_per_second;
 }