From d85ef4458e4ac4170d5e6fd134b939dd53aab6df Mon Sep 17 00:00:00 2001
From: 3bd-ulrahman <abdulrahman198276301@gmail.com>
Date: Tue, 15 Oct 2024 00:30:27 +0300
Subject: [PATCH 01/24] update to algoliasearch-client-php v4

---
 composer.json                    |  2 +-
 src/EngineManager.php            | 12 +++++++----
 src/Engines/AlgoliaEngine.php    | 34 +++++++++++++++-----------------
 tests/Unit/AlgoliaEngineTest.php |  2 +-
 4 files changed, 26 insertions(+), 24 deletions(-)

diff --git a/composer.json b/composer.json
index 7b59fed4..df0d3233 100644
--- a/composer.json
+++ b/composer.json
@@ -25,7 +25,7 @@
         "symfony/console": "^6.0|^7.0"
     },
     "require-dev": {
-        "algolia/algoliasearch-client-php": "^3.2",
+        "algolia/algoliasearch-client-php": "^4.0",
         "typesense/typesense-php": "^4.9.3",
         "meilisearch/meilisearch-php": "^1.0",
         "mockery/mockery": "^1.0",
diff --git a/src/EngineManager.php b/src/EngineManager.php
index 10b1be8e..16d32354 100644
--- a/src/EngineManager.php
+++ b/src/EngineManager.php
@@ -2,8 +2,10 @@
 
 namespace Laravel\Scout;
 
-use Algolia\AlgoliaSearch\Config\SearchConfig;
-use Algolia\AlgoliaSearch\SearchClient as Algolia;
+use Algolia\AlgoliaSearch\Configuration\SearchConfig;
+use Algolia\AlgoliaSearch\Api\SearchClient as Algolia;
+use Algolia\AlgoliaSearch\Model\Ingestion\Event;
+use Algolia\AlgoliaSearch\Support\AlgoliaAgent;
 use Algolia\AlgoliaSearch\Support\UserAgent;
 use Exception;
 use Illuminate\Support\Manager;
@@ -39,7 +41,8 @@ public function createAlgoliaDriver()
     {
         $this->ensureAlgoliaClientIsInstalled();
 
-        UserAgent::addCustomUserAgent('Laravel Scout', Scout::VERSION);
+        // UserAgent::addCustomUserAgent('Laravel Scout', Scout::VERSION);
+        AlgoliaAgent::addAlgoliaAgent('Laravel Scout', 'Laravel Scout', Scout::VERSION);
 
         $config = SearchConfig::create(
             config('scout.algolia.id'),
@@ -61,7 +64,8 @@ public function createAlgoliaDriver()
         }
 
         if (is_int($batchSize = config('scout.algolia.batch_size'))) {
-            $config->setBatchSize($batchSize);
+            // $config->setBatchSize($batchSize);
+            (new Event())->setBatchSize($batchSize);
         }
 
         return new AlgoliaEngine(Algolia::createWithConfig($config), config('scout.soft_delete'));
diff --git a/src/Engines/AlgoliaEngine.php b/src/Engines/AlgoliaEngine.php
index cec72d48..8b5b6a41 100644
--- a/src/Engines/AlgoliaEngine.php
+++ b/src/Engines/AlgoliaEngine.php
@@ -2,7 +2,7 @@
 
 namespace Laravel\Scout\Engines;
 
-use Algolia\AlgoliaSearch\SearchClient as Algolia;
+use Algolia\AlgoliaSearch\Api\SearchClient as Algolia;
 use Exception;
 use Illuminate\Database\Eloquent\SoftDeletes;
 use Illuminate\Support\LazyCollection;
@@ -14,7 +14,7 @@ class AlgoliaEngine extends Engine
     /**
      * The Algolia client.
      *
-     * @var \Algolia\AlgoliaSearch\SearchClient
+     * @var \Algolia\AlgoliaSearch\Api\SearchClient
      */
     protected $algolia;
 
@@ -28,7 +28,7 @@ class AlgoliaEngine extends Engine
     /**
      * Create a new engine instance.
      *
-     * @param  \Algolia\AlgoliaSearch\SearchClient  $algolia
+     * @param  \Algolia\AlgoliaSearch\Api\SearchClient  $algolia
      * @param  bool  $softDelete
      * @return void
      */
@@ -52,7 +52,7 @@ public function update($models)
             return;
         }
 
-        $index = $this->algolia->initIndex($models->first()->indexableAs());
+        $index = $models->first()->indexableAs();
 
         if ($this->usesSoftDelete($models->first()) && $this->softDelete) {
             $models->each->pushSoftDeleteMetadata();
@@ -71,7 +71,7 @@ public function update($models)
         })->filter()->values()->all();
 
         if (! empty($objects)) {
-            $index->saveObjects($objects);
+            $this->algolia->saveObjects($index, $objects);
         }
     }
 
@@ -87,13 +87,11 @@ public function delete($models)
             return;
         }
 
-        $index = $this->algolia->initIndex($models->first()->indexableAs());
-
         $keys = $models instanceof RemoveableScoutCollection
             ? $models->pluck($models->first()->getScoutKeyName())
             : $models->map->getScoutKey();
 
-        $index->deleteObjects($keys->all());
+        $this->algolia->deleteObjects($models->first()->indexableAs(), $keys->all());
     }
 
     /**
@@ -136,22 +134,24 @@ public function paginate(Builder $builder, $perPage, $page)
      */
     protected function performSearch(Builder $builder, array $options = [])
     {
-        $algolia = $this->algolia->initIndex(
-            $builder->index ?: $builder->model->searchableAs()
-        );
-
         $options = array_merge($builder->options, $options);
 
         if ($builder->callback) {
             return call_user_func(
                 $builder->callback,
-                $algolia,
+                $this->algolia,
                 $builder->query,
                 $options
             );
         }
 
-        return $algolia->search($builder->query, $options);
+        $queryParams = ['query' => $builder->query];
+
+        return $this->algolia->searchSingleIndex(
+            $builder->index ?: $builder->model->searchableAs(),
+            $queryParams,
+            $options
+        );
     }
 
     /**
@@ -280,9 +280,7 @@ public function getTotalCount($results)
      */
     public function flush($model)
     {
-        $index = $this->algolia->initIndex($model->indexableAs());
-
-        $index->clearObjects();
+        $this->algolia->clearObjects($model->indexableAs());
     }
 
     /**
@@ -307,7 +305,7 @@ public function createIndex($name, array $options = [])
      */
     public function deleteIndex($name)
     {
-        return $this->algolia->initIndex($name)->delete();
+        return $this->algolia->deleteIndex($name);
     }
 
     /**
diff --git a/tests/Unit/AlgoliaEngineTest.php b/tests/Unit/AlgoliaEngineTest.php
index 98c991b1..fc27077a 100644
--- a/tests/Unit/AlgoliaEngineTest.php
+++ b/tests/Unit/AlgoliaEngineTest.php
@@ -2,7 +2,7 @@
 
 namespace Laravel\Scout\Tests\Unit;
 
-use Algolia\AlgoliaSearch\SearchClient;
+use Algolia\AlgoliaSearch\Api\SearchClient;
 use Illuminate\Container\Container;
 use Illuminate\Database\Eloquent\Collection;
 use Illuminate\Support\Facades\Config;

From 6f2cdee8ef9c6506fb2e3519bd71ce3331301806 Mon Sep 17 00:00:00 2001
From: Mior Muhammad Zaki <crynobone@gmail.com>
Date: Thu, 7 Nov 2024 12:13:58 +0800
Subject: [PATCH 02/24] Fixes tests

Signed-off-by: Mior Muhammad Zaki <crynobone@gmail.com>
---
 tests/Unit/AlgoliaEngineTest.php | 42 +++++++++++++++-----------------
 1 file changed, 20 insertions(+), 22 deletions(-)

diff --git a/tests/Unit/AlgoliaEngineTest.php b/tests/Unit/AlgoliaEngineTest.php
index fc27077a..06177c6a 100644
--- a/tests/Unit/AlgoliaEngineTest.php
+++ b/tests/Unit/AlgoliaEngineTest.php
@@ -48,8 +48,7 @@ public function test_update_adds_objects_to_index()
     public function test_delete_removes_objects_to_index()
     {
         $client = m::mock(SearchClient::class);
-        $client->shouldReceive('initIndex')->with('table')->andReturn($index = m::mock(stdClass::class));
-        $index->shouldReceive('deleteObjects')->with([1]);
+        $client->shouldReceive('deleteObjects')->with('table', [1]);
 
         $engine = new AlgoliaEngine($client);
         $engine->delete(Collection::make([new SearchableModel(['id' => 1])]));
@@ -58,8 +57,7 @@ public function test_delete_removes_objects_to_index()
     public function test_delete_removes_objects_to_index_with_a_custom_search_key()
     {
         $client = m::mock(SearchClient::class);
-        $client->shouldReceive('initIndex')->with('table')->andReturn($index = m::mock(Indexes::class));
-        $index->shouldReceive('deleteObjects')->once()->with(['my-algolia-key.5']);
+        $client->shouldReceive('deleteObjects')->once()->with('table', ['my-algolia-key.5']);
 
         $engine = new AlgoliaEngine($client);
         $engine->delete(Collection::make([new AlgoliaCustomKeySearchableModel(['id' => 5])]));
@@ -74,8 +72,7 @@ public function test_delete_with_removeable_scout_collection_using_custom_search
         $job = unserialize(serialize($job));
 
         $client = m::mock(SearchClient::class);
-        $client->shouldReceive('initIndex')->with('table')->andReturn($index = m::mock(stdClass::class));
-        $index->shouldReceive('deleteObjects')->once()->with(['my-algolia-key.5']);
+        $client->shouldReceive('deleteObjects')->once()->with('table', ['my-algolia-key.5']);
 
         $engine = new AlgoliaEngine($client);
         $engine->delete($job->models);
@@ -111,10 +108,11 @@ public function test_remove_from_search_job_uses_custom_search_key()
     public function test_search_sends_correct_parameters_to_algolia()
     {
         $client = m::mock(SearchClient::class);
-        $client->shouldReceive('initIndex')->with('table')->andReturn($index = m::mock(stdClass::class));
-        $index->shouldReceive('search')->with('zonda', [
-            'numericFilters' => ['foo=1'],
-        ]);
+        $client->shouldReceive('searchSingleIndex')->with(
+            'table',
+            ['query' => 'zonda'],
+            ['numericFilters' => ['foo=1']]
+        );
 
         $engine = new AlgoliaEngine($client);
         $builder = new Builder(new SearchableModel, 'zonda');
@@ -125,10 +123,11 @@ public function test_search_sends_correct_parameters_to_algolia()
     public function test_search_sends_correct_parameters_to_algolia_for_where_in_search()
     {
         $client = m::mock(SearchClient::class);
-        $client->shouldReceive('initIndex')->with('table')->andReturn($index = m::mock(stdClass::class));
-        $index->shouldReceive('search')->with('zonda', [
-            'numericFilters' => ['foo=1', ['bar=1', 'bar=2']],
-        ]);
+        $client->shouldReceive('searchSingleIndex')->with(
+            'table',
+            ['query' => 'zonda'],
+            ['numericFilters' => ['foo=1', ['bar=1', 'bar=2']]]
+        );
 
         $engine = new AlgoliaEngine($client);
         $builder = new Builder(new SearchableModel, 'zonda');
@@ -139,10 +138,11 @@ public function test_search_sends_correct_parameters_to_algolia_for_where_in_sea
     public function test_search_sends_correct_parameters_to_algolia_for_empty_where_in_search()
     {
         $client = m::mock(SearchClient::class);
-        $client->shouldReceive('initIndex')->with('table')->andReturn($index = m::mock(stdClass::class));
-        $index->shouldReceive('search')->with('zonda', [
-            'numericFilters' => ['foo=1', '0=1'],
-        ]);
+        $client->shouldReceive('searchSingleIndex')->with(
+            'table',
+            ['query' => 'zonda'],
+            ['numericFilters' => ['foo=1', '0=1']]
+        );
 
         $engine = new AlgoliaEngine($client);
         $builder = new Builder(new SearchableModel, 'zonda');
@@ -280,8 +280,7 @@ public function test_a_model_is_indexed_with_a_custom_algolia_key()
     public function test_a_model_is_removed_with_a_custom_algolia_key()
     {
         $client = m::mock(SearchClient::class);
-        $client->shouldReceive('initIndex')->with('table')->andReturn($index = m::mock(stdClass::class));
-        $index->shouldReceive('deleteObjects')->with(['my-algolia-key.1']);
+        $client->shouldReceive('deleteObjects')->with('table', ['my-algolia-key.1']);
 
         $engine = new AlgoliaEngine($client);
         $engine->delete(Collection::make([new AlgoliaCustomKeySearchableModel(['id' => 1])]));
@@ -290,8 +289,7 @@ public function test_a_model_is_removed_with_a_custom_algolia_key()
     public function test_flush_a_model_with_a_custom_algolia_key()
     {
         $client = m::mock(SearchClient::class);
-        $client->shouldReceive('initIndex')->with('table')->andReturn($index = m::mock(stdClass::class));
-        $index->shouldReceive('clearObjects');
+        $client->shouldReceive('clearObjects')->with('table');
 
         $engine = new AlgoliaEngine($client);
         $engine->flush(new AlgoliaCustomKeySearchableModel);

From 1f89224665b9e177eabd85aae3566cb5700ea777 Mon Sep 17 00:00:00 2001
From: Mior Muhammad Zaki <crynobone@gmail.com>
Date: Thu, 7 Nov 2024 12:16:00 +0800
Subject: [PATCH 03/24] wip

Signed-off-by: Mior Muhammad Zaki <crynobone@gmail.com>
---
 composer.json | 3 +++
 1 file changed, 3 insertions(+)

diff --git a/composer.json b/composer.json
index df0d3233..eed9db7f 100644
--- a/composer.json
+++ b/composer.json
@@ -34,6 +34,9 @@
         "phpstan/phpstan": "^1.10",
         "phpunit/phpunit": "^9.3|^10.4"
     },
+    "conflict": {
+        "algolia/algoliasearch-client-php": "<4.0.0|>=5.0.0"
+    },
     "autoload": {
         "psr-4": {
             "Laravel\\Scout\\": "src/"

From 8fce0d3a6aaee29eee879ddcdbe0ad10483d3691 Mon Sep 17 00:00:00 2001
From: Mior Muhammad Zaki <crynobone@gmail.com>
Date: Thu, 7 Nov 2024 12:19:13 +0800
Subject: [PATCH 04/24] wip

Signed-off-by: Mior Muhammad Zaki <crynobone@gmail.com>
---
 tests/Integration/AlgoliaSearchableTest.php | 17 ++---------------
 1 file changed, 2 insertions(+), 15 deletions(-)

diff --git a/tests/Integration/AlgoliaSearchableTest.php b/tests/Integration/AlgoliaSearchableTest.php
index 42c1b23c..66d977fc 100644
--- a/tests/Integration/AlgoliaSearchableTest.php
+++ b/tests/Integration/AlgoliaSearchableTest.php
@@ -4,30 +4,17 @@
 
 use Illuminate\Support\Env;
 use Laravel\Scout\Tests\Fixtures\User;
+use Orchestra\Testbench\Attributes\RequiresEnv;
 
 /**
  * @group algolia
  * @group external-network
  */
+#[RequiresEnv('ALGOLIA_APP_ID')]
 class AlgoliaSearchableTest extends TestCase
 {
     use SearchableTests;
 
-    /**
-     * Define environment setup.
-     *
-     * @param  \Illuminate\Foundation\Application  $app
-     * @return void
-     */
-    protected function defineEnvironment($app)
-    {
-        if (is_null(Env::get('ALGOLIA_APP_ID'))) {
-            $this->markTestSkipped();
-        }
-
-        $this->defineScoutEnvironment($app);
-    }
-
     /**
      * Define database migrations.
      *

From 7a253d2c413e0a97eb199c945d7b89814a596b14 Mon Sep 17 00:00:00 2001
From: Mior Muhammad Zaki <crynobone@gmail.com>
Date: Thu, 7 Nov 2024 12:24:18 +0800
Subject: [PATCH 05/24] wip

Signed-off-by: Mior Muhammad Zaki <crynobone@gmail.com>
---
 tests/Integration/AlgoliaSearchableTest.php     | 11 +++++++++++
 tests/Integration/MeilisearchSearchableTest.php |  8 ++------
 tests/Integration/TypesenseSearchableTest.php   |  5 +----
 3 files changed, 14 insertions(+), 10 deletions(-)

diff --git a/tests/Integration/AlgoliaSearchableTest.php b/tests/Integration/AlgoliaSearchableTest.php
index 66d977fc..5ddfa559 100644
--- a/tests/Integration/AlgoliaSearchableTest.php
+++ b/tests/Integration/AlgoliaSearchableTest.php
@@ -15,6 +15,17 @@ class AlgoliaSearchableTest extends TestCase
 {
     use SearchableTests;
 
+    /**
+     * Define environment setup.
+     *
+     * @param  \Illuminate\Foundation\Application  $app
+     * @return void
+     */
+    protected function defineEnvironment($app)
+    {
+        $this->defineScoutEnvironment($app);
+    }
+
     /**
      * Define database migrations.
      *
diff --git a/tests/Integration/MeilisearchSearchableTest.php b/tests/Integration/MeilisearchSearchableTest.php
index 2fd9ad41..322cfd20 100644
--- a/tests/Integration/MeilisearchSearchableTest.php
+++ b/tests/Integration/MeilisearchSearchableTest.php
@@ -11,11 +11,13 @@
 use Meilisearch\Client;
 use Meilisearch\Endpoints\Indexes;
 use Mockery as m;
+use Orchestra\Testbench\Attributes\RequiresEnv;
 
 /**
  * @group meilisearch
  * @group external-network
  */
+#[RequiresEnv('MEILISEARCH_HOST')]
 class MeilisearchSearchableTest extends TestCase
 {
     use SearchableTests {
@@ -30,12 +32,6 @@ class MeilisearchSearchableTest extends TestCase
      */
     protected function defineEnvironment($app)
     {
-        if (is_null(Env::get('MEILISEARCH_HOST'))) {
-            $this->markTestSkipped();
-
-            return;
-        }
-
         $this->defineScoutEnvironment($app);
     }
 
diff --git a/tests/Integration/TypesenseSearchableTest.php b/tests/Integration/TypesenseSearchableTest.php
index c6405f25..a25c4fde 100644
--- a/tests/Integration/TypesenseSearchableTest.php
+++ b/tests/Integration/TypesenseSearchableTest.php
@@ -9,6 +9,7 @@
  * @group typesense
  * @group external-network
  */
+#[RequiresEnv('TYPESENSE_API_KEY')]
 class TypesenseSearchableTest extends TestCase
 {
     use SearchableTests;
@@ -21,10 +22,6 @@ class TypesenseSearchableTest extends TestCase
      */
     protected function defineEnvironment($app)
     {
-        if (is_null(Env::get('TYPESENSE_API_KEY'))) {
-            $this->markTestSkipped();
-        }
-
         $this->defineScoutEnvironment($app);
     }
 

From 43a7c8f5dcdf64c4bed30b0b25a79e68c64ecd73 Mon Sep 17 00:00:00 2001
From: Mior Muhammad Zaki <crynobone@gmail.com>
Date: Thu, 7 Nov 2024 12:26:30 +0800
Subject: [PATCH 06/24] wip

Signed-off-by: Mior Muhammad Zaki <crynobone@gmail.com>
---
 src/EngineManager.php                           | 2 +-
 tests/Integration/AlgoliaSearchableTest.php     | 1 -
 tests/Integration/MeilisearchSearchableTest.php | 1 -
 tests/Integration/TypesenseSearchableTest.php   | 1 -
 4 files changed, 1 insertion(+), 4 deletions(-)

diff --git a/src/EngineManager.php b/src/EngineManager.php
index 16d32354..d2c48ab1 100644
--- a/src/EngineManager.php
+++ b/src/EngineManager.php
@@ -2,8 +2,8 @@
 
 namespace Laravel\Scout;
 
-use Algolia\AlgoliaSearch\Configuration\SearchConfig;
 use Algolia\AlgoliaSearch\Api\SearchClient as Algolia;
+use Algolia\AlgoliaSearch\Configuration\SearchConfig;
 use Algolia\AlgoliaSearch\Model\Ingestion\Event;
 use Algolia\AlgoliaSearch\Support\AlgoliaAgent;
 use Algolia\AlgoliaSearch\Support\UserAgent;
diff --git a/tests/Integration/AlgoliaSearchableTest.php b/tests/Integration/AlgoliaSearchableTest.php
index 5ddfa559..76ce55d0 100644
--- a/tests/Integration/AlgoliaSearchableTest.php
+++ b/tests/Integration/AlgoliaSearchableTest.php
@@ -2,7 +2,6 @@
 
 namespace Laravel\Scout\Tests\Integration;
 
-use Illuminate\Support\Env;
 use Laravel\Scout\Tests\Fixtures\User;
 use Orchestra\Testbench\Attributes\RequiresEnv;
 
diff --git a/tests/Integration/MeilisearchSearchableTest.php b/tests/Integration/MeilisearchSearchableTest.php
index 322cfd20..0881fac1 100644
--- a/tests/Integration/MeilisearchSearchableTest.php
+++ b/tests/Integration/MeilisearchSearchableTest.php
@@ -3,7 +3,6 @@
 namespace Laravel\Scout\Tests\Integration;
 
 use Illuminate\Database\Eloquent\Collection;
-use Illuminate\Support\Env;
 use Laravel\Scout\Builder;
 use Laravel\Scout\Engines\MeilisearchEngine;
 use Laravel\Scout\Tests\Fixtures\User;
diff --git a/tests/Integration/TypesenseSearchableTest.php b/tests/Integration/TypesenseSearchableTest.php
index a25c4fde..c388f368 100644
--- a/tests/Integration/TypesenseSearchableTest.php
+++ b/tests/Integration/TypesenseSearchableTest.php
@@ -2,7 +2,6 @@
 
 namespace Laravel\Scout\Tests\Integration;
 
-use Illuminate\Support\Env;
 use Laravel\Scout\Tests\Fixtures\User;
 
 /**

From 25605a352b9f47b82740f3b74166d795a9b02598 Mon Sep 17 00:00:00 2001
From: Mior Muhammad Zaki <crynobone@gmail.com>
Date: Thu, 7 Nov 2024 12:28:28 +0800
Subject: [PATCH 07/24] wip

Signed-off-by: Mior Muhammad Zaki <crynobone@gmail.com>
---
 tests/Integration/TypesenseSearchableTest.php | 1 +
 1 file changed, 1 insertion(+)

diff --git a/tests/Integration/TypesenseSearchableTest.php b/tests/Integration/TypesenseSearchableTest.php
index c388f368..237bd468 100644
--- a/tests/Integration/TypesenseSearchableTest.php
+++ b/tests/Integration/TypesenseSearchableTest.php
@@ -3,6 +3,7 @@
 namespace Laravel\Scout\Tests\Integration;
 
 use Laravel\Scout\Tests\Fixtures\User;
+use Orchestra\Testbench\Attributes\RequiresEnv;
 
 /**
  * @group typesense

From a3689ab5da5caf11a0cd70472b8f018218144acb Mon Sep 17 00:00:00 2001
From: Mior Muhammad Zaki <crynobone@gmail.com>
Date: Thu, 7 Nov 2024 12:32:34 +0800
Subject: [PATCH 08/24] wip

Signed-off-by: Mior Muhammad Zaki <crynobone@gmail.com>
---
 tests/Integration/TestCase.php | 8 +++++---
 1 file changed, 5 insertions(+), 3 deletions(-)

diff --git a/tests/Integration/TestCase.php b/tests/Integration/TestCase.php
index 73f6be09..caad2db6 100644
--- a/tests/Integration/TestCase.php
+++ b/tests/Integration/TestCase.php
@@ -29,9 +29,11 @@ protected function importScoutIndexFrom($model = null)
      */
     public static function tearDownAfterClass(): void
     {
-        remote('scout:delete-all-indexes', [
-            'SCOUT_DRIVER' => static::scoutDriver(),
-        ])->mustRun();
+        rescue(function () {
+            remote('scout:delete-all-indexes', [
+                'SCOUT_DRIVER' => static::scoutDriver(),
+            ])->mustRun();
+        });
 
         parent::tearDownAfterClass();
     }

From 8e3e6288337d3fdeb04d0fb63df68fd27c4ed45c Mon Sep 17 00:00:00 2001
From: Mior Muhammad Zaki <crynobone@gmail.com>
Date: Thu, 7 Nov 2024 12:34:38 +0800
Subject: [PATCH 09/24] wip

Signed-off-by: Mior Muhammad Zaki <crynobone@gmail.com>
---
 .github/workflows/tests.yml | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml
index 5f468fe3..692bc3a2 100644
--- a/.github/workflows/tests.yml
+++ b/.github/workflows/tests.yml
@@ -90,7 +90,7 @@ jobs:
            composer update --prefer-dist --no-interaction --no-progress
 
       - name: Execute tests
-        run: vendor/bin/phpunit --group external-network,meilisearch
+        run: vendor/bin/phpunit --group meilisearch
         env:
           MEILISEARCH_HOST: "http://localhost:7700"
           MEILISEARCH_KEY: 'masterKey'

From 2bf18d9c90bdacfc73499b0719f34efbea0b4de6 Mon Sep 17 00:00:00 2001
From: Mior Muhammad Zaki <crynobone@gmail.com>
Date: Thu, 7 Nov 2024 12:35:11 +0800
Subject: [PATCH 10/24] wip

Signed-off-by: Mior Muhammad Zaki <crynobone@gmail.com>
---
 tests/Integration/TestCase.php | 8 +++-----
 1 file changed, 3 insertions(+), 5 deletions(-)

diff --git a/tests/Integration/TestCase.php b/tests/Integration/TestCase.php
index caad2db6..73f6be09 100644
--- a/tests/Integration/TestCase.php
+++ b/tests/Integration/TestCase.php
@@ -29,11 +29,9 @@ protected function importScoutIndexFrom($model = null)
      */
     public static function tearDownAfterClass(): void
     {
-        rescue(function () {
-            remote('scout:delete-all-indexes', [
-                'SCOUT_DRIVER' => static::scoutDriver(),
-            ])->mustRun();
-        });
+        remote('scout:delete-all-indexes', [
+            'SCOUT_DRIVER' => static::scoutDriver(),
+        ])->mustRun();
 
         parent::tearDownAfterClass();
     }

From cbc914f5df17064c20c7ea42c7584fb6cb20aaa9 Mon Sep 17 00:00:00 2001
From: Mior Muhammad Zaki <crynobone@gmail.com>
Date: Thu, 7 Nov 2024 12:35:57 +0800
Subject: [PATCH 11/24] Apply suggestions from code review

---
 src/EngineManager.php | 2 --
 1 file changed, 2 deletions(-)

diff --git a/src/EngineManager.php b/src/EngineManager.php
index d2c48ab1..930b26b3 100644
--- a/src/EngineManager.php
+++ b/src/EngineManager.php
@@ -41,7 +41,6 @@ public function createAlgoliaDriver()
     {
         $this->ensureAlgoliaClientIsInstalled();
 
-        // UserAgent::addCustomUserAgent('Laravel Scout', Scout::VERSION);
         AlgoliaAgent::addAlgoliaAgent('Laravel Scout', 'Laravel Scout', Scout::VERSION);
 
         $config = SearchConfig::create(
@@ -64,7 +63,6 @@ public function createAlgoliaDriver()
         }
 
         if (is_int($batchSize = config('scout.algolia.batch_size'))) {
-            // $config->setBatchSize($batchSize);
             (new Event())->setBatchSize($batchSize);
         }
 

From 70480de377656252596fc88e1296da3871c98c6d Mon Sep 17 00:00:00 2001
From: Mior Muhammad Zaki <crynobone@gmail.com>
Date: Thu, 7 Nov 2024 12:38:29 +0800
Subject: [PATCH 12/24] Update EngineManager.php

---
 src/EngineManager.php | 1 -
 1 file changed, 1 deletion(-)

diff --git a/src/EngineManager.php b/src/EngineManager.php
index 930b26b3..c199ce23 100644
--- a/src/EngineManager.php
+++ b/src/EngineManager.php
@@ -6,7 +6,6 @@
 use Algolia\AlgoliaSearch\Configuration\SearchConfig;
 use Algolia\AlgoliaSearch\Model\Ingestion\Event;
 use Algolia\AlgoliaSearch\Support\AlgoliaAgent;
-use Algolia\AlgoliaSearch\Support\UserAgent;
 use Exception;
 use Illuminate\Support\Manager;
 use Laravel\Scout\Engines\AlgoliaEngine;

From f37d448d40bf6ce9be812cb33d882372468d9833 Mon Sep 17 00:00:00 2001
From: Mior Muhammad Zaki <crynobone@gmail.com>
Date: Thu, 7 Nov 2024 14:32:27 +0800
Subject: [PATCH 13/24] Update composer.json

---
 composer.json | 3 ---
 1 file changed, 3 deletions(-)

diff --git a/composer.json b/composer.json
index eed9db7f..df0d3233 100644
--- a/composer.json
+++ b/composer.json
@@ -34,9 +34,6 @@
         "phpstan/phpstan": "^1.10",
         "phpunit/phpunit": "^9.3|^10.4"
     },
-    "conflict": {
-        "algolia/algoliasearch-client-php": "<4.0.0|>=5.0.0"
-    },
     "autoload": {
         "psr-4": {
             "Laravel\\Scout\\": "src/"

From 29e23c93e960671825effd669ee7ca72665db5c1 Mon Sep 17 00:00:00 2001
From: Mior Muhammad Zaki <crynobone@gmail.com>
Date: Thu, 7 Nov 2024 14:57:14 +0800
Subject: [PATCH 14/24] wip

Signed-off-by: Mior Muhammad Zaki <crynobone@gmail.com>
---
 .github/workflows/tests.yml | 3 ++-
 1 file changed, 2 insertions(+), 1 deletion(-)

diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml
index 692bc3a2..bf4ab9aa 100644
--- a/.github/workflows/tests.yml
+++ b/.github/workflows/tests.yml
@@ -90,7 +90,8 @@ jobs:
            composer update --prefer-dist --no-interaction --no-progress
 
       - name: Execute tests
-        run: vendor/bin/phpunit --group meilisearch
+        run: vendor/bin/phpunit --no-configuration --no-coverage --color --bootstrap vendor/autoload.php --group meilisearch tests/Integration
         env:
           MEILISEARCH_HOST: "http://localhost:7700"
           MEILISEARCH_KEY: 'masterKey'
+          DB_CONNECTION: 'testing'

From 096b330dc0045e86ff63d6ff83d1f21f43c80edd Mon Sep 17 00:00:00 2001
From: Taylor Otwell <taylor@laravel.com>
Date: Fri, 8 Nov 2024 08:20:27 +1000
Subject: [PATCH 15/24] Update EngineManager.php

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

diff --git a/src/EngineManager.php b/src/EngineManager.php
index c199ce23..3ac6fc6c 100644
--- a/src/EngineManager.php
+++ b/src/EngineManager.php
@@ -62,7 +62,7 @@ public function createAlgoliaDriver()
         }
 
         if (is_int($batchSize = config('scout.algolia.batch_size'))) {
-            (new Event())->setBatchSize($batchSize);
+            (new Event)->setBatchSize($batchSize);
         }
 
         return new AlgoliaEngine(Algolia::createWithConfig($config), config('scout.soft_delete'));

From 827a97f8e3423a82c8438557a42cdc69811ae0ee Mon Sep 17 00:00:00 2001
From: Mior Muhammad Zaki <crynobone@gmail.com>
Date: Fri, 8 Nov 2024 07:24:04 +0800
Subject: [PATCH 16/24] wip

---
 src/EngineManager.php | 15 ++++++---------
 1 file changed, 6 insertions(+), 9 deletions(-)

diff --git a/src/EngineManager.php b/src/EngineManager.php
index 3ac6fc6c..b03907cc 100644
--- a/src/EngineManager.php
+++ b/src/EngineManager.php
@@ -4,7 +4,6 @@
 
 use Algolia\AlgoliaSearch\Api\SearchClient as Algolia;
 use Algolia\AlgoliaSearch\Configuration\SearchConfig;
-use Algolia\AlgoliaSearch\Model\Ingestion\Event;
 use Algolia\AlgoliaSearch\Support\AlgoliaAgent;
 use Exception;
 use Illuminate\Support\Manager;
@@ -42,10 +41,12 @@ public function createAlgoliaDriver()
 
         AlgoliaAgent::addAlgoliaAgent('Laravel Scout', 'Laravel Scout', Scout::VERSION);
 
-        $config = SearchConfig::create(
-            config('scout.algolia.id'),
-            config('scout.algolia.secret')
-        )->setDefaultHeaders(
+        $config = (new SearchConfig(array_merge([
+            'appId' => config('scout.algolia.id'),
+            'apiKey' => config('scout.algolia.secret'),
+        ]), array_filter([
+            'batchSize' => config('scout.algolia.batch_size'),
+        ])))->setDefaultHeaders(
             $this->defaultAlgoliaHeaders()
         );
 
@@ -61,10 +62,6 @@ public function createAlgoliaDriver()
             $config->setWriteTimeout($writeTimeout);
         }
 
-        if (is_int($batchSize = config('scout.algolia.batch_size'))) {
-            (new Event)->setBatchSize($batchSize);
-        }
-
         return new AlgoliaEngine(Algolia::createWithConfig($config), config('scout.soft_delete'));
     }
 

From b487bea1bbeb7abc7194f1069984ed9169fe3729 Mon Sep 17 00:00:00 2001
From: Mior Muhammad Zaki <crynobone@gmail.com>
Date: Fri, 8 Nov 2024 15:14:14 +0800
Subject: [PATCH 17/24] wip

Signed-off-by: Mior Muhammad Zaki <crynobone@gmail.com>
---
 .github/workflows/tests.yml                   |   5 +-
 composer.json                                 |   2 +-
 src/EngineManager.php                         |  50 ++-
 src/Engines/Algolia3Engine.php                | 173 +++++++++
 src/Engines/Algolia4Engine.php                | 170 +++++++++
 src/Engines/AlgoliaEngine.php                 | 127 ++-----
 tests/Unit/Algolia3EngineTest.php             | 327 ++++++++++++++++++
 ...aEngineTest.php => Algolia4EngineTest.php} |  52 +--
 8 files changed, 769 insertions(+), 137 deletions(-)
 create mode 100644 src/Engines/Algolia3Engine.php
 create mode 100644 src/Engines/Algolia4Engine.php
 create mode 100644 tests/Unit/Algolia3EngineTest.php
 rename tests/Unit/{AlgoliaEngineTest.php => Algolia4EngineTest.php} (87%)

diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml
index bf4ab9aa..b9922876 100644
--- a/.github/workflows/tests.yml
+++ b/.github/workflows/tests.yml
@@ -19,8 +19,6 @@ jobs:
         php: ['8.0', 8.1, 8.2, 8.3]
         laravel: [9, 10, 11]
         exclude:
-          - php: '8.0'
-            laravel: 9
           - php: '8.0'
             laravel: 10
           - php: '8.0'
@@ -47,8 +45,7 @@ jobs:
 
       - name: Install dependencies
         run: |
-           composer require "illuminate/contracts=^${{ matrix.laravel }}" --no-update
-           composer update --prefer-dist --no-interaction --no-progress
+           composer update --prefer-dist --no-interaction --no-progress --with="illuminate/contracts=^${{ matrix.laravel }}"
 
       - name: Execute tests
         run: vendor/bin/phpunit
diff --git a/composer.json b/composer.json
index df0d3233..a86a6d4e 100644
--- a/composer.json
+++ b/composer.json
@@ -25,7 +25,7 @@
         "symfony/console": "^6.0|^7.0"
     },
     "require-dev": {
-        "algolia/algoliasearch-client-php": "^4.0",
+        "algolia/algoliasearch-client-php": "^3.2|^4.0",
         "typesense/typesense-php": "^4.9.3",
         "meilisearch/meilisearch-php": "^1.0",
         "mockery/mockery": "^1.0",
diff --git a/src/EngineManager.php b/src/EngineManager.php
index b03907cc..b739a9b0 100644
--- a/src/EngineManager.php
+++ b/src/EngineManager.php
@@ -2,9 +2,9 @@
 
 namespace Laravel\Scout;
 
-use Algolia\AlgoliaSearch\Api\SearchClient as Algolia;
-use Algolia\AlgoliaSearch\Configuration\SearchConfig;
-use Algolia\AlgoliaSearch\Support\AlgoliaAgent;
+use Algolia\AlgoliaSearch\Algolia;
+use Algolia\AlgoliaSearch\Support\AlgoliaAgent as Algolia4UserAgent;
+use Algolia\AlgoliaSearch\Support\UserAgent as Algolia3UserAgent;
 use Exception;
 use Illuminate\Support\Manager;
 use Laravel\Scout\Engines\AlgoliaEngine;
@@ -31,7 +31,7 @@ public function engine($name = null)
     }
 
     /**
-     * Create an Algolia engine instance.
+     * Create a Meilisearch engine instance.
      *
      * @return \Laravel\Scout\Engines\AlgoliaEngine
      */
@@ -39,14 +39,24 @@ public function createAlgoliaDriver()
     {
         $this->ensureAlgoliaClientIsInstalled();
 
-        AlgoliaAgent::addAlgoliaAgent('Laravel Scout', 'Laravel Scout', Scout::VERSION);
+        return version_compare(Algolia::VERSION, '4.0.0', '>=')
+            ? $this->configureAlgolia4Driver()
+            : $this->configureAlgolia3Driver();
+    }
+
+    /**
+     * Create an Algolia v4 engine instance.
+     *
+     * @return \Laravel\Scout\Engines\Algolia3Engine
+     */
+    protected function configureAlgolia3Driver()
+    {
+        Algolia3UserAgent::addCustomUserAgent('Laravel Scout', Scout::VERSION);
 
-        $config = (new SearchConfig(array_merge([
-            'appId' => config('scout.algolia.id'),
-            'apiKey' => config('scout.algolia.secret'),
-        ]), array_filter([
-            'batchSize' => config('scout.algolia.batch_size'),
-        ])))->setDefaultHeaders(
+        $config = SearchConfig::create(
+            config('scout.algolia.id'),
+            config('scout.algolia.secret')
+        )->setDefaultHeaders(
             $this->defaultAlgoliaHeaders()
         );
 
@@ -62,9 +72,25 @@ public function createAlgoliaDriver()
             $config->setWriteTimeout($writeTimeout);
         }
 
+        if (is_int($batchSize = config('scout.algolia.batch_size'))) {
+            $config->setBatchSize($batchSize);
+        }
+
         return new AlgoliaEngine(Algolia::createWithConfig($config), config('scout.soft_delete'));
     }
 
+    /**
+     * Create an Algolia v4 engine instance.
+     *
+     * @return \Laravel\Scout\Engines\Algolia4Engine
+     */
+    protected function configureAlgolia4Driver()
+    {
+        Algolia4UserAgent::addAlgoliaAgent('Laravel Scout', 'Laravel Scout', Scout::VERSION);
+
+        return Algolia4Engine::make(config('scout.algolia'), config('scout.soft_delete'));
+    }
+
     /**
      * Ensure the Algolia API client is installed.
      *
@@ -79,7 +105,7 @@ protected function ensureAlgoliaClientIsInstalled()
         }
 
         if (class_exists('AlgoliaSearch\Client')) {
-            throw new Exception('Please upgrade your Algolia client to version: ^3.2.');
+            throw new Exception('Please upgrade your Algolia client to version: ^3.2|^4.0.');
         }
 
         throw new Exception('Please install the suggested Algolia client: algolia/algoliasearch-client-php.');
diff --git a/src/Engines/Algolia3Engine.php b/src/Engines/Algolia3Engine.php
new file mode 100644
index 00000000..a4239fe8
--- /dev/null
+++ b/src/Engines/Algolia3Engine.php
@@ -0,0 +1,173 @@
+<?php
+
+namespace Laravel\Scout\Engines;
+
+use Algolia\AlgoliaSearch\Config\SearchConfig as Algolia3SearchConfig;
+use Algolia\AlgoliaSearch\SearchClient as Algolia3SearchClient;
+use Exception;
+use Illuminate\Database\Eloquent\SoftDeletes;
+use Illuminate\Support\LazyCollection;
+use Laravel\Scout\Builder;
+use Laravel\Scout\Jobs\RemoveableScoutCollection;
+
+/**
+ * @template TAlgoliaClient of \Algolia\AlgoliaSearch\SearchClient
+ */
+class Algolia3Engine extends AlgoliaEngine
+{
+    /**
+     * Create a new engine instance.
+     *
+     * @param  TAlgoliaClient  $algolia
+     * @param  bool  $softDelete
+     * @return void
+     */
+    public function __construct(Algolia3SearchClient $algolia, $softDelete = false)
+    {
+        parent::__construct($algolia, $softDelete);
+    }
+
+    /**
+     * Make a new engine instance.
+     *
+     * @param  array  $config
+     * @param  bool  $softDelete
+     * @return static
+     */
+    public static function make(array $config, bool $softDelete = false)
+    {
+        $config = Algolia3SearchConfig::create([
+            'appId' => $config['id'],
+            'apiKey' => $config['secret'],
+        ])->setDefaultHeaders(
+            $this->defaultAlgoliaHeaders()
+        );
+
+        if (is_int($connectTimeout = $config['connect_timeout'])) {
+            $configuration->setConnectTimeout($connectTimeout);
+        }
+
+        if (is_int($readTimeout = $config['read_timeout'])) {
+            $configuration->setReadTimeout($readTimeout);
+        }
+
+        if (is_int($writeTimeout = $config['write_timeout'])) {
+            $configuration->setWriteTimeout($writeTimeout);
+        }
+
+        if (is_int($batchSize = $config['batch_size'])) {
+            $configuration->setBatchSize($batchSize);
+        }
+
+        return new static(Algolia3SearchClient::createWithConfig($configuration), $softDelete);
+    }
+
+    /**
+     * Update the given model in the index.
+     *
+     * @param  \Illuminate\Database\Eloquent\Collection  $models
+     * @return void
+     *
+     * @throws \Algolia\AlgoliaSearch\Exceptions\AlgoliaException
+     */
+    public function update($models)
+    {
+        if ($models->isEmpty()) {
+            return;
+        }
+
+        $index = $this->algolia->initIndex($models->first()->indexableAs());
+
+        if ($this->usesSoftDelete($models->first()) && $this->softDelete) {
+            $models->each->pushSoftDeleteMetadata();
+        }
+
+        $objects = $models->map(function ($model) {
+            if (empty($searchableData = $model->toSearchableArray())) {
+                return;
+            }
+
+            return array_merge(
+                $searchableData,
+                $model->scoutMetadata(),
+                ['objectID' => $model->getScoutKey()],
+            );
+        })->filter()->values()->all();
+
+        if (! empty($objects)) {
+            $index->saveObjects($objects);
+        }
+    }
+
+    /**
+     * Remove the given model from the index.
+     *
+     * @param  \Illuminate\Database\Eloquent\Collection  $models
+     * @return void
+     */
+    public function delete($models)
+    {
+        if ($models->isEmpty()) {
+            return;
+        }
+
+        $index = $this->algolia->initIndex($models->first()->indexableAs());
+
+        $keys = $models instanceof RemoveableScoutCollection
+            ? $models->pluck($models->first()->getScoutKeyName())
+            : $models->map->getScoutKey();
+
+        $index->deleteObjects($keys->all());
+    }
+
+    /**
+     * Delete a search index.
+     *
+     * @param  string  $name
+     * @return mixed
+     */
+    public function deleteIndex($name)
+    {
+        return $this->algolia->initIndex($name)->delete();
+    }
+
+    /**
+     * Flush all of the model's records from the engine.
+     *
+     * @param  \Illuminate\Database\Eloquent\Model  $model
+     * @return void
+     */
+    public function flush($model)
+    {
+        $index = $this->algolia->initIndex($model->indexableAs());
+
+        $index->clearObjects();
+    }
+
+    /**
+     * Perform the given search on the engine.
+     *
+     * @param  \Laravel\Scout\Builder  $builder
+     * @param  array  $options
+     * @return mixed
+     */
+    protected function performSearch(Builder $builder, array $options = [])
+    {
+        $algolia = $this->algolia->initIndex(
+            $builder->index ?: $builder->model->searchableAs()
+        );
+
+        $options = array_merge($builder->options, $options);
+
+        if ($builder->callback) {
+            return call_user_func(
+                $builder->callback,
+                $algolia,
+                $builder->query,
+                $options
+            );
+        }
+
+        return $algolia->search($builder->query, $options);
+    }
+}
diff --git a/src/Engines/Algolia4Engine.php b/src/Engines/Algolia4Engine.php
new file mode 100644
index 00000000..d88857c6
--- /dev/null
+++ b/src/Engines/Algolia4Engine.php
@@ -0,0 +1,170 @@
+<?php
+
+namespace Laravel\Scout\Engines;
+
+use Algolia\AlgoliaSearch\Api\SearchClient as Algolia4SearchClient;
+use Algolia\AlgoliaSearch\Configuration\SearchConfig as Algolia4SearchConfig;
+use Algolia\AlgoliaSearch\Support\AlgoliaAgent as Algolia4UserAgent;
+use Exception;
+use Illuminate\Database\Eloquent\SoftDeletes;
+use Illuminate\Support\LazyCollection;
+use Laravel\Scout\Builder;
+use Laravel\Scout\Jobs\RemoveableScoutCollection;
+
+/**
+ * @template TAlgoliaClient of \Algolia\AlgoliaSearch\Api\SearchClient
+ */
+class Algolia4Engine extends AlgoliaEngine
+{
+    /**
+     * Create a new engine instance.
+     *
+     * @param  TAlgoliaClient  $algolia
+     * @param  bool  $softDelete
+     * @return void
+     */
+    public function __construct(Algolia4SearchClient $algolia, $softDelete = false)
+    {
+        parent::__construct($algolia, $softDelete);
+    }
+
+    /**
+     * Make a new engine instance.
+     *
+     * @param  array  $config
+     * @param  bool  $softDelete
+     * @return static
+     */
+    public static function make(array $config, bool $softDelete = false)
+    {
+        $configuration = (new Algolia4SearchConfig(array_merge([
+            'appId' => $config['id'],
+            'apiKey' => $config['secret'],
+        ]), array_filter([
+            'batchSize' => $config['batch_size'],
+        ])))->setDefaultHeaders(
+            $this->defaultAlgoliaHeaders()
+        );
+
+        if (is_int($connectTimeout = $config['connect_timeout'])) {
+            $configuration->setConnectTimeout($connectTimeout);
+        }
+
+        if (is_int($readTimeout = $config['read_timeout'])) {
+            $configuration->setReadTimeout($readTimeout);
+        }
+
+        if (is_int($writeTimeout = $config['write_timeout'])) {
+            $configuration->setWriteTimeout($writeTimeout);
+        }
+
+        return new static(Algolia4SearchClient::createWithConfig($configuration), $softDelete);
+    }
+
+    /**
+     * Update the given model in the index.
+     *
+     * @param  \Illuminate\Database\Eloquent\Collection  $models
+     * @return void
+     *
+     * @throws \Algolia\AlgoliaSearch\Exceptions\AlgoliaException
+     */
+    public function update($models)
+    {
+        if ($models->isEmpty()) {
+            return;
+        }
+
+        $index = $models->first()->indexableAs();
+
+        if ($this->usesSoftDelete($models->first()) && $this->softDelete) {
+            $models->each->pushSoftDeleteMetadata();
+        }
+
+        $objects = $models->map(function ($model) {
+            if (empty($searchableData = $model->toSearchableArray())) {
+                return;
+            }
+
+            return array_merge(
+                $searchableData,
+                $model->scoutMetadata(),
+                ['objectID' => $model->getScoutKey()],
+            );
+        })->filter()->values()->all();
+
+        if (! empty($objects)) {
+            $this->algolia->saveObjects($index, $objects);
+        }
+    }
+
+    /**
+     * Remove the given model from the index.
+     *
+     * @param  \Illuminate\Database\Eloquent\Collection  $models
+     * @return void
+     */
+    public function delete($models)
+    {
+        if ($models->isEmpty()) {
+            return;
+        }
+
+        $keys = $models instanceof RemoveableScoutCollection
+            ? $models->pluck($models->first()->getScoutKeyName())
+            : $models->map->getScoutKey();
+
+        $this->algolia->deleteObjects($models->first()->indexableAs(), $keys->all());
+    }
+
+    /**
+     * Delete a search index.
+     *
+     * @param  string  $name
+     * @return mixed
+     */
+    public function deleteIndex($name)
+    {
+        return $this->algolia->deleteIndex($name);
+    }
+
+    /**
+     * Flush all of the model's records from the engine.
+     *
+     * @param  \Illuminate\Database\Eloquent\Model  $model
+     * @return void
+     */
+    public function flush($model)
+    {
+        $this->algolia->clearObjects($model->indexableAs());
+    }
+
+    /**
+     * Perform the given search on the engine.
+     *
+     * @param  \Laravel\Scout\Builder  $builder
+     * @param  array  $options
+     * @return mixed
+     */
+    protected function performSearch(Builder $builder, array $options = [])
+    {
+        $options = array_merge($builder->options, $options);
+
+        if ($builder->callback) {
+            return call_user_func(
+                $builder->callback,
+                $this->algolia,
+                $builder->query,
+                $options
+            );
+        }
+
+        $queryParams = ['query' => $builder->query];
+
+        return $this->algolia->searchSingleIndex(
+            $builder->index ?: $builder->model->searchableAs(),
+            $queryParams,
+            $options
+        );
+    }
+}
diff --git a/src/Engines/AlgoliaEngine.php b/src/Engines/AlgoliaEngine.php
index 8b5b6a41..91d47b56 100644
--- a/src/Engines/AlgoliaEngine.php
+++ b/src/Engines/AlgoliaEngine.php
@@ -9,12 +9,15 @@
 use Laravel\Scout\Builder;
 use Laravel\Scout\Jobs\RemoveableScoutCollection;
 
-class AlgoliaEngine extends Engine
+/**
+ * @template TAlgoliaClient of object
+ */
+abstract class AlgoliaEngine extends Engine
 {
     /**
      * The Algolia client.
      *
-     * @var \Algolia\AlgoliaSearch\Api\SearchClient
+     * @var TAlgoliaClient
      */
     protected $algolia;
 
@@ -23,16 +26,16 @@ class AlgoliaEngine extends Engine
      *
      * @var bool
      */
-    protected $softDelete;
+    protected $softDelete = false;
 
     /**
      * Create a new engine instance.
      *
-     * @param  \Algolia\AlgoliaSearch\Api\SearchClient  $algolia
+     * @param  TAlgoliaClient  $algolia
      * @param  bool  $softDelete
      * @return void
      */
-    public function __construct(Algolia $algolia, $softDelete = false)
+    public function __construct($algolia, $softDelete = false)
     {
         $this->algolia = $algolia;
         $this->softDelete = $softDelete;
@@ -46,34 +49,7 @@ public function __construct(Algolia $algolia, $softDelete = false)
      *
      * @throws \Algolia\AlgoliaSearch\Exceptions\AlgoliaException
      */
-    public function update($models)
-    {
-        if ($models->isEmpty()) {
-            return;
-        }
-
-        $index = $models->first()->indexableAs();
-
-        if ($this->usesSoftDelete($models->first()) && $this->softDelete) {
-            $models->each->pushSoftDeleteMetadata();
-        }
-
-        $objects = $models->map(function ($model) {
-            if (empty($searchableData = $model->toSearchableArray())) {
-                return;
-            }
-
-            return array_merge(
-                $searchableData,
-                $model->scoutMetadata(),
-                ['objectID' => $model->getScoutKey()],
-            );
-        })->filter()->values()->all();
-
-        if (! empty($objects)) {
-            $this->algolia->saveObjects($index, $objects);
-        }
-    }
+    abstract public function update($models);
 
     /**
      * Remove the given model from the index.
@@ -81,18 +57,32 @@ public function update($models)
      * @param  \Illuminate\Database\Eloquent\Collection  $models
      * @return void
      */
-    public function delete($models)
-    {
-        if ($models->isEmpty()) {
-            return;
-        }
+    abstract public function delete($models);
 
-        $keys = $models instanceof RemoveableScoutCollection
-            ? $models->pluck($models->first()->getScoutKeyName())
-            : $models->map->getScoutKey();
+    /**
+     * Delete a search index.
+     *
+     * @param  string  $name
+     * @return mixed
+     */
+    abstract public function deleteIndex($name);
 
-        $this->algolia->deleteObjects($models->first()->indexableAs(), $keys->all());
-    }
+    /**
+     * Flush all of the model's records from the engine.
+     *
+     * @param  \Illuminate\Database\Eloquent\Model  $model
+     * @return void
+     */
+    abstract public function flush($model);
+
+    /**
+     * Perform the given search on the engine.
+     *
+     * @param  \Laravel\Scout\Builder  $builder
+     * @param  array  $options
+     * @return mixed
+     */
+    abstract protected function performSearch(Builder $builder, array $options = []);
 
     /**
      * Perform the given search on the engine.
@@ -125,35 +115,6 @@ public function paginate(Builder $builder, $perPage, $page)
         ]);
     }
 
-    /**
-     * Perform the given search on the engine.
-     *
-     * @param  \Laravel\Scout\Builder  $builder
-     * @param  array  $options
-     * @return mixed
-     */
-    protected function performSearch(Builder $builder, array $options = [])
-    {
-        $options = array_merge($builder->options, $options);
-
-        if ($builder->callback) {
-            return call_user_func(
-                $builder->callback,
-                $this->algolia,
-                $builder->query,
-                $options
-            );
-        }
-
-        $queryParams = ['query' => $builder->query];
-
-        return $this->algolia->searchSingleIndex(
-            $builder->index ?: $builder->model->searchableAs(),
-            $queryParams,
-            $options
-        );
-    }
-
     /**
      * Get the filter array for the query.
      *
@@ -272,17 +233,6 @@ public function getTotalCount($results)
         return $results['nbHits'];
     }
 
-    /**
-     * Flush all of the model's records from the engine.
-     *
-     * @param  \Illuminate\Database\Eloquent\Model  $model
-     * @return void
-     */
-    public function flush($model)
-    {
-        $this->algolia->clearObjects($model->indexableAs());
-    }
-
     /**
      * Create a search index.
      *
@@ -297,17 +247,6 @@ public function createIndex($name, array $options = [])
         throw new Exception('Algolia indexes are created automatically upon adding objects.');
     }
 
-    /**
-     * Delete a search index.
-     *
-     * @param  string  $name
-     * @return mixed
-     */
-    public function deleteIndex($name)
-    {
-        return $this->algolia->deleteIndex($name);
-    }
-
     /**
      * Determine if the given model uses soft deletes.
      *
diff --git a/tests/Unit/Algolia3EngineTest.php b/tests/Unit/Algolia3EngineTest.php
new file mode 100644
index 00000000..2b486bed
--- /dev/null
+++ b/tests/Unit/Algolia3EngineTest.php
@@ -0,0 +1,327 @@
+<?php
+
+namespace Laravel\Scout\Tests\Unit;
+
+use Algolia\AlgoliaSearch\SearchClient;
+use Illuminate\Container\Container;
+use Illuminate\Database\Eloquent\Collection;
+use Illuminate\Support\Facades\Config;
+use Illuminate\Support\LazyCollection;
+use Laravel\Scout\Builder;
+use Laravel\Scout\EngineManager;
+use Laravel\Scout\Engines\Algolia3Engine;
+use Laravel\Scout\Jobs\RemoveFromSearch;
+use Laravel\Scout\Tests\Fixtures\EmptySearchableModel;
+use Laravel\Scout\Tests\Fixtures\SearchableModel;
+use Laravel\Scout\Tests\Fixtures\SoftDeletedEmptySearchableModel;
+use Mockery as m;
+use PHPUnit\Framework\TestCase;
+use stdClass;
+
+class Algolia3EngineTest extends TestCase
+{
+    protected function setUp(): void
+    {
+        Config::shouldReceive('get')->with('scout.after_commit', m::any())->andReturn(false);
+        Config::shouldReceive('get')->with('scout.soft_delete', m::any())->andReturn(false);
+    }
+
+    protected function tearDown(): void
+    {
+        Container::getInstance()->flush();
+        m::close();
+    }
+
+    public function test_update_adds_objects_to_index()
+    {
+        $client = m::mock(SearchClient::class);
+        $client->shouldReceive('initIndex')->with('table')->andReturn($index = m::mock(stdClass::class));
+        $index->shouldReceive('saveObjects')->with([[
+            'id' => 1,
+            'objectID' => 1,
+        ]]);
+
+        $engine = new Algolia3Engine($client);
+        $engine->update(Collection::make([new SearchableModel]));
+    }
+
+    public function test_delete_removes_objects_to_index()
+    {
+        $client = m::mock(SearchClient::class);
+        $client->shouldReceive('initIndex')->with('table')->andReturn($index = m::mock(stdClass::class));
+        $index->shouldReceive('deleteObjects')->with([1]);
+
+        $engine = new Algolia3Engine($client);
+        $engine->delete(Collection::make([new SearchableModel(['id' => 1])]));
+    }
+
+    public function test_delete_removes_objects_to_index_with_a_custom_search_key()
+    {
+        $client = m::mock(SearchClient::class);
+        $client->shouldReceive('initIndex')->with('table')->andReturn($index = m::mock(Indexes::class));
+        $index->shouldReceive('deleteObjects')->once()->with(['my-algolia-key.5']);
+
+        $engine = new Algolia3Engine($client);
+        $engine->delete(Collection::make([new Algolia3CustomKeySearchableModel(['id' => 5])]));
+    }
+
+    public function test_delete_with_removeable_scout_collection_using_custom_search_key()
+    {
+        $job = new RemoveFromSearch(Collection::make([
+            new Algolia3CustomKeySearchableModel(['id' => 5]),
+        ]));
+
+        $job = unserialize(serialize($job));
+
+        $client = m::mock(SearchClient::class);
+        $client->shouldReceive('initIndex')->with('table')->andReturn($index = m::mock(stdClass::class));
+        $index->shouldReceive('deleteObjects')->once()->with(['my-algolia-key.5']);
+
+        $engine = new Algolia3Engine($client);
+        $engine->delete($job->models);
+    }
+
+    public function test_remove_from_search_job_uses_custom_search_key()
+    {
+        $job = new RemoveFromSearch(Collection::make([
+            new Algolia3CustomKeySearchableModel(['id' => 5]),
+        ]));
+
+        $job = unserialize(serialize($job));
+
+        Container::getInstance()->bind(EngineManager::class, function () {
+            $engine = m::mock(Algolia3Engine::class);
+
+            $engine->shouldReceive('delete')->once()->with(m::on(function ($collection) {
+                $keyName = ($model = $collection->first())->getScoutKeyName();
+
+                return $model->getAttributes()[$keyName] === 'my-algolia-key.5';
+            }));
+
+            $manager = m::mock(EngineManager::class);
+
+            $manager->shouldReceive('engine')->andReturn($engine);
+
+            return $manager;
+        });
+
+        $job->handle();
+    }
+
+    public function test_search_sends_correct_parameters_to_algolia()
+    {
+        $client = m::mock(SearchClient::class);
+        $client->shouldReceive('initIndex')->with('table')->andReturn($index = m::mock(stdClass::class));
+        $index->shouldReceive('search')->with('zonda', [
+            'numericFilters' => ['foo=1'],
+        ]);
+
+        $engine = new Algolia3Engine($client);
+        $builder = new Builder(new SearchableModel, 'zonda');
+        $builder->where('foo', 1);
+        $engine->search($builder);
+    }
+
+    public function test_search_sends_correct_parameters_to_algolia_for_where_in_search()
+    {
+        $client = m::mock(SearchClient::class);
+        $client->shouldReceive('initIndex')->with('table')->andReturn($index = m::mock(stdClass::class));
+        $index->shouldReceive('search')->with('zonda', [
+            'numericFilters' => ['foo=1', ['bar=1', 'bar=2']],
+        ]);
+
+        $engine = new Algolia3Engine($client);
+        $builder = new Builder(new SearchableModel, 'zonda');
+        $builder->where('foo', 1)->whereIn('bar', [1, 2]);
+        $engine->search($builder);
+    }
+
+    public function test_search_sends_correct_parameters_to_algolia_for_empty_where_in_search()
+    {
+        $client = m::mock(SearchClient::class);
+        $client->shouldReceive('initIndex')->with('table')->andReturn($index = m::mock(stdClass::class));
+        $index->shouldReceive('search')->with('zonda', [
+            'numericFilters' => ['foo=1', '0=1'],
+        ]);
+
+        $engine = new Algolia3Engine($client);
+        $builder = new Builder(new SearchableModel, 'zonda');
+        $builder->where('foo', 1)->whereIn('bar', []);
+        $engine->search($builder);
+    }
+
+    public function test_map_correctly_maps_results_to_models()
+    {
+        $client = m::mock(SearchClient::class);
+        $engine = new Algolia3Engine($client);
+
+        $model = m::mock(stdClass::class);
+
+        $model->shouldReceive('getScoutModelsByIds')->andReturn($models = Collection::make([
+            new SearchableModel(['id' => 1, 'name' => 'test']),
+        ]));
+
+        $builder = m::mock(Builder::class);
+
+        $results = $engine->map($builder, [
+            'nbHits' => 1,
+            'hits' => [
+                ['objectID' => 1, 'id' => 1, '_rankingInfo' => ['nbTypos' => 0]],
+            ],
+        ], $model);
+
+        $this->assertCount(1, $results);
+        $this->assertEquals(['id' => 1, 'name' => 'test'], $results->first()->toArray());
+        $this->assertEquals(['_rankingInfo' => ['nbTypos' => 0]], $results->first()->scoutMetaData());
+    }
+
+    public function test_map_method_respects_order()
+    {
+        $client = m::mock(SearchClient::class);
+        $engine = new Algolia3Engine($client);
+
+        $model = m::mock(stdClass::class);
+        $model->shouldReceive('getScoutModelsByIds')->andReturn($models = Collection::make([
+            new SearchableModel(['id' => 1]),
+            new SearchableModel(['id' => 2]),
+            new SearchableModel(['id' => 3]),
+            new SearchableModel(['id' => 4]),
+        ]));
+
+        $builder = m::mock(Builder::class);
+
+        $results = $engine->map($builder, ['nbHits' => 4, 'hits' => [
+            ['objectID' => 1, 'id' => 1],
+            ['objectID' => 2, 'id' => 2],
+            ['objectID' => 4, 'id' => 4],
+            ['objectID' => 3, 'id' => 3],
+        ]], $model);
+
+        $this->assertCount(4, $results);
+
+        // It's important we assert with array keys to ensure
+        // they have been reset after sorting.
+        $this->assertEquals([
+            0 => ['id' => 1],
+            1 => ['id' => 2],
+            2 => ['id' => 4],
+            3 => ['id' => 3],
+        ], $results->toArray());
+    }
+
+    public function test_lazy_map_correctly_maps_results_to_models()
+    {
+        $client = m::mock(SearchClient::class);
+        $engine = new Algolia3Engine($client);
+
+        $model = m::mock(stdClass::class);
+        $model->shouldReceive('queryScoutModelsByIds->cursor')->andReturn($models = LazyCollection::make([
+            new SearchableModel(['id' => 1, 'name' => 'test']),
+        ]));
+
+        $builder = m::mock(Builder::class);
+
+        $results = $engine->lazyMap($builder, ['nbHits' => 1, 'hits' => [
+            ['objectID' => 1, 'id' => 1, '_rankingInfo' => ['nbTypos' => 0]],
+        ]], $model);
+
+        $this->assertCount(1, $results);
+        $this->assertEquals(['id' => 1, 'name' => 'test'], $results->first()->toArray());
+        $this->assertEquals(['_rankingInfo' => ['nbTypos' => 0]], $results->first()->scoutMetaData());
+    }
+
+    public function test_lazy_map_method_respects_order()
+    {
+        $client = m::mock(SearchClient::class);
+        $engine = new Algolia3Engine($client);
+
+        $model = m::mock(stdClass::class);
+        $model->shouldReceive('queryScoutModelsByIds->cursor')->andReturn($models = LazyCollection::make([
+            new SearchableModel(['id' => 1]),
+            new SearchableModel(['id' => 2]),
+            new SearchableModel(['id' => 3]),
+            new SearchableModel(['id' => 4]),
+        ]));
+
+        $builder = m::mock(Builder::class);
+
+        $results = $engine->lazyMap($builder, ['nbHits' => 4, 'hits' => [
+            ['objectID' => 1, 'id' => 1],
+            ['objectID' => 2, 'id' => 2],
+            ['objectID' => 4, 'id' => 4],
+            ['objectID' => 3, 'id' => 3],
+        ]], $model);
+
+        $this->assertCount(4, $results);
+
+        // It's important we assert with array keys to ensure
+        // they have been reset after sorting.
+        $this->assertEquals([
+            0 => ['id' => 1],
+            1 => ['id' => 2],
+            2 => ['id' => 4],
+            3 => ['id' => 3],
+        ], $results->toArray());
+    }
+
+    public function test_a_model_is_indexed_with_a_custom_algolia_key()
+    {
+        $client = m::mock(SearchClient::class);
+        $client->shouldReceive('initIndex')->with('table')->andReturn($index = m::mock(stdClass::class));
+        $index->shouldReceive('saveObjects')->with([[
+            'id' => 1,
+            'objectID' => 'my-algolia-key.1',
+        ]]);
+
+        $engine = new Algolia3Engine($client);
+        $engine->update(Collection::make([new Algolia3CustomKeySearchableModel]));
+    }
+
+    public function test_a_model_is_removed_with_a_custom_algolia_key()
+    {
+        $client = m::mock(SearchClient::class);
+        $client->shouldReceive('initIndex')->with('table')->andReturn($index = m::mock(stdClass::class));
+        $index->shouldReceive('deleteObjects')->with(['my-algolia-key.1']);
+
+        $engine = new Algolia3Engine($client);
+        $engine->delete(Collection::make([new Algolia3CustomKeySearchableModel(['id' => 1])]));
+    }
+
+    public function test_flush_a_model_with_a_custom_algolia_key()
+    {
+        $client = m::mock(SearchClient::class);
+        $client->shouldReceive('initIndex')->with('table')->andReturn($index = m::mock(stdClass::class));
+        $index->shouldReceive('clearObjects');
+
+        $engine = new Algolia3Engine($client);
+        $engine->flush(new Algolia3CustomKeySearchableModel);
+    }
+
+    public function test_update_empty_searchable_array_does_not_add_objects_to_index()
+    {
+        $client = m::mock(SearchClient::class);
+        $client->shouldReceive('initIndex')->with('table')->andReturn($index = m::mock(stdClass::class));
+        $index->shouldNotReceive('saveObjects');
+
+        $engine = new Algolia3Engine($client);
+        $engine->update(Collection::make([new EmptySearchableModel]));
+    }
+
+    public function test_update_empty_searchable_array_from_soft_deleted_model_does_not_add_objects_to_index()
+    {
+        $client = m::mock(SearchClient::class);
+        $client->shouldReceive('initIndex')->with('table')->andReturn($index = m::mock('StdClass'));
+        $index->shouldNotReceive('saveObjects');
+
+        $engine = new Algolia3Engine($client, true);
+        $engine->update(Collection::make([new SoftDeletedEmptySearchableModel]));
+    }
+}
+
+class Algolia3CustomKeySearchableModel extends SearchableModel
+{
+    public function getScoutKey()
+    {
+        return 'my-algolia-key.'.$this->getKey();
+    }
+}
diff --git a/tests/Unit/AlgoliaEngineTest.php b/tests/Unit/Algolia4EngineTest.php
similarity index 87%
rename from tests/Unit/AlgoliaEngineTest.php
rename to tests/Unit/Algolia4EngineTest.php
index 06177c6a..49da1075 100644
--- a/tests/Unit/AlgoliaEngineTest.php
+++ b/tests/Unit/Algolia4EngineTest.php
@@ -9,7 +9,7 @@
 use Illuminate\Support\LazyCollection;
 use Laravel\Scout\Builder;
 use Laravel\Scout\EngineManager;
-use Laravel\Scout\Engines\AlgoliaEngine;
+use Laravel\Scout\Engines\Algolia4Engine;
 use Laravel\Scout\Jobs\RemoveFromSearch;
 use Laravel\Scout\Tests\Fixtures\EmptySearchableModel;
 use Laravel\Scout\Tests\Fixtures\SearchableModel;
@@ -18,7 +18,7 @@
 use PHPUnit\Framework\TestCase;
 use stdClass;
 
-class AlgoliaEngineTest extends TestCase
+class Algolia4EngineTest extends TestCase
 {
     protected function setUp(): void
     {
@@ -41,7 +41,7 @@ public function test_update_adds_objects_to_index()
             'objectID' => 1,
         ]]);
 
-        $engine = new AlgoliaEngine($client);
+        $engine = new Algolia4Engine($client);
         $engine->update(Collection::make([new SearchableModel]));
     }
 
@@ -50,7 +50,7 @@ public function test_delete_removes_objects_to_index()
         $client = m::mock(SearchClient::class);
         $client->shouldReceive('deleteObjects')->with('table', [1]);
 
-        $engine = new AlgoliaEngine($client);
+        $engine = new Algolia4Engine($client);
         $engine->delete(Collection::make([new SearchableModel(['id' => 1])]));
     }
 
@@ -59,14 +59,14 @@ public function test_delete_removes_objects_to_index_with_a_custom_search_key()
         $client = m::mock(SearchClient::class);
         $client->shouldReceive('deleteObjects')->once()->with('table', ['my-algolia-key.5']);
 
-        $engine = new AlgoliaEngine($client);
-        $engine->delete(Collection::make([new AlgoliaCustomKeySearchableModel(['id' => 5])]));
+        $engine = new Algolia4Engine($client);
+        $engine->delete(Collection::make([new Algolia4CustomKeySearchableModel(['id' => 5])]));
     }
 
     public function test_delete_with_removeable_scout_collection_using_custom_search_key()
     {
         $job = new RemoveFromSearch(Collection::make([
-            new AlgoliaCustomKeySearchableModel(['id' => 5]),
+            new Algolia4CustomKeySearchableModel(['id' => 5]),
         ]));
 
         $job = unserialize(serialize($job));
@@ -74,20 +74,20 @@ public function test_delete_with_removeable_scout_collection_using_custom_search
         $client = m::mock(SearchClient::class);
         $client->shouldReceive('deleteObjects')->once()->with('table', ['my-algolia-key.5']);
 
-        $engine = new AlgoliaEngine($client);
+        $engine = new Algolia4Engine($client);
         $engine->delete($job->models);
     }
 
     public function test_remove_from_search_job_uses_custom_search_key()
     {
         $job = new RemoveFromSearch(Collection::make([
-            new AlgoliaCustomKeySearchableModel(['id' => 5]),
+            new Algolia4CustomKeySearchableModel(['id' => 5]),
         ]));
 
         $job = unserialize(serialize($job));
 
         Container::getInstance()->bind(EngineManager::class, function () {
-            $engine = m::mock(AlgoliaEngine::class);
+            $engine = m::mock(Algolia4Engine::class);
 
             $engine->shouldReceive('delete')->once()->with(m::on(function ($collection) {
                 $keyName = ($model = $collection->first())->getScoutKeyName();
@@ -114,7 +114,7 @@ public function test_search_sends_correct_parameters_to_algolia()
             ['numericFilters' => ['foo=1']]
         );
 
-        $engine = new AlgoliaEngine($client);
+        $engine = new Algolia4Engine($client);
         $builder = new Builder(new SearchableModel, 'zonda');
         $builder->where('foo', 1);
         $engine->search($builder);
@@ -129,7 +129,7 @@ public function test_search_sends_correct_parameters_to_algolia_for_where_in_sea
             ['numericFilters' => ['foo=1', ['bar=1', 'bar=2']]]
         );
 
-        $engine = new AlgoliaEngine($client);
+        $engine = new Algolia4Engine($client);
         $builder = new Builder(new SearchableModel, 'zonda');
         $builder->where('foo', 1)->whereIn('bar', [1, 2]);
         $engine->search($builder);
@@ -144,7 +144,7 @@ public function test_search_sends_correct_parameters_to_algolia_for_empty_where_
             ['numericFilters' => ['foo=1', '0=1']]
         );
 
-        $engine = new AlgoliaEngine($client);
+        $engine = new Algolia4Engine($client);
         $builder = new Builder(new SearchableModel, 'zonda');
         $builder->where('foo', 1)->whereIn('bar', []);
         $engine->search($builder);
@@ -153,7 +153,7 @@ public function test_search_sends_correct_parameters_to_algolia_for_empty_where_
     public function test_map_correctly_maps_results_to_models()
     {
         $client = m::mock(SearchClient::class);
-        $engine = new AlgoliaEngine($client);
+        $engine = new Algolia4Engine($client);
 
         $model = m::mock(stdClass::class);
 
@@ -178,7 +178,7 @@ public function test_map_correctly_maps_results_to_models()
     public function test_map_method_respects_order()
     {
         $client = m::mock(SearchClient::class);
-        $engine = new AlgoliaEngine($client);
+        $engine = new Algolia4Engine($client);
 
         $model = m::mock(stdClass::class);
         $model->shouldReceive('getScoutModelsByIds')->andReturn($models = Collection::make([
@@ -212,7 +212,7 @@ public function test_map_method_respects_order()
     public function test_lazy_map_correctly_maps_results_to_models()
     {
         $client = m::mock(SearchClient::class);
-        $engine = new AlgoliaEngine($client);
+        $engine = new Algolia4Engine($client);
 
         $model = m::mock(stdClass::class);
         $model->shouldReceive('queryScoutModelsByIds->cursor')->andReturn($models = LazyCollection::make([
@@ -233,7 +233,7 @@ public function test_lazy_map_correctly_maps_results_to_models()
     public function test_lazy_map_method_respects_order()
     {
         $client = m::mock(SearchClient::class);
-        $engine = new AlgoliaEngine($client);
+        $engine = new Algolia4Engine($client);
 
         $model = m::mock(stdClass::class);
         $model->shouldReceive('queryScoutModelsByIds->cursor')->andReturn($models = LazyCollection::make([
@@ -273,8 +273,8 @@ public function test_a_model_is_indexed_with_a_custom_algolia_key()
             'objectID' => 'my-algolia-key.1',
         ]]);
 
-        $engine = new AlgoliaEngine($client);
-        $engine->update(Collection::make([new AlgoliaCustomKeySearchableModel]));
+        $engine = new Algolia4Engine($client);
+        $engine->update(Collection::make([new Algolia4CustomKeySearchableModel]));
     }
 
     public function test_a_model_is_removed_with_a_custom_algolia_key()
@@ -282,8 +282,8 @@ public function test_a_model_is_removed_with_a_custom_algolia_key()
         $client = m::mock(SearchClient::class);
         $client->shouldReceive('deleteObjects')->with('table', ['my-algolia-key.1']);
 
-        $engine = new AlgoliaEngine($client);
-        $engine->delete(Collection::make([new AlgoliaCustomKeySearchableModel(['id' => 1])]));
+        $engine = new Algolia4Engine($client);
+        $engine->delete(Collection::make([new Algolia4CustomKeySearchableModel(['id' => 1])]));
     }
 
     public function test_flush_a_model_with_a_custom_algolia_key()
@@ -291,8 +291,8 @@ public function test_flush_a_model_with_a_custom_algolia_key()
         $client = m::mock(SearchClient::class);
         $client->shouldReceive('clearObjects')->with('table');
 
-        $engine = new AlgoliaEngine($client);
-        $engine->flush(new AlgoliaCustomKeySearchableModel);
+        $engine = new Algolia4Engine($client);
+        $engine->flush(new Algolia4CustomKeySearchableModel);
     }
 
     public function test_update_empty_searchable_array_does_not_add_objects_to_index()
@@ -301,7 +301,7 @@ public function test_update_empty_searchable_array_does_not_add_objects_to_index
         $client->shouldReceive('initIndex')->with('table')->andReturn($index = m::mock(stdClass::class));
         $index->shouldNotReceive('saveObjects');
 
-        $engine = new AlgoliaEngine($client);
+        $engine = new Algolia4Engine($client);
         $engine->update(Collection::make([new EmptySearchableModel]));
     }
 
@@ -311,12 +311,12 @@ public function test_update_empty_searchable_array_from_soft_deleted_model_does_
         $client->shouldReceive('initIndex')->with('table')->andReturn($index = m::mock('StdClass'));
         $index->shouldNotReceive('saveObjects');
 
-        $engine = new AlgoliaEngine($client, true);
+        $engine = new Algolia4Engine($client, true);
         $engine->update(Collection::make([new SoftDeletedEmptySearchableModel]));
     }
 }
 
-class AlgoliaCustomKeySearchableModel extends SearchableModel
+class Algolia4CustomKeySearchableModel extends SearchableModel
 {
     public function getScoutKey()
     {

From b2568745ee7479e0ce1a4e2d108a2e2b36b304fe Mon Sep 17 00:00:00 2001
From: Mior Muhammad Zaki <crynobone@gmail.com>
Date: Fri, 8 Nov 2024 15:15:07 +0800
Subject: [PATCH 18/24] wip

Signed-off-by: Mior Muhammad Zaki <crynobone@gmail.com>
---
 src/EngineManager.php | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/src/EngineManager.php b/src/EngineManager.php
index b739a9b0..897a6a0f 100644
--- a/src/EngineManager.php
+++ b/src/EngineManager.php
@@ -31,7 +31,7 @@ public function engine($name = null)
     }
 
     /**
-     * Create a Meilisearch engine instance.
+     * Create an Algolia engine instance.
      *
      * @return \Laravel\Scout\Engines\AlgoliaEngine
      */
@@ -45,7 +45,7 @@ public function createAlgoliaDriver()
     }
 
     /**
-     * Create an Algolia v4 engine instance.
+     * Create an Algolia v3 engine instance.
      *
      * @return \Laravel\Scout\Engines\Algolia3Engine
      */

From 97f40dd9dc0b8c85ef0e3057f557b0f94232684f Mon Sep 17 00:00:00 2001
From: Mior Muhammad Zaki <crynobone@gmail.com>
Date: Fri, 8 Nov 2024 15:16:22 +0800
Subject: [PATCH 19/24] wip

Signed-off-by: Mior Muhammad Zaki <crynobone@gmail.com>
---
 composer.json         | 3 +++
 src/EngineManager.php | 4 ----
 2 files changed, 3 insertions(+), 4 deletions(-)

diff --git a/composer.json b/composer.json
index a86a6d4e..bcc2cec0 100644
--- a/composer.json
+++ b/composer.json
@@ -34,6 +34,9 @@
         "phpstan/phpstan": "^1.10",
         "phpunit/phpunit": "^9.3|^10.4"
     },
+    "conflict": {
+        "algolia/algoliasearch-client-php": "<3.2.0|>=5.0.0"
+    },
     "autoload": {
         "psr-4": {
             "Laravel\\Scout\\": "src/"
diff --git a/src/EngineManager.php b/src/EngineManager.php
index 897a6a0f..711067a0 100644
--- a/src/EngineManager.php
+++ b/src/EngineManager.php
@@ -104,10 +104,6 @@ protected function ensureAlgoliaClientIsInstalled()
             return;
         }
 
-        if (class_exists('AlgoliaSearch\Client')) {
-            throw new Exception('Please upgrade your Algolia client to version: ^3.2|^4.0.');
-        }
-
         throw new Exception('Please install the suggested Algolia client: algolia/algoliasearch-client-php.');
     }
 

From e77103decd99e9f09fbdcc7289f2e4808a02ad1f Mon Sep 17 00:00:00 2001
From: Mior Muhammad Zaki <crynobone@gmail.com>
Date: Fri, 8 Nov 2024 15:24:28 +0800
Subject: [PATCH 20/24] wip

Signed-off-by: Mior Muhammad Zaki <crynobone@gmail.com>
---
 phpstan.src.neon.dist          |  4 ++++
 src/EngineManager.php          | 38 +++++++++++-----------------------
 src/Engines/Algolia3Engine.php |  7 +++----
 src/Engines/Algolia4Engine.php |  7 +++----
 4 files changed, 22 insertions(+), 34 deletions(-)

diff --git a/phpstan.src.neon.dist b/phpstan.src.neon.dist
index fe17868f..e43f2c8c 100644
--- a/phpstan.src.neon.dist
+++ b/phpstan.src.neon.dist
@@ -6,4 +6,8 @@ parameters:
   level: 0
 
   ignoreErrors:
+    - identifier: new.static
     - "#\\(void\\) is used.#"
+
+  excludePaths:
+    - src/Engines/Algolia3Engine.php
diff --git a/src/EngineManager.php b/src/EngineManager.php
index 711067a0..9e461c37 100644
--- a/src/EngineManager.php
+++ b/src/EngineManager.php
@@ -7,7 +7,8 @@
 use Algolia\AlgoliaSearch\Support\UserAgent as Algolia3UserAgent;
 use Exception;
 use Illuminate\Support\Manager;
-use Laravel\Scout\Engines\AlgoliaEngine;
+use Laravel\Scout\Engines\Algolia3Engine;
+use Laravel\Scout\Engines\Algolia4Engine;
 use Laravel\Scout\Engines\CollectionEngine;
 use Laravel\Scout\Engines\DatabaseEngine;
 use Laravel\Scout\Engines\MeilisearchEngine;
@@ -51,32 +52,13 @@ public function createAlgoliaDriver()
      */
     protected function configureAlgolia3Driver()
     {
-        Algolia3UserAgent::addCustomUserAgent('Laravel Scout', Scout::VERSION);
+        Algolia3UserAgent::addCustomUserAgent('Laravel Scout', Scout::VERSION); // @phpstan-ignore class.notFound
 
-        $config = SearchConfig::create(
-            config('scout.algolia.id'),
-            config('scout.algolia.secret')
-        )->setDefaultHeaders(
-            $this->defaultAlgoliaHeaders()
+        return Algolia3Engine::make(
+            config: config('scout.algolia'),
+            headers: $this->defaultAlgoliaHeaders(),
+            softDelete: config('scout.soft_delete')
         );
-
-        if (is_int($connectTimeout = config('scout.algolia.connect_timeout'))) {
-            $config->setConnectTimeout($connectTimeout);
-        }
-
-        if (is_int($readTimeout = config('scout.algolia.read_timeout'))) {
-            $config->setReadTimeout($readTimeout);
-        }
-
-        if (is_int($writeTimeout = config('scout.algolia.write_timeout'))) {
-            $config->setWriteTimeout($writeTimeout);
-        }
-
-        if (is_int($batchSize = config('scout.algolia.batch_size'))) {
-            $config->setBatchSize($batchSize);
-        }
-
-        return new AlgoliaEngine(Algolia::createWithConfig($config), config('scout.soft_delete'));
     }
 
     /**
@@ -88,7 +70,11 @@ protected function configureAlgolia4Driver()
     {
         Algolia4UserAgent::addAlgoliaAgent('Laravel Scout', 'Laravel Scout', Scout::VERSION);
 
-        return Algolia4Engine::make(config('scout.algolia'), config('scout.soft_delete'));
+        return Algolia4Engine::make(
+            config: config('scout.algolia'),
+            headers: $this->defaultAlgoliaHeaders(),
+            softDelete: config('scout.soft_delete')
+        );
     }
 
     /**
diff --git a/src/Engines/Algolia3Engine.php b/src/Engines/Algolia3Engine.php
index a4239fe8..4df86cc1 100644
--- a/src/Engines/Algolia3Engine.php
+++ b/src/Engines/Algolia3Engine.php
@@ -31,17 +31,16 @@ public function __construct(Algolia3SearchClient $algolia, $softDelete = false)
      * Make a new engine instance.
      *
      * @param  array  $config
+     * @param  array  $headers
      * @param  bool  $softDelete
      * @return static
      */
-    public static function make(array $config, bool $softDelete = false)
+    public static function make(array $config, array $headers, bool $softDelete = false)
     {
         $config = Algolia3SearchConfig::create([
             'appId' => $config['id'],
             'apiKey' => $config['secret'],
-        ])->setDefaultHeaders(
-            $this->defaultAlgoliaHeaders()
-        );
+        ])->setDefaultHeaders($headers);
 
         if (is_int($connectTimeout = $config['connect_timeout'])) {
             $configuration->setConnectTimeout($connectTimeout);
diff --git a/src/Engines/Algolia4Engine.php b/src/Engines/Algolia4Engine.php
index d88857c6..c1e88496 100644
--- a/src/Engines/Algolia4Engine.php
+++ b/src/Engines/Algolia4Engine.php
@@ -32,19 +32,18 @@ public function __construct(Algolia4SearchClient $algolia, $softDelete = false)
      * Make a new engine instance.
      *
      * @param  array  $config
+     * @param  array  $headers
      * @param  bool  $softDelete
      * @return static
      */
-    public static function make(array $config, bool $softDelete = false)
+    public static function make(array $config, array $headers, bool $softDelete = false)
     {
         $configuration = (new Algolia4SearchConfig(array_merge([
             'appId' => $config['id'],
             'apiKey' => $config['secret'],
         ]), array_filter([
             'batchSize' => $config['batch_size'],
-        ])))->setDefaultHeaders(
-            $this->defaultAlgoliaHeaders()
-        );
+        ])))->setDefaultHeaders($headers);
 
         if (is_int($connectTimeout = $config['connect_timeout'])) {
             $configuration->setConnectTimeout($connectTimeout);

From bedd3b79f9231af9286bdd4548f24ffafd217628 Mon Sep 17 00:00:00 2001
From: Mior Muhammad Zaki <crynobone@gmail.com>
Date: Fri, 8 Nov 2024 15:25:43 +0800
Subject: [PATCH 21/24] wip

Signed-off-by: Mior Muhammad Zaki <crynobone@gmail.com>
---
 src/Engines/AlgoliaEngine.php | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/src/Engines/AlgoliaEngine.php b/src/Engines/AlgoliaEngine.php
index 91d47b56..11777fd3 100644
--- a/src/Engines/AlgoliaEngine.php
+++ b/src/Engines/AlgoliaEngine.php
@@ -26,7 +26,7 @@ abstract class AlgoliaEngine extends Engine
      *
      * @var bool
      */
-    protected $softDelete = false;
+    protected $softDelete;
 
     /**
      * Create a new engine instance.

From 7265a567ed03584b69f0b7c4e4104127b524686c Mon Sep 17 00:00:00 2001
From: Mior Muhammad Zaki <crynobone@gmail.com>
Date: Fri, 8 Nov 2024 15:28:17 +0800
Subject: [PATCH 22/24] wip

Signed-off-by: Mior Muhammad Zaki <crynobone@gmail.com>
---
 src/Engines/Algolia3Engine.php | 3 ---
 src/Engines/Algolia4Engine.php | 4 ----
 src/Engines/AlgoliaEngine.php  | 1 -
 3 files changed, 8 deletions(-)

diff --git a/src/Engines/Algolia3Engine.php b/src/Engines/Algolia3Engine.php
index 4df86cc1..3710ca4f 100644
--- a/src/Engines/Algolia3Engine.php
+++ b/src/Engines/Algolia3Engine.php
@@ -4,9 +4,6 @@
 
 use Algolia\AlgoliaSearch\Config\SearchConfig as Algolia3SearchConfig;
 use Algolia\AlgoliaSearch\SearchClient as Algolia3SearchClient;
-use Exception;
-use Illuminate\Database\Eloquent\SoftDeletes;
-use Illuminate\Support\LazyCollection;
 use Laravel\Scout\Builder;
 use Laravel\Scout\Jobs\RemoveableScoutCollection;
 
diff --git a/src/Engines/Algolia4Engine.php b/src/Engines/Algolia4Engine.php
index c1e88496..0cb08e1f 100644
--- a/src/Engines/Algolia4Engine.php
+++ b/src/Engines/Algolia4Engine.php
@@ -4,10 +4,6 @@
 
 use Algolia\AlgoliaSearch\Api\SearchClient as Algolia4SearchClient;
 use Algolia\AlgoliaSearch\Configuration\SearchConfig as Algolia4SearchConfig;
-use Algolia\AlgoliaSearch\Support\AlgoliaAgent as Algolia4UserAgent;
-use Exception;
-use Illuminate\Database\Eloquent\SoftDeletes;
-use Illuminate\Support\LazyCollection;
 use Laravel\Scout\Builder;
 use Laravel\Scout\Jobs\RemoveableScoutCollection;
 
diff --git a/src/Engines/AlgoliaEngine.php b/src/Engines/AlgoliaEngine.php
index 11777fd3..73cb2463 100644
--- a/src/Engines/AlgoliaEngine.php
+++ b/src/Engines/AlgoliaEngine.php
@@ -7,7 +7,6 @@
 use Illuminate\Database\Eloquent\SoftDeletes;
 use Illuminate\Support\LazyCollection;
 use Laravel\Scout\Builder;
-use Laravel\Scout\Jobs\RemoveableScoutCollection;
 
 /**
  * @template TAlgoliaClient of object

From 9723fc9c34042582a7082d450cf6a5d2f0359e8e Mon Sep 17 00:00:00 2001
From: Mior Muhammad Zaki <crynobone@gmail.com>
Date: Fri, 8 Nov 2024 15:30:13 +0800
Subject: [PATCH 23/24] wip

Signed-off-by: Mior Muhammad Zaki <crynobone@gmail.com>
---
 src/Engines/AlgoliaEngine.php | 1 -
 1 file changed, 1 deletion(-)

diff --git a/src/Engines/AlgoliaEngine.php b/src/Engines/AlgoliaEngine.php
index 73cb2463..5990adda 100644
--- a/src/Engines/AlgoliaEngine.php
+++ b/src/Engines/AlgoliaEngine.php
@@ -2,7 +2,6 @@
 
 namespace Laravel\Scout\Engines;
 
-use Algolia\AlgoliaSearch\Api\SearchClient as Algolia;
 use Exception;
 use Illuminate\Database\Eloquent\SoftDeletes;
 use Illuminate\Support\LazyCollection;

From 50665af7bf2da66c80749e5ba24ad3cfa852f89a Mon Sep 17 00:00:00 2001
From: Taylor Otwell <taylor@laravel.com>
Date: Mon, 11 Nov 2024 15:07:26 -0600
Subject: [PATCH 24/24] Update AlgoliaEngine.php

---
 src/Engines/AlgoliaEngine.php | 18 +++++++++---------
 1 file changed, 9 insertions(+), 9 deletions(-)

diff --git a/src/Engines/AlgoliaEngine.php b/src/Engines/AlgoliaEngine.php
index 5990adda..23d2f7cc 100644
--- a/src/Engines/AlgoliaEngine.php
+++ b/src/Engines/AlgoliaEngine.php
@@ -39,6 +39,15 @@ public function __construct($algolia, $softDelete = false)
         $this->softDelete = $softDelete;
     }
 
+    /**
+     * Perform the given search on the engine.
+     *
+     * @param  \Laravel\Scout\Builder  $builder
+     * @param  array  $options
+     * @return mixed
+     */
+    abstract protected function performSearch(Builder $builder, array $options = []);
+
     /**
      * Update the given model in the index.
      *
@@ -73,15 +82,6 @@ abstract public function deleteIndex($name);
      */
     abstract public function flush($model);
 
-    /**
-     * Perform the given search on the engine.
-     *
-     * @param  \Laravel\Scout\Builder  $builder
-     * @param  array  $options
-     * @return mixed
-     */
-    abstract protected function performSearch(Builder $builder, array $options = []);
-
     /**
      * Perform the given search on the engine.
      *