Skip to content

Commit dffc919

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

3 files changed

Lines changed: 140 additions & 0 deletions

File tree

doc/02-query-objects.md

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -642,6 +642,21 @@ $span = new \Spameri\ElasticQuery\Query\SpanOr(
642642
$span->addClause(new \Spameri\ElasticQuery\Query\SpanTerm('field', 'value2'));
643643
```
644644

645+
##### SpanNot Query
646+
Match `include` spans not overlapping `exclude` spans.
647+
- Class: `\Spameri\ElasticQuery\Query\SpanNot`
648+
- [Documentation](https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-span-not-query.html)
649+
- [Implementation](https://github.com/Spameri/ElasticQuery/blob/master/src/Query/SpanNot.php)
650+
651+
```php
652+
new \Spameri\ElasticQuery\Query\SpanNot(
653+
include: new \Spameri\ElasticQuery\Query\SpanTerm('field', 'hot'),
654+
exclude: new \Spameri\ElasticQuery\Query\SpanTerm('field', 'dog'),
655+
pre: 0,
656+
post: 1,
657+
);
658+
```
659+
645660
##### SpanTerm Query
646661
Match a single term in a span-aware way.
647662
- Class: `\Spameri\ElasticQuery\Query\SpanTerm`

src/Query/SpanNot.php

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
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-not-query.html
9+
*/
10+
class SpanNot implements \Spameri\ElasticQuery\Query\LeafQueryInterface
11+
{
12+
13+
public function __construct(
14+
private \Spameri\ElasticQuery\Query\LeafQueryInterface $include,
15+
private \Spameri\ElasticQuery\Query\LeafQueryInterface $exclude,
16+
private int|null $pre = null,
17+
private int|null $post = null,
18+
private int|null $dist = null,
19+
)
20+
{
21+
}
22+
23+
24+
public function key(): string
25+
{
26+
return 'span_not_' . $this->include->key() . '_' . $this->exclude->key();
27+
}
28+
29+
30+
/**
31+
* @return array<string, array<string, mixed>>
32+
*/
33+
public function toArray(): array
34+
{
35+
$body = [
36+
'include' => $this->include->toArray(),
37+
'exclude' => $this->exclude->toArray(),
38+
];
39+
40+
if ($this->pre !== null) {
41+
$body['pre'] = $this->pre;
42+
}
43+
44+
if ($this->post !== null) {
45+
$body['post'] = $this->post;
46+
}
47+
48+
if ($this->dist !== null) {
49+
$body['dist'] = $this->dist;
50+
}
51+
52+
return [
53+
'span_not' => $body,
54+
];
55+
}
56+
57+
}
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace SpameriTests\ElasticQuery\Query;
4+
5+
require_once __DIR__ . '/../../bootstrap.php';
6+
7+
8+
class SpanNot extends \Tester\TestCase
9+
{
10+
11+
private const INDEX = 'spameri_test_query_span_not';
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\SpanNot(
29+
include: new \Spameri\ElasticQuery\Query\SpanTerm('field', 'hot'),
30+
exclude: new \Spameri\ElasticQuery\Query\SpanTerm('field', 'dog'),
31+
pre: 0,
32+
post: 1,
33+
);
34+
35+
$array = $span->toArray();
36+
37+
\Tester\Assert::same(0, $array['span_not']['pre']);
38+
\Tester\Assert::same(1, $array['span_not']['post']);
39+
\Tester\Assert::true(isset($array['span_not']['include']));
40+
\Tester\Assert::true(isset($array['span_not']['exclude']));
41+
}
42+
43+
44+
public function testKey(): void
45+
{
46+
$span = new \Spameri\ElasticQuery\Query\SpanNot(
47+
new \Spameri\ElasticQuery\Query\SpanTerm('field', 'hot'),
48+
new \Spameri\ElasticQuery\Query\SpanTerm('field', 'dog'),
49+
);
50+
51+
\Tester\Assert::same('span_not_span_term_field_hot_span_term_field_dog', $span->key());
52+
}
53+
54+
55+
public function tearDown(): void
56+
{
57+
$ch = \curl_init();
58+
\curl_setopt($ch, \CURLOPT_URL, \ELASTICSEARCH_HOST . '/' . self::INDEX);
59+
\curl_setopt($ch, \CURLOPT_RETURNTRANSFER, 1);
60+
\curl_setopt($ch, \CURLOPT_CUSTOMREQUEST, 'DELETE');
61+
\curl_setopt($ch, \CURLOPT_HTTPHEADER, ['Content-Type: application/json']);
62+
63+
\curl_exec($ch);
64+
}
65+
66+
}
67+
68+
(new SpanNot())->run();

0 commit comments

Comments
 (0)