Skip to content

Commit eb312ed

Browse files
committed
Add get similar documents method
1 parent f4eab65 commit eb312ed

File tree

5 files changed

+308
-0
lines changed

5 files changed

+308
-0
lines changed
Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Meilisearch\Contracts;
6+
7+
class SimilarDocumentsQuery
8+
{
9+
private int|string $id;
10+
private ?int $offset = null;
11+
private ?int $limit = null;
12+
private ?string $embedder = null;
13+
private ?array $attributesToRetrieve = null;
14+
private ?bool $showRankingScore = null;
15+
private ?bool $showRankingScoreDetails = null;
16+
private ?array $filter = null;
17+
18+
public function setId(string|int $id): SimilarDocumentsQuery
19+
{
20+
$this->id = $id;
21+
22+
return $this;
23+
}
24+
25+
public function setOffset(?int $offset): SimilarDocumentsQuery
26+
{
27+
$this->offset = $offset;
28+
29+
return $this;
30+
}
31+
32+
public function setLimit(?int $limit): SimilarDocumentsQuery
33+
{
34+
$this->limit = $limit;
35+
36+
return $this;
37+
}
38+
39+
public function setFilter(array $filter): SimilarDocumentsQuery
40+
{
41+
$this->filter = $filter;
42+
43+
return $this;
44+
}
45+
46+
public function setEmbedder(string $embedder): SimilarDocumentsQuery
47+
{
48+
$this->embedder = $embedder;
49+
50+
return $this;
51+
}
52+
53+
public function setAttributesToRetrieve(array $attributesToRetrieve): SimilarDocumentsQuery
54+
{
55+
$this->attributesToRetrieve = $attributesToRetrieve;
56+
57+
return $this;
58+
}
59+
60+
public function setShowRankingScore(?bool $showRankingScore): SimilarDocumentsQuery
61+
{
62+
$this->showRankingScore = $showRankingScore;
63+
64+
return $this;
65+
}
66+
67+
public function setShowRankingScoreDetails(?bool $showRankingScoreDetails): SimilarDocumentsQuery
68+
{
69+
$this->showRankingScoreDetails = $showRankingScoreDetails;
70+
71+
return $this;
72+
}
73+
74+
public function toArray(): array
75+
{
76+
return array_filter([
77+
'id' => $this->id,
78+
'offset' => $this->offset,
79+
'limit' => $this->limit,
80+
'filter' => $this->filter,
81+
'embedder' => $this->embedder,
82+
'attributesToRetrieve' => $this->attributesToRetrieve,
83+
'showRankingScore' => $this->showRankingScore,
84+
'showRankingScoreDetails' => $this->showRankingScoreDetails,
85+
], function ($item) { return null !== $item; });
86+
}
87+
}

src/Endpoints/Indexes.php

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
use Meilisearch\Contracts\Index\Settings;
1111
use Meilisearch\Contracts\IndexesQuery;
1212
use Meilisearch\Contracts\IndexesResults;
13+
use Meilisearch\Contracts\SimilarDocumentsQuery;
1314
use Meilisearch\Contracts\TasksQuery;
1415
use Meilisearch\Contracts\TasksResults;
1516
use Meilisearch\Endpoints\Delegates\HandlesDocuments;
@@ -18,6 +19,7 @@
1819
use Meilisearch\Exceptions\ApiException;
1920
use Meilisearch\Search\FacetSearchResult;
2021
use Meilisearch\Search\SearchResult;
22+
use Meilisearch\Search\SimilarDocumentsSearchResult;
2123

2224
class Indexes extends Endpoint
2325
{
@@ -213,6 +215,13 @@ public function rawSearch(?string $query, array $searchParams = []): array
213215
return $result;
214216
}
215217

218+
public function searchSimilarDocuments(SimilarDocumentsQuery $parameters): SimilarDocumentsSearchResult
219+
{
220+
$result = $this->http->post(self::PATH.'/'.$this->uid.'/similar', $parameters->toArray());
221+
222+
return new SimilarDocumentsSearchResult($result);
223+
}
224+
216225
// Facet Search
217226

218227
public function facetSearch(FacetSearchQuery $params): FacetSearchResult
Lines changed: 135 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,135 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Meilisearch\Search;
6+
7+
class SimilarDocumentsSearchResult implements \Countable, \IteratorAggregate
8+
{
9+
/**
10+
* @var array<int, array<string, mixed>>
11+
*/
12+
private array $hits;
13+
14+
/**
15+
* `estimatedTotalHits` is the attributes returned by the Meilisearch server
16+
* and its value will not be modified by the methods in this class.
17+
* Please, use `hitsCount` if you want to know the real size of the `hits` array at any time.
18+
*/
19+
private int $estimatedTotalHits;
20+
private int $hitsCount;
21+
private int $offset;
22+
private int $limit;
23+
private int $processingTimeMs;
24+
25+
private string $id;
26+
27+
public function __construct(array $body)
28+
{
29+
$this->id = $body['id'];
30+
$this->hits = $body['hits'] ?? [];
31+
$this->hitsCount = \count($body['hits']);
32+
$this->processingTimeMs = $body['processingTimeMs'];
33+
$this->offset = $body['offset'];
34+
$this->limit = $body['limit'];
35+
$this->estimatedTotalHits = $body['estimatedTotalHits'];
36+
}
37+
38+
/**
39+
* Return a new {@see SearchResult} instance.
40+
*
41+
* The $options parameter is an array, and the following keys are accepted:
42+
* - transformHits (callable)
43+
*
44+
* The method does NOT trigger a new search.
45+
*/
46+
public function applyOptions($options): self
47+
{
48+
if (\array_key_exists('transformHits', $options) && \is_callable($options['transformHits'])) {
49+
$this->transformHits($options['transformHits']);
50+
}
51+
52+
return $this;
53+
}
54+
55+
public function transformHits(callable $callback): self
56+
{
57+
$this->hits = $callback($this->hits);
58+
$this->hitsCount = \count($this->hits);
59+
60+
return $this;
61+
}
62+
63+
public function getHit(int $key, $default = null)
64+
{
65+
return $this->hits[$key] ?? $default;
66+
}
67+
68+
/**
69+
* @return array<int, array>
70+
*/
71+
public function getHits(): array
72+
{
73+
return $this->hits;
74+
}
75+
76+
public function getOffset(): int
77+
{
78+
return $this->offset;
79+
}
80+
81+
public function getLimit(): int
82+
{
83+
return $this->limit;
84+
}
85+
86+
public function getEstimatedTotalHits(): int
87+
{
88+
return $this->estimatedTotalHits;
89+
}
90+
91+
public function getProcessingTimeMs(): int
92+
{
93+
return $this->processingTimeMs;
94+
}
95+
96+
public function getId(): string
97+
{
98+
return $this->id;
99+
}
100+
101+
public function getHitsCount(): int
102+
{
103+
return $this->hitsCount;
104+
}
105+
106+
public function toArray(): array
107+
{
108+
$arr = [
109+
'id' => $this->id,
110+
'hits' => $this->hits,
111+
'hitsCount' => $this->hitsCount,
112+
'processingTimeMs' => $this->processingTimeMs,
113+
'offset' => $this->offset,
114+
'limit' => $this->limit,
115+
'estimatedTotalHits' => $this->estimatedTotalHits,
116+
];
117+
118+
return $arr;
119+
}
120+
121+
public function toJSON(): string
122+
{
123+
return json_encode($this->toArray(), JSON_PRETTY_PRINT);
124+
}
125+
126+
public function getIterator(): \ArrayIterator
127+
{
128+
return new \ArrayIterator($this->hits);
129+
}
130+
131+
public function count(): int
132+
{
133+
return $this->hitsCount;
134+
}
135+
}
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Tests\Endpoints;
6+
7+
use Meilisearch\Contracts\SimilarDocumentsQuery;
8+
use Meilisearch\Endpoints\Indexes;
9+
use Tests\TestCase;
10+
11+
final class SimilarDocumentsTest extends TestCase
12+
{
13+
private Indexes $index;
14+
15+
protected function setUp(): void
16+
{
17+
parent::setUp();
18+
19+
$this->index = $this->createEmptyIndex($this->safeIndexName());
20+
$this->index->updateDocuments(self::VECTOR_MOVIES);
21+
}
22+
23+
public function testBasicSearchWithSimilarDocuments(): void
24+
{
25+
$task = $this->index->updateSettings(['embedders' => ['manual' => ['source' => 'userProvided', 'dimensions' => 3]]]);
26+
$this->client->waitForTask($task['taskUid']);
27+
28+
$response = $this->index->search('room');
29+
30+
self::assertEquals(1, $response->getHitsCount());
31+
32+
$documentId = $response->getHit(0)['id'];
33+
$response = $this->index->searchSimilarDocuments(
34+
(new SimilarDocumentsQuery())
35+
->setId($documentId)
36+
);
37+
38+
self::assertNotNull($response);
39+
self::assertGreaterThanOrEqual(4, $response->getHitsCount());
40+
self::assertArrayHasKey('_vectors', $response->getHit(0));
41+
self::assertArrayHasKey('id', $response->getHit(0));
42+
self::assertSame($documentId, $response->getId());
43+
}
44+
}

tests/TestCase.php

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,39 @@
1515

1616
abstract class TestCase extends BaseTestCase
1717
{
18+
protected const VECTOR_MOVIES = [
19+
[
20+
'title' => 'Shazam!',
21+
'release_year' => 2019,
22+
'id' => '287947',
23+
'_vectors' => ['manual' => [0.8, 0.4, -0.5]],
24+
],
25+
[
26+
'title' => 'Captain Marvel',
27+
'release_year' => 2019,
28+
'id' => '299537',
29+
'_vectors' => ['manual' => [0.6, 0.8, -0.2]],
30+
],
31+
[
32+
'title' => 'Escape Room',
33+
'release_year' => 2019,
34+
'id' => '522681',
35+
'_vectors' => ['manual' => [0.1, 0.6, 0.8]],
36+
],
37+
[
38+
'title' => 'How to Train Your Dragon: The Hidden World',
39+
'release_year' => 2019,
40+
'id' => '166428',
41+
'_vectors' => ['manual' => [0.7, 0.7, -0.4]],
42+
],
43+
[
44+
'title' => 'All Quiet on the Western Front',
45+
'release_year' => 1930,
46+
'id' => '143',
47+
'_vectors' => ['manual' => [-0.5, 0.3, 0.85]],
48+
],
49+
];
50+
1851
protected const DOCUMENTS = [
1952
['id' => 123, 'title' => 'Pride and Prejudice', 'comment' => 'A great book', 'genre' => 'romance'],
2053
['id' => 456, 'title' => 'Le Petit Prince', 'comment' => 'A french book', 'genre' => 'adventure'],

0 commit comments

Comments
 (0)