Skip to content

Commit 8da126d

Browse files
Spamerczclaude
andcommitted
feat(query): add pinned query
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent f354b6b commit 8da126d

3 files changed

Lines changed: 149 additions & 0 deletions

File tree

doc/02-query-objects.md

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -546,6 +546,19 @@ new \Spameri\ElasticQuery\Query\DistanceFeature(
546546
);
547547
```
548548

549+
##### Pinned Query
550+
Promote specific document IDs above an organic query's results.
551+
- Class: `\Spameri\ElasticQuery\Query\Pinned`
552+
- [Documentation](https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-pinned-query.html)
553+
- [Implementation](https://github.com/Spameri/ElasticQuery/blob/master/src/Query/Pinned.php)
554+
555+
```php
556+
new \Spameri\ElasticQuery\Query\Pinned(
557+
organic: new \Spameri\ElasticQuery\Query\ElasticMatch('content', 'elasticsearch'),
558+
ids: ['1', '4', '100'],
559+
);
560+
```
561+
549562
##### Script Query
550563
Filter documents with a Painless boolean script.
551564
- Class: `\Spameri\ElasticQuery\Query\Script`

src/Query/Pinned.php

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
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-pinned-query.html
9+
*/
10+
class Pinned implements \Spameri\ElasticQuery\Query\LeafQueryInterface
11+
{
12+
13+
/**
14+
* @param array<int, string> $ids IDs to pin (use either $ids or $docs).
15+
* @param array<int, array<string, string>> $docs Document references: [['_index' => ..., '_id' => ...]].
16+
*/
17+
public function __construct(
18+
private \Spameri\ElasticQuery\Query\LeafQueryInterface $organic,
19+
private array $ids = [],
20+
private array $docs = [],
21+
)
22+
{
23+
if ($ids === [] && $docs === []) {
24+
throw new \Spameri\ElasticQuery\Exception\InvalidArgumentException(
25+
'Pinned query requires either ids or docs to pin.',
26+
);
27+
}
28+
}
29+
30+
31+
public function key(): string
32+
{
33+
return 'pinned_' . $this->organic->key();
34+
}
35+
36+
37+
/**
38+
* @return array<string, array<string, mixed>>
39+
*/
40+
public function toArray(): array
41+
{
42+
$body = [
43+
'organic' => $this->organic->toArray(),
44+
];
45+
46+
if ($this->ids !== []) {
47+
$body['ids'] = $this->ids;
48+
}
49+
50+
if ($this->docs !== []) {
51+
$body['docs'] = $this->docs;
52+
}
53+
54+
return [
55+
'pinned' => $body,
56+
];
57+
}
58+
59+
}
Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace SpameriTests\ElasticQuery\Query;
4+
5+
require_once __DIR__ . '/../../bootstrap.php';
6+
7+
8+
class Pinned extends \Tester\TestCase
9+
{
10+
11+
private const INDEX = 'spameri_test_query_pinned';
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+
$pinned = new \Spameri\ElasticQuery\Query\Pinned(
29+
organic: new \Spameri\ElasticQuery\Query\ElasticMatch('content', 'elasticsearch'),
30+
ids: ['1', '4', '100'],
31+
);
32+
33+
$array = $pinned->toArray();
34+
35+
\Tester\Assert::same(['1', '4', '100'], $array['pinned']['ids']);
36+
\Tester\Assert::true(isset($array['pinned']['organic']['match']));
37+
}
38+
39+
40+
public function testRequiresIdsOrDocs(): void
41+
{
42+
\Tester\Assert::exception(
43+
static function (): void {
44+
new \Spameri\ElasticQuery\Query\Pinned(
45+
new \Spameri\ElasticQuery\Query\MatchAll(),
46+
);
47+
},
48+
\Spameri\ElasticQuery\Exception\InvalidArgumentException::class,
49+
);
50+
}
51+
52+
53+
public function testKey(): void
54+
{
55+
$pinned = new \Spameri\ElasticQuery\Query\Pinned(
56+
organic: new \Spameri\ElasticQuery\Query\MatchAll(),
57+
ids: ['1'],
58+
);
59+
60+
\Tester\Assert::same('pinned_match_all', $pinned->key());
61+
}
62+
63+
64+
public function tearDown(): void
65+
{
66+
$ch = \curl_init();
67+
\curl_setopt($ch, \CURLOPT_URL, \ELASTICSEARCH_HOST . '/' . self::INDEX);
68+
\curl_setopt($ch, \CURLOPT_RETURNTRANSFER, 1);
69+
\curl_setopt($ch, \CURLOPT_CUSTOMREQUEST, 'DELETE');
70+
\curl_setopt($ch, \CURLOPT_HTTPHEADER, ['Content-Type: application/json']);
71+
72+
\curl_exec($ch);
73+
}
74+
75+
}
76+
77+
(new Pinned())->run();

0 commit comments

Comments
 (0)