Skip to content

Commit 17992b6

Browse files
Spamerczclaude
andcommitted
feat(query): add span near query
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent d0eecef commit 17992b6

3 files changed

Lines changed: 146 additions & 0 deletions

File tree

doc/02-query-objects.md

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -614,6 +614,21 @@ new \Spameri\ElasticQuery\Query\SpanFirst(
614614
);
615615
```
616616

617+
##### SpanNear Query
618+
Match multiple span clauses within `slop` positions of each other.
619+
- Class: `\Spameri\ElasticQuery\Query\SpanNear`
620+
- [Documentation](https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-span-near-query.html)
621+
- [Implementation](https://github.com/Spameri/ElasticQuery/blob/master/src/Query/SpanNear.php)
622+
623+
```php
624+
$span = new \Spameri\ElasticQuery\Query\SpanNear(
625+
new \Spameri\ElasticQuery\Query\SpanTerm('field', 'value1'),
626+
slop: 12,
627+
inOrder: false,
628+
);
629+
$span->addClause(new \Spameri\ElasticQuery\Query\SpanTerm('field', 'value2'));
630+
```
631+
617632
##### SpanTerm Query
618633
Match a single term in a span-aware way.
619634
- Class: `\Spameri\ElasticQuery\Query\SpanTerm`

src/Query/SpanNear.php

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
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-span-near-query.html
9+
*/
10+
class SpanNear implements \Spameri\ElasticQuery\Query\LeafQueryInterface
11+
{
12+
13+
/**
14+
* @var array<int, \Spameri\ElasticQuery\Query\LeafQueryInterface>
15+
*/
16+
private array $clauses;
17+
18+
19+
public function __construct(
20+
\Spameri\ElasticQuery\Query\LeafQueryInterface $clause,
21+
private int $slop = 0,
22+
private bool $inOrder = true,
23+
)
24+
{
25+
$this->clauses = [$clause];
26+
}
27+
28+
29+
public function addClause(\Spameri\ElasticQuery\Query\LeafQueryInterface $clause): void
30+
{
31+
$this->clauses[] = $clause;
32+
}
33+
34+
35+
public function key(): string
36+
{
37+
$keys = [];
38+
foreach ($this->clauses as $clause) {
39+
$keys[] = $clause->key();
40+
}
41+
42+
return 'span_near_' . \implode('-', $keys);
43+
}
44+
45+
46+
/**
47+
* @return array<string, array<string, mixed>>
48+
*/
49+
public function toArray(): array
50+
{
51+
$clauses = [];
52+
foreach ($this->clauses as $clause) {
53+
$clauses[] = $clause->toArray();
54+
}
55+
56+
return [
57+
'span_near' => [
58+
'clauses' => $clauses,
59+
'slop' => $this->slop,
60+
'in_order' => $this->inOrder,
61+
],
62+
];
63+
}
64+
65+
}
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace SpameriTests\ElasticQuery\Query;
4+
5+
require_once __DIR__ . '/../../bootstrap.php';
6+
7+
8+
class SpanNear extends \Tester\TestCase
9+
{
10+
11+
private const INDEX = 'spameri_test_query_span_near';
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+
$span = new \Spameri\ElasticQuery\Query\SpanNear(
29+
new \Spameri\ElasticQuery\Query\SpanTerm('field', 'value1'),
30+
slop: 12,
31+
inOrder: false,
32+
);
33+
$span->addClause(new \Spameri\ElasticQuery\Query\SpanTerm('field', 'value2'));
34+
35+
$array = $span->toArray();
36+
37+
\Tester\Assert::same(12, $array['span_near']['slop']);
38+
\Tester\Assert::false($array['span_near']['in_order']);
39+
\Tester\Assert::count(2, $array['span_near']['clauses']);
40+
}
41+
42+
43+
public function testKey(): void
44+
{
45+
$span = new \Spameri\ElasticQuery\Query\SpanNear(
46+
new \Spameri\ElasticQuery\Query\SpanTerm('field', 'value1'),
47+
);
48+
49+
\Tester\Assert::same('span_near_span_term_field_value1', $span->key());
50+
}
51+
52+
53+
public function tearDown(): void
54+
{
55+
$ch = \curl_init();
56+
\curl_setopt($ch, \CURLOPT_URL, \ELASTICSEARCH_HOST . '/' . self::INDEX);
57+
\curl_setopt($ch, \CURLOPT_RETURNTRANSFER, 1);
58+
\curl_setopt($ch, \CURLOPT_CUSTOMREQUEST, 'DELETE');
59+
\curl_setopt($ch, \CURLOPT_HTTPHEADER, ['Content-Type: application/json']);
60+
61+
\curl_exec($ch);
62+
}
63+
64+
}
65+
66+
(new SpanNear())->run();

0 commit comments

Comments
 (0)