Skip to content

Commit 40b57ac

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

3 files changed

Lines changed: 150 additions & 0 deletions

File tree

doc/02-query-objects.md

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -340,6 +340,27 @@ new \Spameri\ElasticQuery\Query\Boosting(
340340

341341
---
342342

343+
## Joining Queries
344+
345+
Queries that traverse parent/child or join relationships.
346+
347+
##### HasChild Query
348+
Match parents whose children match the inner query.
349+
- Class: `\Spameri\ElasticQuery\Query\HasChild`
350+
- [Documentation](https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-has-child-query.html)
351+
- [Implementation](https://github.com/Spameri/ElasticQuery/blob/master/src/Query/HasChild.php)
352+
353+
```php
354+
new \Spameri\ElasticQuery\Query\HasChild(
355+
type: 'comment',
356+
query: new \Spameri\ElasticQuery\Query\Term('author', 'john'),
357+
scoreMode: 'max',
358+
minChildren: 1,
359+
);
360+
```
361+
362+
---
363+
343364
## Specialized Queries
344365

345366
##### MatchAll Query

src/Query/HasChild.php

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
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-child-query.html
9+
*/
10+
class HasChild implements \Spameri\ElasticQuery\Query\LeafQueryInterface
11+
{
12+
13+
public function __construct(
14+
private string $type,
15+
private \Spameri\ElasticQuery\Query\LeafQueryInterface $query,
16+
private string|null $scoreMode = null,
17+
private int|null $minChildren = null,
18+
private int|null $maxChildren = null,
19+
private bool|null $ignoreUnmapped = null,
20+
)
21+
{
22+
}
23+
24+
25+
public function key(): string
26+
{
27+
return 'has_child_' . $this->type;
28+
}
29+
30+
31+
/**
32+
* @return array<string, array<string, mixed>>
33+
*/
34+
public function toArray(): array
35+
{
36+
$body = [
37+
'type' => $this->type,
38+
'query' => $this->query->toArray(),
39+
];
40+
41+
if ($this->scoreMode !== null) {
42+
$body['score_mode'] = $this->scoreMode;
43+
}
44+
45+
if ($this->minChildren !== null) {
46+
$body['min_children'] = $this->minChildren;
47+
}
48+
49+
if ($this->maxChildren !== null) {
50+
$body['max_children'] = $this->maxChildren;
51+
}
52+
53+
if ($this->ignoreUnmapped !== null) {
54+
$body['ignore_unmapped'] = $this->ignoreUnmapped;
55+
}
56+
57+
return [
58+
'has_child' => $body,
59+
];
60+
}
61+
62+
}
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace SpameriTests\ElasticQuery\Query;
4+
5+
require_once __DIR__ . '/../../bootstrap.php';
6+
7+
8+
class HasChild extends \Tester\TestCase
9+
{
10+
11+
private const INDEX = 'spameri_test_query_has_child';
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+
$hasChild = new \Spameri\ElasticQuery\Query\HasChild(
29+
type: 'comment',
30+
query: new \Spameri\ElasticQuery\Query\Term('author', 'john'),
31+
scoreMode: 'max',
32+
minChildren: 1,
33+
);
34+
35+
$array = $hasChild->toArray();
36+
37+
\Tester\Assert::same('comment', $array['has_child']['type']);
38+
\Tester\Assert::same('max', $array['has_child']['score_mode']);
39+
\Tester\Assert::same(1, $array['has_child']['min_children']);
40+
}
41+
42+
43+
public function testKey(): void
44+
{
45+
$hasChild = new \Spameri\ElasticQuery\Query\HasChild(
46+
'comment',
47+
new \Spameri\ElasticQuery\Query\Term('author', 'john'),
48+
);
49+
50+
\Tester\Assert::same('has_child_comment', $hasChild->key());
51+
}
52+
53+
54+
public function tearDown(): void
55+
{
56+
$ch = \curl_init();
57+
\curl_setopt($ch, \CURLOPT_URL, \ELASTICSEARCH_HOST . '/' . self::INDEX);
58+
\curl_setopt($ch, \CURLOPT_RETURNTRANSFER, 1);
59+
\curl_setopt($ch, \CURLOPT_CUSTOMREQUEST, 'DELETE');
60+
\curl_setopt($ch, \CURLOPT_HTTPHEADER, ['Content-Type: application/json']);
61+
62+
\curl_exec($ch);
63+
}
64+
65+
}
66+
67+
(new HasChild())->run();

0 commit comments

Comments
 (0)