Skip to content

Commit 0369e28

Browse files
Spamerczclaude
andcommitted
feat(query): add has parent joining query
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 40b57ac commit 0369e28

3 files changed

Lines changed: 131 additions & 0 deletions

File tree

doc/02-query-objects.md

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -344,6 +344,20 @@ new \Spameri\ElasticQuery\Query\Boosting(
344344

345345
Queries that traverse parent/child or join relationships.
346346

347+
##### HasParent Query
348+
Match children whose parent matches the inner query.
349+
- Class: `\Spameri\ElasticQuery\Query\HasParent`
350+
- [Documentation](https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-has-parent-query.html)
351+
- [Implementation](https://github.com/Spameri/ElasticQuery/blob/master/src/Query/HasParent.php)
352+
353+
```php
354+
new \Spameri\ElasticQuery\Query\HasParent(
355+
parentType: 'blog',
356+
query: new \Spameri\ElasticQuery\Query\Term('tag', 'tech'),
357+
score: true,
358+
);
359+
```
360+
347361
##### HasChild Query
348362
Match parents whose children match the inner query.
349363
- Class: `\Spameri\ElasticQuery\Query\HasChild`

src/Query/HasParent.php

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
<?php
2+
3+
declare(strict_types = 1);
4+
5+
namespace Spameri\ElasticQuery\Query;
6+
7+
/**
8+
* @see https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-has-parent-query.html
9+
*/
10+
class HasParent implements \Spameri\ElasticQuery\Query\LeafQueryInterface
11+
{
12+
13+
public function __construct(
14+
private string $parentType,
15+
private \Spameri\ElasticQuery\Query\LeafQueryInterface $query,
16+
private bool|null $score = null,
17+
private bool|null $ignoreUnmapped = null,
18+
)
19+
{
20+
}
21+
22+
23+
public function key(): string
24+
{
25+
return 'has_parent_' . $this->parentType;
26+
}
27+
28+
29+
/**
30+
* @return array<string, array<string, mixed>>
31+
*/
32+
public function toArray(): array
33+
{
34+
$body = [
35+
'parent_type' => $this->parentType,
36+
'query' => $this->query->toArray(),
37+
];
38+
39+
if ($this->score !== null) {
40+
$body['score'] = $this->score;
41+
}
42+
43+
if ($this->ignoreUnmapped !== null) {
44+
$body['ignore_unmapped'] = $this->ignoreUnmapped;
45+
}
46+
47+
return [
48+
'has_parent' => $body,
49+
];
50+
}
51+
52+
}
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace SpameriTests\ElasticQuery\Query;
4+
5+
require_once __DIR__ . '/../../bootstrap.php';
6+
7+
8+
class HasParent extends \Tester\TestCase
9+
{
10+
11+
private const INDEX = 'spameri_test_query_has_parent';
12+
13+
14+
public function setUp(): void
15+
{
16+
$ch = \curl_init();
17+
\curl_setopt($ch, \CURLOPT_URL, \ELASTICSEARCH_HOST . '/' . self::INDEX);
18+
\curl_setopt($ch, \CURLOPT_RETURNTRANSFER, 1);
19+
\curl_setopt($ch, \CURLOPT_CUSTOMREQUEST, 'PUT');
20+
\curl_setopt($ch, \CURLOPT_HTTPHEADER, ['Content-Type: application/json']);
21+
22+
\curl_exec($ch);
23+
}
24+
25+
26+
public function testToArray(): void
27+
{
28+
$hasParent = new \Spameri\ElasticQuery\Query\HasParent(
29+
parentType: 'blog',
30+
query: new \Spameri\ElasticQuery\Query\Term('tag', 'tech'),
31+
score: true,
32+
);
33+
34+
$array = $hasParent->toArray();
35+
36+
\Tester\Assert::same('blog', $array['has_parent']['parent_type']);
37+
\Tester\Assert::true($array['has_parent']['score']);
38+
}
39+
40+
41+
public function testKey(): void
42+
{
43+
$hasParent = new \Spameri\ElasticQuery\Query\HasParent(
44+
'blog',
45+
new \Spameri\ElasticQuery\Query\Term('tag', 'tech'),
46+
);
47+
48+
\Tester\Assert::same('has_parent_blog', $hasParent->key());
49+
}
50+
51+
52+
public function tearDown(): void
53+
{
54+
$ch = \curl_init();
55+
\curl_setopt($ch, \CURLOPT_URL, \ELASTICSEARCH_HOST . '/' . self::INDEX);
56+
\curl_setopt($ch, \CURLOPT_RETURNTRANSFER, 1);
57+
\curl_setopt($ch, \CURLOPT_CUSTOMREQUEST, 'DELETE');
58+
\curl_setopt($ch, \CURLOPT_HTTPHEADER, ['Content-Type: application/json']);
59+
60+
\curl_exec($ch);
61+
}
62+
63+
}
64+
65+
(new HasParent())->run();

0 commit comments

Comments
 (0)