Skip to content

Nested query improvements #53

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 10 commits into from
Jan 20, 2025
23 changes: 23 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,29 @@ The following query types are available:
);
```

##### `NestedQuery` `InnerHits`

[https://www.elastic.co/guide/en/elasticsearch/reference/current/inner-hits.html](https://www.elastic.co/guide/en/elasticsearch/reference/current/inner-hits.html)

```php
$nestedQuery = \Spatie\ElasticsearchQueryBuilder\Queries\NestedQuery::create(
'comments',
\Spatie\ElasticsearchQueryBuilder\Queries\TermsQuery::create('comments.published', true)
);

$nestedQuery->innerHits(
\Spatie\ElasticsearchQueryBuilder\Queries\NestedQuery\InnerHits::create('top_three_liked_comments')
->size(3)
->addSort(
\Spatie\ElasticsearchQueryBuilder\Sorts\Sort::create(
'comments.likes',
\Spatie\ElasticsearchQueryBuilder\Sorts\Sort::DESC
)
)
->fields(['comments.content', 'comments.author', 'comments.likes'])
);
```

#### `RangeQuery`

[https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-range-query.html](https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-range-query.html)
Expand Down
53 changes: 42 additions & 11 deletions src/Queries/NestedQuery.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,32 +2,63 @@

namespace Spatie\ElasticsearchQueryBuilder\Queries;

use Spatie\ElasticsearchQueryBuilder\Queries\NestedQuery\InnerHits;

class NestedQuery implements Query
{
protected string $path;

protected Query $query;
public const SCORE_MODE_AVG = 'avg';
public const SCORE_MODE_MAX = 'max';
public const SCORE_MODE_MIN = 'min';
public const SCORE_MODE_NONE = 'none';
public const SCORE_MODE_SUM = 'sum';

public static function create(string $path, Query $query): self
{
return new self($path, $query);
}

public function __construct(
string $path,
Query $query
protected string $path,
protected Query $query,
protected ?string $scoreMode = null,
protected ?bool $ignoreUnmapped = null,
protected ?InnerHits $innerHits = null
) {
$this->path = $path;
$this->query = $query;
}

public function scoreMode(string $scoreMode): self
{
$this->scoreMode = $scoreMode;

return $this;
}

public function ignoreUnmapped(bool $ignoreUnmapped): self
{
$this->ignoreUnmapped = $ignoreUnmapped;

return $this;
}

public function innerHits(InnerHits $innerHits): self
{
$this->innerHits = $innerHits;

return $this;
}

public function toArray(): array
{
return [
'nested' => [
'path' => $this->path,
'query' => $this->query->toArray(),
],
'nested' => array_filter(
[
'path' => $this->path,
'query' => $this->query->toArray(),
'score_mode' => $this->scoreMode,
'ignore_unmapped' => $this->ignoreUnmapped,
'inner_hits' => $this->innerHits?->toArray()
]
),
];
}
}
74 changes: 74 additions & 0 deletions src/Queries/NestedQuery/InnerHits.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
<?php

namespace Spatie\ElasticsearchQueryBuilder\Queries\NestedQuery;

use Spatie\ElasticsearchQueryBuilder\SortCollection;
use Spatie\ElasticsearchQueryBuilder\Sorts\Sort;

class InnerHits
{
public static function create(string $name): self
{
return new InnerHits($name);
}

/**
* @param string[]|null $fields
*/
public function __construct(
protected string $name,
protected ?int $from = null,
protected ?int $size = null,
protected ?SortCollection $sorts = null,
protected ?array $fields = null
) {
}

public function from(int $from): self
{
$this->from = $from;

return $this;
}

public function size(int $size): self
{
$this->size = $size;

return $this;
}

/**
* @param string[] $fields
*/
public function fields(array $fields): self
{
$this->fields = $fields;

return $this;
}

public function addSort(Sort $sort): self
{
if (! $this->sorts) {
$this->sorts = new SortCollection();
}

$this->sorts->add($sort);

return $this;
}

public function toArray(): array
{
return array_filter(
[
'from' => $this->from,
'size' => $this->size,
'name' => $this->name,
'sort' => $this->sorts?->toArray(),
'_source' => $this->fields
]
);
}
}
93 changes: 93 additions & 0 deletions tests/Queries/NestedQuery/InnerHitsTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
<?php

namespace Spatie\ElasticsearchQueryBuilder\Tests\Queries\NestedQuery;

use Spatie\ElasticsearchQueryBuilder\Queries\NestedQuery\InnerHits;
use PHPUnit\Framework\TestCase;
use Spatie\ElasticsearchQueryBuilder\SortCollection;
use Spatie\ElasticsearchQueryBuilder\Sorts\Sort;

class InnerHitsTest extends TestCase
{
private InnerHits $innerHits;

protected function setUp(): void
{
$this->innerHits = new InnerHits('test');
}

public function testToArrayBuildsCorrectInnerHits(): void
{
$this->assertEquals(
[
'name' => 'test',
],
$this->innerHits->toArray()
);
}

public function testToArrayBuildsCorrectInnerHitsWithFrom(): void
{
$this->assertEquals(
[
'name' => 'test',
'from' => 123,
],
$this->innerHits->from(123)->toArray()
);
}

public function testToArrayBuildsCorrectInnerHitsWithSize(): void
{
$this->assertEquals(
[
'name' => 'test',
'size' => 123,
],
$this->innerHits->size(123)->toArray()
);
}

public function testToArrayBuildsCorrectInnerHitsWithSorts(): void
{
$sortMock = $this->createMock(Sort::class);

$sortMock
->method('toArray')
->willReturn(['field' => ['order' => Sort::ASC]]);

$this->assertEquals(
[
'name' => 'test',
'sort' => [
[
'field' => [
'order' => Sort::ASC,
]
]
]
],
$this->innerHits->addSort($sortMock)->toArray()
);
}

public function testAddSortAddsSortToSorts(): void
{
$sortMock = $this->createMock(Sort::class);

$sortMock
->method('toArray')
->willReturn(['sort']);

$sortsMock = $this->createMock(SortCollection::class);

$sortsMock
->expects($this->once())
->method('add')
->with($sortMock);

$innerHits = new InnerHits('test', sorts: $sortsMock);

$innerHits->addSort($sortMock);
}
}
92 changes: 92 additions & 0 deletions tests/Queries/NestedQueryTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
<?php

namespace Spatie\ElasticsearchQueryBuilder\Tests\Queries;

use PHPUnit\Framework\TestCase;
use Spatie\ElasticsearchQueryBuilder\Queries\NestedQuery;
use Spatie\ElasticsearchQueryBuilder\Queries\NestedQuery\InnerHits;
use Spatie\ElasticsearchQueryBuilder\Queries\Query;

class NestedQueryTest extends TestCase
{
private NestedQuery $nestedQuery;

protected function setUp(): void
{
$queryMock = $this->createMock(Query::class);

$queryMock
->method('toArray')
->willReturn(['query']);

$this->nestedQuery = new NestedQuery('path', $queryMock);
}

public function testToArrayBuildsCorrectNestedQuery(): void
{
$this->assertEquals(
[
'nested' => [
'path' => 'path',
'query' => ['query']
]
],
$this->nestedQuery->toArray()
);
}

public function testToArrayBuildsCorrectNestedQueryWithScoreMode(): void
{
$this->assertEquals(
[
'nested' => [
'path' => 'path',
'query' => ['query'],
'score_mode' => NestedQuery::SCORE_MODE_MIN,
]
],
$this->nestedQuery->scoreMode(NestedQuery::SCORE_MODE_MIN)->toArray()
);
}

public function testToArrayBuildsCorrectNestedQueryWithIgnoreUnmapped(): void
{
$this->assertEquals(
[
'nested' => [
'path' => 'path',
'query' => ['query'],
'ignore_unmapped' => true,
]
],
$this->nestedQuery->ignoreUnmapped(true)->toArray()
);
}

public function testToArrayBuildsCorrectNestedQueryWithInnerHits(): void
{
$innerHitsMock = $this->createMock(InnerHits::class);
$innerHitsMock
->method('toArray')
->willReturn(
[
'size' => 10,
'name' => 'test'
]
);

$this->assertEquals(
[
'nested' => [
'path' => 'path',
'query' => ['query'],
'inner_hits' => [
'size' => 10,
'name' => 'test'
]
]
],
$this->nestedQuery->innerHits($innerHitsMock)->toArray()
);
}
}
Loading