Skip to content

Commit 6e24431

Browse files
Spamerczclaude
andcommitted
feat(query): add more like this query
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent d64cf17 commit 6e24431

3 files changed

Lines changed: 178 additions & 0 deletions

File tree

doc/02-query-objects.md

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -504,6 +504,21 @@ new \Spameri\ElasticQuery\Query\ScriptScore(
504504
);
505505
```
506506

507+
##### MoreLikeThis Query
508+
Find documents similar to provided text or document references.
509+
- Class: `\Spameri\ElasticQuery\Query\MoreLikeThis`
510+
- [Documentation](https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-mlt-query.html)
511+
- [Implementation](https://github.com/Spameri/ElasticQuery/blob/master/src/Query/MoreLikeThis.php)
512+
513+
```php
514+
new \Spameri\ElasticQuery\Query\MoreLikeThis(
515+
fields: ['title', 'body'],
516+
like: ['quick brown fox', ['_index' => 'imdb', '_id' => '1']],
517+
minTermFreq: 1,
518+
maxQueryTerms: 12,
519+
);
520+
```
521+
507522
##### Script Query
508523
Filter documents with a Painless boolean script.
509524
- Class: `\Spameri\ElasticQuery\Query\Script`

src/Query/MoreLikeThis.php

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
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-mlt-query.html
9+
*/
10+
class MoreLikeThis implements \Spameri\ElasticQuery\Query\LeafQueryInterface
11+
{
12+
13+
/**
14+
* @param array<int, string> $fields
15+
* @param array<int, string|array<string, mixed>> $like Texts or doc refs (['_index' => ..., '_id' => ...]).
16+
* @param array<int, string|array<string, mixed>> $unlike
17+
*/
18+
public function __construct(
19+
private array $fields,
20+
private array $like,
21+
private array $unlike = [],
22+
private int|null $minTermFreq = null,
23+
private int|null $maxQueryTerms = null,
24+
private int|string|null $minimumShouldMatch = null,
25+
)
26+
{
27+
if ($fields === []) {
28+
throw new \Spameri\ElasticQuery\Exception\InvalidArgumentException(
29+
'MoreLikeThis query requires at least one field.',
30+
);
31+
}
32+
33+
if ($like === []) {
34+
throw new \Spameri\ElasticQuery\Exception\InvalidArgumentException(
35+
'MoreLikeThis query requires at least one like value.',
36+
);
37+
}
38+
}
39+
40+
41+
public function key(): string
42+
{
43+
return 'more_like_this_' . \implode('-', $this->fields);
44+
}
45+
46+
47+
/**
48+
* @return array<string, array<string, mixed>>
49+
*/
50+
public function toArray(): array
51+
{
52+
$body = [
53+
'fields' => $this->fields,
54+
'like' => $this->like,
55+
];
56+
57+
if ($this->unlike !== []) {
58+
$body['unlike'] = $this->unlike;
59+
}
60+
61+
if ($this->minTermFreq !== null) {
62+
$body['min_term_freq'] = $this->minTermFreq;
63+
}
64+
65+
if ($this->maxQueryTerms !== null) {
66+
$body['max_query_terms'] = $this->maxQueryTerms;
67+
}
68+
69+
if ($this->minimumShouldMatch !== null) {
70+
$body['minimum_should_match'] = $this->minimumShouldMatch;
71+
}
72+
73+
return [
74+
'more_like_this' => $body,
75+
];
76+
}
77+
78+
}
Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace SpameriTests\ElasticQuery\Query;
4+
5+
require_once __DIR__ . '/../../bootstrap.php';
6+
7+
8+
class MoreLikeThis extends \Tester\TestCase
9+
{
10+
11+
private const INDEX = 'spameri_test_query_mlt';
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+
$mlt = new \Spameri\ElasticQuery\Query\MoreLikeThis(
29+
fields: ['title', 'body'],
30+
like: ['quick brown fox', ['_index' => 'imdb', '_id' => '1']],
31+
minTermFreq: 1,
32+
maxQueryTerms: 12,
33+
);
34+
35+
$array = $mlt->toArray();
36+
37+
\Tester\Assert::same(['title', 'body'], $array['more_like_this']['fields']);
38+
\Tester\Assert::same(1, $array['more_like_this']['min_term_freq']);
39+
}
40+
41+
42+
public function testRequiresFields(): void
43+
{
44+
\Tester\Assert::exception(
45+
static function (): void {
46+
new \Spameri\ElasticQuery\Query\MoreLikeThis([], ['foo']);
47+
},
48+
\Spameri\ElasticQuery\Exception\InvalidArgumentException::class,
49+
);
50+
}
51+
52+
53+
public function testRequiresLike(): void
54+
{
55+
\Tester\Assert::exception(
56+
static function (): void {
57+
new \Spameri\ElasticQuery\Query\MoreLikeThis(['f'], []);
58+
},
59+
\Spameri\ElasticQuery\Exception\InvalidArgumentException::class,
60+
);
61+
}
62+
63+
64+
public function testKey(): void
65+
{
66+
$mlt = new \Spameri\ElasticQuery\Query\MoreLikeThis(['title'], ['foo']);
67+
68+
\Tester\Assert::same('more_like_this_title', $mlt->key());
69+
}
70+
71+
72+
public function tearDown(): void
73+
{
74+
$ch = \curl_init();
75+
\curl_setopt($ch, \CURLOPT_URL, \ELASTICSEARCH_HOST . '/' . self::INDEX);
76+
\curl_setopt($ch, \CURLOPT_RETURNTRANSFER, 1);
77+
\curl_setopt($ch, \CURLOPT_CUSTOMREQUEST, 'DELETE');
78+
\curl_setopt($ch, \CURLOPT_HTTPHEADER, ['Content-Type: application/json']);
79+
80+
\curl_exec($ch);
81+
}
82+
83+
}
84+
85+
(new MoreLikeThis())->run();

0 commit comments

Comments
 (0)