diff --git a/src/Search/Comb/Index.php b/src/Search/Comb/Index.php index baeeeb1d4ea..3143082b915 100644 --- a/src/Search/Comb/Index.php +++ b/src/Search/Comb/Index.php @@ -3,6 +3,7 @@ namespace Statamic\Search\Comb; use Closure; +use Illuminate\Support\Collection; use Illuminate\Support\Facades\Cache; use Statamic\Facades\File; use Statamic\Search\Comb\Exceptions\NoQuery; @@ -16,6 +17,8 @@ class Index extends BaseIndex { + protected ?Collection $pending = null; + public function search($query) { return (new Query($this))->query($query); @@ -111,6 +114,12 @@ public function delete($document) public function insertDocuments(Documents $documents) { + if (! is_null($this->pending)) { + $this->pending = $documents->union($this->pending); + + return; + } + $this->locked(function () use ($documents) { try { $data = $this->data(); @@ -122,6 +131,23 @@ public function insertDocuments(Documents $documents) }); } + public function update() + { + $this->pending = collect(); + + try { + parent::update(); + } finally { + [$pending, $this->pending] = [$this->pending, null]; + } + + if ($pending->isNotEmpty()) { + $this->locked(fn () => $this->save($pending)); + } + + return $this; + } + public function deleteIndex() { File::delete($this->path()); diff --git a/tests/Search/CombIndexTest.php b/tests/Search/CombIndexTest.php index e5c23cc8365..a1b21db96c2 100644 --- a/tests/Search/CombIndexTest.php +++ b/tests/Search/CombIndexTest.php @@ -2,14 +2,20 @@ namespace Tests\Search; +use Facades\Tests\Factories\EntryFactory; use Illuminate\Contracts\Cache\Lock; use Illuminate\Contracts\Filesystem\Filesystem; +use Illuminate\Filesystem\Filesystem as Files; +use Illuminate\Support\Facades\Bus; use Illuminate\Support\Facades\Cache; use Mockery; use PHPUnit\Framework\Attributes\Test; use Statamic\Contracts\Search\Searchable; +use Statamic\Facades\Collection; +use Statamic\Facades\Search; use Statamic\Search\Comb\Index; use Statamic\Search\Documents; +use Tests\PreventSavingStacheItemsToDisk; use Tests\TestCase; class CombIndexTest extends TestCase @@ -17,6 +23,7 @@ class CombIndexTest extends TestCase use IndexTests { tearDown as protected indexTestsTearDown; } + use PreventSavingStacheItemsToDisk; private $fs; @@ -117,6 +124,63 @@ public function delete_rewrites_the_index_when_the_reference_is_present() ); } + #[Test] + public function updating_on_a_sync_queue_writes_the_index_file_once() + { + $files = $this->prepareUpdate(250); + + Search::index('test')->update(); + + $files->shouldHaveReceived('put')->once(); + $this->assertCount(250, json_decode(file_get_contents($this->tmpDir.'/test.json'), true)); + } + + #[Test] + public function updating_on_a_faked_bus_deletes_the_index_and_writes_nothing() + { + Bus::fake(); + $files = $this->prepareUpdate(3); + + Search::index('test')->update(); + + $files->shouldNotHaveReceived('put'); + $this->assertFileDoesNotExist($this->tmpDir.'/test.json'); + } + + #[Test] + public function inserting_outside_an_update_writes_immediately() + { + $files = $this->prepareUpdate(0); + + Search::index('test')->insertDocuments(new Documents(['entry::a' => ['title' => 'Foo']])); + Search::index('test')->insertDocuments(new Documents(['entry::b' => ['title' => 'Bar']])); + + $files->shouldHaveReceived('put')->twice(); + $this->assertCount(3, json_decode(file_get_contents($this->tmpDir.'/test.json'), true)); + } + + private function prepareUpdate(int $entries) + { + $this->createIndexFile(['entry::stale' => ['title' => 'Stale']]); + + config([ + 'statamic.search.chunk_size' => 100, + 'statamic.search.drivers.local.path' => $this->tmpDir, + 'statamic.search.indexes.test' => ['driver' => 'local', 'searchables' => 'collection:blog', 'fields' => ['title']], + ]); + + Collection::make('blog')->save(); + + for ($i = 1; $i <= $entries; $i++) { + EntryFactory::collection('blog')->id("entry-$i")->slug("entry-$i")->data(['title' => "Entry $i"])->create(); + } + + $files = Mockery::mock(Files::class)->makePartial(); + $this->instance('files', $files); + + return $files; + } + #[Test] public function inserting_holds_the_index_lock_across_the_read_and_the_write() {