Skip to content

Commit

Permalink
Orders Staches indexes; uses index order to optimize some sorts
Browse files Browse the repository at this point in the history
  • Loading branch information
JohnathonKoster committed Mar 9, 2024
1 parent 9770ba2 commit 6dc2217
Show file tree
Hide file tree
Showing 12 changed files with 119 additions and 29 deletions.
2 changes: 1 addition & 1 deletion src/Sites/Site.php
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ public function name()

public function locale()
{
return $this->config['locale'];
return $this->config['locale'] ?? null;
}

public function shortLocale()
Expand Down
13 changes: 13 additions & 0 deletions src/Stache/Indexes/Index.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
namespace Statamic\Stache\Indexes;

use Illuminate\Support\Facades\Cache;
use Statamic\Facades\Compare;
use Statamic\Facades\Stache;
use Statamic\Statamic;

Expand Down Expand Up @@ -104,8 +105,20 @@ public function isCached()
return Cache::has($this->cacheKey());
}

protected function orderIndex()
{
// Order all indexes in ascending order. If we want
// a descending sort later, we can reverse the
// keys after doing our index comparisons.
uasort($this->items, function ($a, $b) {
return Compare::values($a, $b);
});
}

public function cache()
{
$this->orderIndex();

Cache::forever($this->cacheKey(), $this->items);
}

Expand Down
59 changes: 59 additions & 0 deletions src/Stache/Query/Builder.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

use Statamic\Data\DataCollection;
use Statamic\Query\Builder as BaseBuilder;
use Statamic\Stache\Stores\AggregateStore;
use Statamic\Stache\Stores\Store;

abstract class Builder extends BaseBuilder
Expand Down Expand Up @@ -57,6 +58,60 @@ public function inRandomOrder()
return $this;
}

protected function prepareKeysForOptimizedSort($keys)
{
return $keys->combine($keys);
}

protected function getOptimizedSortIndex()
{
if (count($this->orderBys) != 1) {
return null;
}

$indexName = $this->orderBys[0]->sort;

$store = $this->store;

if ($this->store instanceof AggregateStore) {
if ($this->store->stores()->count() != 1) {
return null;
}

$store = $this->store->stores()->first();
}

if (! $store->indexes()->has($indexName)) {
return null;
}

return $store->index($indexName);
}

private function sortUsingIndex($sortIndex, $keys)
{
$indexKeys = $sortIndex->items()->keys();
$preparedKeys = $this->prepareKeysForOptimizedSort($keys);
$sortKeys = $indexKeys->intersect($preparedKeys->keys());

$sortedKeys = [];

// Reassemble our keys using their indexed order.
// Some builders may change how keys look, and
// we cannot blindly return the index keys.
foreach ($sortKeys as $key) {
$sortedKeys[] = $preparedKeys[$key];
}

$sortedKeys = collect($sortedKeys);

if ($this->orderBys[0]->direction === 'desc') {
$sortedKeys = $sortedKeys->reverse()->values();
}

return $sortedKeys;
}

protected function orderKeys($keys)
{
if ($this->randomize) {
Expand All @@ -67,6 +122,10 @@ protected function orderKeys($keys)
return $keys;
}

if ($sortIndex = $this->getOptimizedSortIndex()) {
return $this->sortUsingIndex($sortIndex, $keys);
}

// Get key/value pairs for each orderBy's corresponding index, grouped by index.
// eg. [
// 'title' => ['one' => 'One', 'two' => 'Two'],
Expand Down
8 changes: 8 additions & 0 deletions src/Stache/Query/EntryQueryBuilder.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

namespace Statamic\Stache\Query;

use Illuminate\Support\Str;
use Statamic\Contracts\Entries\QueryBuilder;
use Statamic\Entries\EntryCollection;
use Statamic\Facades;
Expand All @@ -12,6 +13,13 @@ class EntryQueryBuilder extends Builder implements QueryBuilder

protected $collections;

protected function prepareKeysForOptimizedSort($keys)
{
return $keys->map(function ($value) {
return Str::after($value, '::');
})->combine($keys);
}

public function where($column, $operator = null, $value = null, $boolean = 'and')
{
if ($column === 'collection') {
Expand Down
8 changes: 8 additions & 0 deletions src/Stache/Query/TermQueryBuilder.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

namespace Statamic\Stache\Query;

use Illuminate\Support\Str;
use Statamic\Facades;
use Statamic\Facades\Collection;
use Statamic\Taxonomies\TermCollection;
Expand All @@ -11,6 +12,13 @@ class TermQueryBuilder extends Builder
protected $taxonomies;
protected $collections;

protected function prepareKeysForOptimizedSort($keys)
{
return $keys->map(function ($value) {
return Str::after($value, '::');
})->combine($keys);
}

public function where($column, $operator = null, $value = null, $boolean = 'and')
{
if ($column === 'taxonomy') {
Expand Down
2 changes: 1 addition & 1 deletion tests/Auth/UserGroupTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@ public function it_gets_all_the_users()

$userB->addToGroup($group)->save();
$this->assertCount(2, $group->users());
$this->assertSame([$userA, $userB], $group->users()->all());
$this->assertSame([$userB, $userA], $group->users()->all());
$this->assertTrue($group->hasUser($userA));
$this->assertTrue($group->hasUser($userB));
}
Expand Down
8 changes: 4 additions & 4 deletions tests/Data/Entries/EntryQueryBuilderTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -511,7 +511,7 @@ public function entries_are_found_using_where_with_json_value()
$this->assertCount(2, $entries);
$this->assertEquals(['Post 1', 'Post 5'], $entries->map->title->all());

$entries = Entry::query()->where('content->value', '<>', 1)->get();
$entries = Entry::query()->where('content->value', '<>', 1)->orderBy('title')->get();

$this->assertCount(5, $entries);
$this->assertEquals(['Post 2', 'Post 3', 'Post 4', 'Post 6', 'Post 7'], $entries->map->title->all());
Expand Down Expand Up @@ -719,17 +719,17 @@ public function entries_are_found_using_like($like, $expected)
->create();
});

$this->assertEquals($expected, Entry::query()->where('title', 'like', $like)->get()->map->title->all());
$this->assertEquals($expected, Entry::query()->where('title', 'like', $like)->orderBy('title')->get()->map->title->all());
}

public static function likeProvider()
{
return collect([
'foo' => ['foo'],
'foo%' => ['foo', 'food', 'foo bar', 'foo_bar', 'foodbar'],
'foo%' => ['foo', 'foo bar', 'foo_bar', 'food', 'foodbar'],
'%world' => ['hello world', 'waterworld'],
'%world%' => ['hello world', 'waterworld', 'world of warcraft'],
'_oo' => ['foo', 'boo'],
'_oo' => ['boo', 'foo'],
'o_' => ['on'],
'foo_bar' => ['foo bar', 'foo_bar', 'foodbar'],
'foo__bar' => [],
Expand Down
6 changes: 3 additions & 3 deletions tests/Data/Taxonomies/TermQueryBuilderTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,8 @@ class TermQueryBuilderTest extends TestCase
public function it_gets_terms()
{
Site::setConfig(['sites' => [
'en' => ['url' => '/'],
'fr' => ['url' => '/fr/'],
'en' => ['url' => '/', 'locale' => 'en_US'],
'fr' => ['url' => '/fr/', 'locale' => 'fr_FR'],
]]);

Taxonomy::make('tags')->sites(['en', 'fr'])->save();
Expand Down Expand Up @@ -83,7 +83,7 @@ public function it_filters_using_or_where_ins()
Term::make('d')->taxonomy('tags')->data(['test' => 'foo'])->save();
Term::make('e')->taxonomy('tags')->data(['test' => 'raz'])->save();

$terms = Term::query()->whereIn('test', ['foo', 'bar'])->orWhereIn('test', ['foo', 'raz'])->get();
$terms = Term::query()->whereIn('test', ['foo', 'bar'])->orWhereIn('test', ['foo', 'raz'])->orderBy('slug')->get();

$this->assertEquals(['a', 'b', 'd', 'e'], $terms->map->slug()->values()->all());
}
Expand Down
16 changes: 9 additions & 7 deletions tests/Data/Users/UserQueryBuilderTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -34,10 +34,10 @@ public function users_are_found_using_or_where_in()
User::make()->email('[email protected]')->data(['name' => 'Aragorn'])->save();
User::make()->email('[email protected]')->data(['name' => 'Tommy'])->save();

$users = User::query()->whereIn('name', ['Gandalf', 'Frodo'])->orWhereIn('name', ['Gandalf', 'Aragorn', 'Tommy'])->get();
$users = User::query()->whereIn('name', ['Gandalf', 'Frodo'])->orWhereIn('name', ['Gandalf', 'Aragorn', 'Tommy'])->orderBy('name')->get();

$this->assertCount(4, $users);
$this->assertEquals(['Gandalf', 'Frodo', 'Aragorn', 'Tommy'], $users->map->name->all());
$this->assertEquals(['Aragorn', 'Frodo', 'Gandalf', 'Tommy'], $users->map->name->all());
}

/** @test **/
Expand All @@ -53,7 +53,7 @@ public function users_are_found_using_or_where_not_in()
$users = User::query()->whereNotIn('name', ['Gandalf', 'Frodo'])->orWhereNotIn('name', ['Gandalf', 'Sauron'])->get();

$this->assertCount(3, $users);
$this->assertEquals(['Smeagol', 'Aragorn', 'Tommy'], $users->map->name->all());
$this->assertEquals(['Aragorn', 'Smeagol', 'Tommy'], $users->map->name->all());
}

/** @test **/
Expand Down Expand Up @@ -119,17 +119,19 @@ public function users_are_found_using_where_with_json_value()

$users = User::query()
->where('content->value', 1)
->orderBy('name')
->get();

$this->assertCount(2, $users);
$this->assertEquals(['Gandalf', 'Aragorn'], $users->map->name->all());
$this->assertEquals(['Aragorn', 'Gandalf'], $users->map->name->all());

$users = User::query()
->where('content->value', '<>', 1)
->orderBy('name', 'desc')
->get();

$this->assertCount(6, $users);
$this->assertEquals(['Smeagol', 'Frodo', 'Tommy', 'Sauron', 'Arwen', 'Bilbo'], $users->map->name->all());
$this->assertEquals(['Tommy', 'Smeagol', 'Sauron', 'Frodo', 'Bilbo', 'Arwen'], $users->map->name->all());
}

/** @test **/
Expand Down Expand Up @@ -225,7 +227,7 @@ public function users_are_found_using_where_group()
$userTwo->addToGroup($groupOne)->save();
$userThree->addToGroup($groupTwo)->save();

$users = User::query()->whereGroup('one')->get();
$users = User::query()->whereGroup('one')->orderBy('name')->get();

$this->assertCount(2, $users);
$this->assertEquals(['Gandalf', 'Smeagol'], $users->map->name->all());
Expand Down Expand Up @@ -302,7 +304,7 @@ public function users_are_found_using_where_role()
$userTwo->assignRole($roleOne)->save();
$userThree->assignRole($roleTwo)->save();

$users = User::query()->whereRole('one')->get();
$users = User::query()->whereRole('one')->orderBy('name')->get();

$this->assertCount(2, $users);
$this->assertEquals(['Gandalf', 'Smeagol'], $users->map->name->all());
Expand Down
6 changes: 3 additions & 3 deletions tests/Feature/GraphQL/TermsTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -100,8 +100,8 @@ public function it_queries_all_terms()
['id' => 'tags::bravo', 'title' => 'Tag Bravo'],
['id' => 'categories::alpha', 'title' => 'Category Alpha'],
['id' => 'categories::bravo', 'title' => 'Category Bravo'],
['id' => 'sizes::small', 'title' => 'Size Small'],
['id' => 'sizes::large', 'title' => 'Size Large'],
['id' => 'sizes::small', 'title' => 'Size Small'],
]]]]);
}

Expand Down Expand Up @@ -272,8 +272,8 @@ public function it_queries_terms_from_multiple_taxonomies()
->assertExactJson(['data' => ['terms' => ['data' => [
['id' => 'categories::alpha', 'title' => 'Category Alpha'],
['id' => 'categories::bravo', 'title' => 'Category Bravo'],
['id' => 'sizes::small', 'title' => 'Size Small'],
['id' => 'sizes::large', 'title' => 'Size Large'],
['id' => 'sizes::small', 'title' => 'Size Small'],
]]]]);
}

Expand Down Expand Up @@ -327,8 +327,8 @@ public function it_queries_blueprint_specific_fields()
->assertExactJson(['data' => ['terms' => ['data' => [
['id' => 'tags::alpha', 'foo' => 'FOO!'],
['id' => 'tags::bravo', 'bar' => 'BAR!'],
['id' => 'sizes::small', 'shorthand' => 'sm'],
['id' => 'sizes::large', 'shorthand' => 'lg'],
['id' => 'sizes::small', 'shorthand' => 'sm'],
]]]]);
}

Expand Down
2 changes: 1 addition & 1 deletion tests/Feature/GraphQL/UsersTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -220,7 +220,7 @@ public function it_can_filter_users_when_configuration_allows_for_it()
contains: "rad",
ends_with: "!"
}
}) {
}, sort: "id") {
data {
id
bio
Expand Down
18 changes: 9 additions & 9 deletions tests/Feature/Taxonomies/TermEntriesTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -139,9 +139,9 @@ public function it_gets_and_counts_entries_for_a_localized_term_across_collectio
$this->assertEquals(['rouge-shirt'], Term::find('colors::red')->in('fr')->entries()->map->slug()->all());

$this->assertEquals(2, Term::find('colors::black')->in('en')->entriesCount());
$this->assertEquals(['panther', 'black-shirt'], Term::find('colors::black')->in('en')->entries()->map->slug()->all());
$this->assertEquals(['black-shirt', 'panther'], Term::find('colors::black')->in('en')->entries()->map->slug()->sort()->values()->all());
$this->assertEquals(2, Term::find('colors::black')->in('fr')->entriesCount());
$this->assertEquals(['panthere', 'noir-shirt'], Term::find('colors::black')->in('fr')->entries()->map->slug()->all());
$this->assertEquals(['noir-shirt', 'panthere'], Term::find('colors::black')->in('fr')->entries()->map->slug()->sort()->values()->all());

$this->assertEquals(1, Term::find('colors::yellow')->in('en')->entriesCount());
$this->assertEquals(['cheetah'], Term::find('colors::yellow')->in('en')->entries()->map->slug()->all());
Expand All @@ -151,13 +151,13 @@ public function it_gets_and_counts_entries_for_a_localized_term_across_collectio
// and for the base Term class, it should not filter by locale

$this->assertEquals(2, Term::find('colors::red')->term()->entriesCount());
$this->assertEquals(['red-shirt', 'rouge-shirt'], Term::find('colors::red')->term()->entries()->map->slug()->all());
$this->assertEquals(['red-shirt', 'rouge-shirt'], Term::find('colors::red')->term()->entries()->map->slug()->sort()->values()->all());

$this->assertEquals(4, Term::find('colors::black')->term()->entriesCount());
$this->assertEquals(['panther', 'panthere', 'black-shirt', 'noir-shirt'], Term::find('colors::black')->term()->entries()->map->slug()->all());
$this->assertEquals(['black-shirt', 'noir-shirt', 'panther', 'panthere'], Term::find('colors::black')->term()->entries()->map->slug()->sort()->values()->all());

$this->assertEquals(2, Term::find('colors::yellow')->term()->entriesCount());
$this->assertEquals(['cheetah', 'guepard'], Term::find('colors::yellow')->term()->entries()->map->slug()->all());
$this->assertEquals(['cheetah', 'guepard'], Term::find('colors::yellow')->term()->entries()->map->slug()->sort()->values()->all());
}

/** @test */
Expand Down Expand Up @@ -233,16 +233,16 @@ public function it_gets_and_counts_entries_for_a_localized_term_for_a_single_col
$this->assertEquals([], Term::find('colors::red')->collection($animals)->term()->entries()->map->slug()->all());

$this->assertEquals(2, Term::find('colors::black')->collection($animals)->term()->entriesCount());
$this->assertEquals(['panther', 'panthere'], Term::find('colors::black')->collection($animals)->term()->entries()->map->slug()->all());
$this->assertEquals(['panther', 'panthere'], Term::find('colors::black')->collection($animals)->term()->entries()->map->slug()->sort()->values()->all());

$this->assertEquals(2, Term::find('colors::yellow')->collection($animals)->term()->entriesCount());
$this->assertEquals(['cheetah', 'guepard'], Term::find('colors::yellow')->collection($animals)->term()->entries()->map->slug()->all());
$this->assertEquals(['cheetah', 'guepard'], Term::find('colors::yellow')->collection($animals)->term()->entries()->map->slug()->sort()->values()->all());

$this->assertEquals(2, Term::find('colors::red')->collection($clothes)->term()->entriesCount());
$this->assertEquals(['red-shirt', 'rouge-shirt'], Term::find('colors::red')->collection($clothes)->term()->entries()->map->slug()->all());
$this->assertEquals(['red-shirt', 'rouge-shirt'], Term::find('colors::red')->collection($clothes)->term()->entries()->map->slug()->sort()->values()->all());

$this->assertEquals(2, Term::find('colors::black')->collection($clothes)->term()->entriesCount());
$this->assertEquals(['black-shirt', 'noir-shirt'], Term::find('colors::black')->collection($clothes)->term()->entries()->map->slug()->all());
$this->assertEquals(['black-shirt', 'noir-shirt'], Term::find('colors::black')->collection($clothes)->term()->entries()->map->slug()->sort()->values()->all());

$this->assertEquals(0, Term::find('colors::yellow')->collection($clothes)->term()->entriesCount());
$this->assertEquals([], Term::find('colors::yellow')->collection($clothes)->term()->entries()->map->slug()->all());
Expand Down

0 comments on commit 6dc2217

Please sign in to comment.