diff --git a/infection.json b/infection.json index 522d3ba4..3bbbd930 100644 --- a/infection.json +++ b/infection.json @@ -8,7 +8,7 @@ "ExplorerServiceProvider.php", "Infrastructure/Console", "Infrastructure/Scout/ElasticEngine", - "Infrastructure/Elastic/ElasticAdapter", + "Infrastructure/Elastic/ElasticClientBuilder", "Infrastructure/Elastic/ElasticIndexAdapter", "Infrastructure/Elastic/ElasticDocumentAdapter" ] diff --git a/src/Application/Results.php b/src/Application/Results.php index 2a935311..625ce1fe 100644 --- a/src/Application/Results.php +++ b/src/Application/Results.php @@ -31,11 +31,7 @@ public function aggregations(): array foreach ($this->rawResults['aggregations'] as $name => $rawAggregation) { if (array_key_exists('doc_count', $rawAggregation)) { - foreach ($rawAggregation as $nestedAggregationName => $rawNestedAggregation) { - if (isset($rawNestedAggregation['buckets'])) { - $aggregations[] = new AggregationResult($nestedAggregationName, $rawNestedAggregation['buckets']); - } - } + $aggregations = array_merge($aggregations, $this->parseNestedAggregations($rawAggregation)); continue; } @@ -49,4 +45,24 @@ public function count(): int { return $this->rawResults['hits']['total']['value']; } + + /** @return AggregationResult[] */ + private function parseNestedAggregations(array $rawAggregation): array + { + $aggregations = []; + foreach ($rawAggregation as $nestedAggregationName => $rawNestedAggregation) { + if (isset($rawNestedAggregation['buckets'])) { + $aggregations[] = new AggregationResult($nestedAggregationName, $rawNestedAggregation['buckets']); + } + + if (isset($rawNestedAggregation['doc_count'])) { + $nested = $this->parseNestedAggregations($rawNestedAggregation); + foreach ($nested as $item) { + $aggregations[] = $item; + } + } + } + + return $aggregations; + } } diff --git a/src/Domain/Aggregations/NestedFilteredAggregation.php b/src/Domain/Aggregations/NestedFilteredAggregation.php new file mode 100644 index 00000000..d9a36a9b --- /dev/null +++ b/src/Domain/Aggregations/NestedFilteredAggregation.php @@ -0,0 +1,83 @@ + + */ + private array $filters; + + private int $size; + + /** + * @param array $filters + */ + public function __construct(string $path, string $name, string $field, array $filters, int $size = 10) + { + $this->path = $path; + $this->name = $name; + $this->field = $field; + $this->size = $size; + $this->filters = $filters; + } + + /** + * @return array + */ + public function build(): array + { + return [ + 'nested' => [ + 'path' => $this->path, + ], + 'aggs' => [ + 'filter_aggs' => [ + 'filter' => $this->buildElasticFilters(), + 'aggs' => [ + $this->name => [ + 'terms' => [ + 'field' => $this->path . '.' . $this->field, + 'size' => $this->size, + ], + ], + ], + ], + ], + ]; + } + + /** + * @return array + */ + private function buildElasticFilters(): array + { + $elasticFilters = []; + foreach ($this->filters as $field => $filterValues) { + $elasticFilters[] = [ + 'terms' => [ + $this->path . '.' . $field => $filterValues, + ], + ]; + } + + return [ + 'bool' => [ + 'should' => [ + 'bool' => [ + 'must' => $elasticFilters, + ], + ], + ], + ]; + } +} diff --git a/src/Domain/IndexManagement/DirectIndexConfiguration.php b/src/Domain/IndexManagement/DirectIndexConfiguration.php index ccb5742a..ead8c190 100644 --- a/src/Domain/IndexManagement/DirectIndexConfiguration.php +++ b/src/Domain/IndexManagement/DirectIndexConfiguration.php @@ -4,8 +4,6 @@ namespace JeroenG\Explorer\Domain\IndexManagement; -use Webmozart\Assert\Assert; - final class DirectIndexConfiguration implements IndexConfigurationInterface { private function __construct( diff --git a/src/Infrastructure/Elastic/FakeResponse.php b/src/Infrastructure/Elastic/FakeResponse.php index b3dda21a..41917717 100644 --- a/src/Infrastructure/Elastic/FakeResponse.php +++ b/src/Infrastructure/Elastic/FakeResponse.php @@ -4,10 +4,6 @@ namespace JeroenG\Explorer\Infrastructure\Elastic; -use Elasticsearch\Client; -use GuzzleHttp\Ring\Future\FutureArrayInterface; -use JeroenG\Explorer\Application\Results; -use JeroenG\Explorer\Application\SearchCommandInterface; use Webmozart\Assert\Assert; class FakeResponse diff --git a/src/Infrastructure/Scout/Builder.php b/src/Infrastructure/Scout/Builder.php index 5179a330..28ac51c6 100644 --- a/src/Infrastructure/Scout/Builder.php +++ b/src/Infrastructure/Scout/Builder.php @@ -4,7 +4,6 @@ namespace JeroenG\Explorer\Infrastructure\Scout; -use JeroenG\Explorer\Application\Paginator; use JeroenG\Explorer\Domain\Aggregations\AggregationSyntaxInterface; use JeroenG\Explorer\Domain\Query\QueryProperties\QueryProperty; use JeroenG\Explorer\Domain\Syntax\Compound\BoolQuery; diff --git a/src/Infrastructure/Scout/ScoutSearchCommandBuilder.php b/src/Infrastructure/Scout/ScoutSearchCommandBuilder.php index edf1d3df..d674c01e 100644 --- a/src/Infrastructure/Scout/ScoutSearchCommandBuilder.php +++ b/src/Infrastructure/Scout/ScoutSearchCommandBuilder.php @@ -12,7 +12,6 @@ use JeroenG\Explorer\Domain\Syntax\Compound\QueryType; use JeroenG\Explorer\Domain\Syntax\MultiMatch; use JeroenG\Explorer\Domain\Syntax\Sort; -use JeroenG\Explorer\Domain\Syntax\SyntaxInterface; use JeroenG\Explorer\Domain\Syntax\Term; use JeroenG\Explorer\Domain\Syntax\Terms; use Laravel\Scout\Builder; diff --git a/tests/Unit/FinderTest.php b/tests/Unit/FinderTest.php index 94665b0f..82dfb4f4 100644 --- a/tests/Unit/FinderTest.php +++ b/tests/Unit/FinderTest.php @@ -9,6 +9,7 @@ use JeroenG\Explorer\Application\AggregationResult; use JeroenG\Explorer\Application\SearchCommand; use JeroenG\Explorer\Domain\Aggregations\MaxAggregation; +use JeroenG\Explorer\Domain\Aggregations\NestedFilteredAggregation; use JeroenG\Explorer\Domain\Aggregations\TermsAggregation; use JeroenG\Explorer\Domain\Query\Query; use JeroenG\Explorer\Domain\Syntax\Compound\BoolQuery; @@ -20,6 +21,7 @@ use Mockery; use Mockery\Adapter\Phpunit\MockeryTestCase; use JeroenG\Explorer\Domain\Aggregations\NestedAggregation; +use function var_dump; class FinderTest extends MockeryTestCase { @@ -83,15 +85,15 @@ public function test_it_accepts_must_should_filter_and_where_queries(): void 'query' => [ 'bool' => [ 'must' => [ - ['match' => ['title' => [ 'query' => 'Lorem Ipsum', 'fuzziness' => 'auto']]], + ['match' => ['title' => ['query' => 'Lorem Ipsum', 'fuzziness' => 'auto']]], ['multi_match' => ['query' => 'fuzzy search', 'fuzziness' => 'auto']], ], 'should' => [ - ['match' => ['text' => [ 'query' => 'consectetur adipiscing elit', 'fuzziness' => 'auto']]], + ['match' => ['text' => ['query' => 'consectetur adipiscing elit', 'fuzziness' => 'auto']]], ], 'filter' => [ - ['term' => ['published' => [ 'value' => true, 'boost' => 1.0]]], - ['term' => ['subtitle' => [ 'value' => 'Dolor sit amet', 'boost' => 1.0]]], + ['term' => ['published' => ['value' => true, 'boost' => 1.0]]], + ['term' => ['subtitle' => ['value' => 'Dolor sit amet', 'boost' => 1.0]]], ['terms' => ['tags' => ['t1', 't2'], 'boost' => 1.0]], ], ], @@ -241,7 +243,7 @@ public function test_it_builds_with_default_fields(): void 'query' => [ 'bool' => [ 'must' => [ - ['multi_match' => ['query' => 'fuzzy search', 'fields' => self::SEARCHABLE_FIELDS, 'fuzziness' => 'auto' ]], + ['multi_match' => ['query' => 'fuzzy search', 'fields' => self::SEARCHABLE_FIELDS, 'fuzziness' => 'auto']], ], 'should' => [], 'filter' => [], @@ -281,7 +283,7 @@ public function test_it_adds_fields_to_query(): void 'filter' => [], ], ], - 'fields' => ['*.length', 'specific.field'] + 'fields' => ['*.length', 'specific.field'], ], ]) ->andReturn([ @@ -331,18 +333,18 @@ public function test_it_adds_aggregates(): void 'aggregations' => [ 'specificAggregation' => [ 'buckets' => [ - ['key' => 'myKey', 'doc_count' => 42] - ] + ['key' => 'myKey', 'doc_count' => 42], + ], ], 'anotherAggregation' => [ 'buckets' => [ - ['key' => 'anotherKey', 'doc_count' => 6] - ] + ['key' => 'anotherKey', 'doc_count' => 6], + ], ], 'metricAggregation' => [ 'value' => 10, - ] - ] + ], + ], ]); $query = Query::with(new BoolQuery()); @@ -389,6 +391,39 @@ public function test_it_adds_nested_aggregations(): void ], ], 'aggs' => [ + 'anotherAggregation' => ['terms' => ['field' => 'anotherField', 'size' => 10]], + 'nestedFilteredAggregation' => [ + 'nested' => [ + 'path' => 'nestedFilteredAggregation', + ], + 'aggs' => [ + 'filter_aggs' => [ + 'filter' => [ + 'bool' => [ + 'should' => [ + 'bool' => [ + 'must' => [ + [ + 'terms' => [ + 'nestedFilteredAggregation.someFilter' => ['values'], + ], + ], + ], + ], + ], + ], + ], + 'aggs' => [ + 'filter_aggs' => [ + 'terms' => [ + 'field' => 'nestedFilteredAggregation.someFieldNestedAggregation', + 'size' => 10, + ], + ], + ], + ], + ], + ], 'nestedAggregation' => [ 'nested' => [ 'path' => 'nestedAggregation', @@ -402,7 +437,6 @@ public function test_it_adds_nested_aggregations(): void ], ], ], - 'anotherAggregation' => ['terms' => ['field' => 'anotherField', 'size' => 10]] ], ], ]) @@ -418,14 +452,32 @@ public function test_it_adds_nested_aggregations(): void 'doc_count_error_upper_bound' => 0, 'sum_other_doc_count' => 0, 'buckets' => [ - ['key' => 'someKey', 'doc_count' => 6,] + ['key' => 'someKey', 'doc_count' => 6,], ], ], ], + + 'nestedFilteredAggregation' => [ + 'doc_count' => 42, + 'filter_aggs' => [ + 'doc_count' => 42, + 'buckets' => [ + ['key' => 'someFieldNestedAggregation_check', 'doc_count' => 6,], + ], + 'someFieldFiltered' => [ + 'doc_count_error_upper_bound' => 0, + 'sum_other_doc_count' => 0, + 'buckets' => [ + ['key' => 'someFieldNestedAggregation', 'doc_count' => 6,], + ], + ], + ], + ], + 'specificAggregation' => [ 'buckets' => [ - ['key' => 'myKey', 'doc_count' => 42] - ] + ['key' => 'myKey', 'doc_count' => 42], + ], ], ], ]); @@ -434,14 +486,23 @@ public function test_it_adds_nested_aggregations(): void $query->addAggregation('anotherAggregation', new TermsAggregation('anotherField')); $nestedAggregation = new NestedAggregation('nestedAggregation'); $nestedAggregation->add('someField', new TermsAggregation('nestedAggregation.someField')); - $query->addAggregation('nestedAggregation',$nestedAggregation); + + $filter = [ + 'someFilter' => ['values'], + ]; + $query->addAggregation( + 'nestedFilteredAggregation', + new NestedFilteredAggregation('nestedFilteredAggregation', 'filter_aggs', 'someFieldNestedAggregation', $filter) + ); + + $query->addAggregation('nestedAggregation', $nestedAggregation); $builder = new SearchCommand(self::TEST_INDEX, $query); $builder->setIndex(self::TEST_INDEX); $subject = new Finder($client, $builder); $results = $subject->find(); - self::assertCount(2, $results->aggregations()); + self::assertCount(4, $results->aggregations()); $nestedAggregation = $results->aggregations()[0]; @@ -453,6 +514,100 @@ public function test_it_adds_nested_aggregations(): void self::assertEquals(6, $nestedAggregationValue['doc_count']); self::assertEquals('someKey', $nestedAggregationValue['key']); + + $nestedFilterAggregation = $results->aggregations()[2]; + + self::assertInstanceOf(AggregationResult::class, $nestedFilterAggregation); + self::assertEquals('someFieldFiltered', $nestedFilterAggregation->name()); + self::assertCount(1, $nestedFilterAggregation->values()); + + $nestedFilterAggregationValue = $nestedFilterAggregation->values()[0]; + + self::assertEquals(6, $nestedFilterAggregationValue['doc_count']); + self::assertEquals('someFieldNestedAggregation', $nestedFilterAggregationValue['key']); + } + + public function test_with_single_aggregation(): void + { + $client = Mockery::mock(Client::class); + $client->expects('search') + ->with([ + 'index' => self::TEST_INDEX, + 'body' => [ + 'query' => [ + 'bool' => [ + 'must' => [], + 'should' => [], + 'filter' => [], + ], + ], + 'aggs' => [ + 'anotherAggregation' => ['terms' => ['field' => 'anotherField', 'size' => 10]], + ], + ], + ]) + ->andReturn([ + 'hits' => [ + 'total' => ['value' => 1], + 'hits' => [$this->hit()], + ], + 'aggregations' => [ + 'nestedAggregation' => [ + 'doc_count' => 42, + 'someField' => [ + 'doc_count_error_upper_bound' => 0, + 'sum_other_doc_count' => 0, + 'buckets' => [ + ['key' => 'someKey', 'doc_count' => 6,], + ], + ], + ], + ], + ]); + + $query = Query::with(new BoolQuery()); + $query->addAggregation('anotherAggregation', new TermsAggregation('anotherField')); + + $builder = new SearchCommand(self::TEST_INDEX, $query); + $builder->setIndex(self::TEST_INDEX); + + $subject = new Finder($client, $builder); + $results = $subject->find(); + + self::assertCount(1, $results->aggregations()); + } + + public function test_it_with_no_aggregations(): void + { + $client = Mockery::mock(Client::class); + $client->expects('search') + ->with([ + 'index' => self::TEST_INDEX, + 'body' => [ + 'query' => [ + 'bool' => [ + 'must' => [], + 'should' => [], + 'filter' => [], + ], + ], + ], + ]) + ->andReturn([ + 'hits' => [ + 'total' => ['value' => 1], + 'hits' => [$this->hit()], + ], + ]); + + $query = Query::with(new BoolQuery()); + $builder = new SearchCommand(self::TEST_INDEX, $query); + $builder->setIndex(self::TEST_INDEX); + + $subject = new Finder($client, $builder); + $results = $subject->find(); + + self::assertCount(0, $results->aggregations()); } private function hit(int $id = 1, float $score = 1.0): array diff --git a/tests/Unit/IndexManagement/ElasticIndexConfigurationRepositoryTest.php b/tests/Unit/IndexManagement/ElasticIndexConfigurationRepositoryTest.php index 300d7b95..f0cb6136 100644 --- a/tests/Unit/IndexManagement/ElasticIndexConfigurationRepositoryTest.php +++ b/tests/Unit/IndexManagement/ElasticIndexConfigurationRepositoryTest.php @@ -22,6 +22,7 @@ public function test_it_creates_the_config_from_array(): void 'a' => [ 'aliased' => true, 'settings' => [ 'test' => true ], + 'model' => 'model', 'properties' => [ 'fld' => [ 'type' => 'text', @@ -41,6 +42,7 @@ public function test_it_creates_the_config_from_array(): void self::assertEquals($indices['a']['properties'], $config->getProperties()); self::assertEquals($indices['a']['settings'], $config->getSettings()); self::assertEquals('a', $config->getName()); + self::assertEquals('model', $config->getModel()); } public function test_it_normalizes_the_configuration(): void @@ -197,6 +199,7 @@ public function test_it_sets_alias_from_aliased_model(): void self::assertInstanceOf(AliasedIndexConfiguration::class, $config); self::assertTrue($config->getAliasConfiguration()->shouldOldAliasesBePruned()); + self::assertEquals(':searchable_as:', $config->getAliasConfiguration()->getAliasName()); } public function test_it_has_pruning_for_aliased_indices_by_default(): void