Skip to content

Commit

Permalink
Add get similar documents method
Browse files Browse the repository at this point in the history
  • Loading branch information
the-sinner committed Jun 10, 2024
1 parent f4eab65 commit eb312ed
Show file tree
Hide file tree
Showing 5 changed files with 308 additions and 0 deletions.
87 changes: 87 additions & 0 deletions src/Contracts/SimilarDocumentsQuery.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
<?php

declare(strict_types=1);

namespace Meilisearch\Contracts;

class SimilarDocumentsQuery
{
private int|string $id;
private ?int $offset = null;
private ?int $limit = null;
private ?string $embedder = null;
private ?array $attributesToRetrieve = null;
private ?bool $showRankingScore = null;
private ?bool $showRankingScoreDetails = null;
private ?array $filter = null;

public function setId(string|int $id): SimilarDocumentsQuery
{
$this->id = $id;

return $this;
}

public function setOffset(?int $offset): SimilarDocumentsQuery
{
$this->offset = $offset;

return $this;
}

public function setLimit(?int $limit): SimilarDocumentsQuery
{
$this->limit = $limit;

return $this;
}

public function setFilter(array $filter): SimilarDocumentsQuery
{
$this->filter = $filter;

return $this;
}

public function setEmbedder(string $embedder): SimilarDocumentsQuery
{
$this->embedder = $embedder;

return $this;
}

public function setAttributesToRetrieve(array $attributesToRetrieve): SimilarDocumentsQuery
{
$this->attributesToRetrieve = $attributesToRetrieve;

return $this;
}

public function setShowRankingScore(?bool $showRankingScore): SimilarDocumentsQuery
{
$this->showRankingScore = $showRankingScore;

return $this;
}

public function setShowRankingScoreDetails(?bool $showRankingScoreDetails): SimilarDocumentsQuery
{
$this->showRankingScoreDetails = $showRankingScoreDetails;

return $this;
}

public function toArray(): array
{
return array_filter([
'id' => $this->id,
'offset' => $this->offset,
'limit' => $this->limit,
'filter' => $this->filter,
'embedder' => $this->embedder,
'attributesToRetrieve' => $this->attributesToRetrieve,
'showRankingScore' => $this->showRankingScore,
'showRankingScoreDetails' => $this->showRankingScoreDetails,
], function ($item) { return null !== $item; });
}
}
9 changes: 9 additions & 0 deletions src/Endpoints/Indexes.php
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
use Meilisearch\Contracts\Index\Settings;
use Meilisearch\Contracts\IndexesQuery;
use Meilisearch\Contracts\IndexesResults;
use Meilisearch\Contracts\SimilarDocumentsQuery;
use Meilisearch\Contracts\TasksQuery;
use Meilisearch\Contracts\TasksResults;
use Meilisearch\Endpoints\Delegates\HandlesDocuments;
Expand All @@ -18,6 +19,7 @@
use Meilisearch\Exceptions\ApiException;
use Meilisearch\Search\FacetSearchResult;
use Meilisearch\Search\SearchResult;
use Meilisearch\Search\SimilarDocumentsSearchResult;

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

public function searchSimilarDocuments(SimilarDocumentsQuery $parameters): SimilarDocumentsSearchResult
{
$result = $this->http->post(self::PATH.'/'.$this->uid.'/similar', $parameters->toArray());

return new SimilarDocumentsSearchResult($result);
}

// Facet Search

public function facetSearch(FacetSearchQuery $params): FacetSearchResult
Expand Down
135 changes: 135 additions & 0 deletions src/Search/SimilarDocumentsSearchResult.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
<?php

declare(strict_types=1);

namespace Meilisearch\Search;

class SimilarDocumentsSearchResult implements \Countable, \IteratorAggregate
{
/**
* @var array<int, array<string, mixed>>
*/
private array $hits;

/**
* `estimatedTotalHits` is the attributes returned by the Meilisearch server
* and its value will not be modified by the methods in this class.
* Please, use `hitsCount` if you want to know the real size of the `hits` array at any time.
*/
private int $estimatedTotalHits;
private int $hitsCount;
private int $offset;
private int $limit;
private int $processingTimeMs;

private string $id;

public function __construct(array $body)
{
$this->id = $body['id'];
$this->hits = $body['hits'] ?? [];
$this->hitsCount = \count($body['hits']);
$this->processingTimeMs = $body['processingTimeMs'];
$this->offset = $body['offset'];
$this->limit = $body['limit'];
$this->estimatedTotalHits = $body['estimatedTotalHits'];
}

/**
* Return a new {@see SearchResult} instance.
*
* The $options parameter is an array, and the following keys are accepted:
* - transformHits (callable)
*
* The method does NOT trigger a new search.
*/
public function applyOptions($options): self
{
if (\array_key_exists('transformHits', $options) && \is_callable($options['transformHits'])) {
$this->transformHits($options['transformHits']);
}

return $this;
}

public function transformHits(callable $callback): self
{
$this->hits = $callback($this->hits);
$this->hitsCount = \count($this->hits);

return $this;
}

public function getHit(int $key, $default = null)
{
return $this->hits[$key] ?? $default;
}

/**
* @return array<int, array>
*/
public function getHits(): array
{
return $this->hits;
}

public function getOffset(): int
{
return $this->offset;
}

public function getLimit(): int
{
return $this->limit;
}

public function getEstimatedTotalHits(): int
{
return $this->estimatedTotalHits;
}

public function getProcessingTimeMs(): int
{
return $this->processingTimeMs;
}

public function getId(): string
{
return $this->id;
}

public function getHitsCount(): int
{
return $this->hitsCount;
}

public function toArray(): array
{
$arr = [
'id' => $this->id,
'hits' => $this->hits,
'hitsCount' => $this->hitsCount,
'processingTimeMs' => $this->processingTimeMs,
'offset' => $this->offset,
'limit' => $this->limit,
'estimatedTotalHits' => $this->estimatedTotalHits,
];

return $arr;
}

public function toJSON(): string
{
return json_encode($this->toArray(), JSON_PRETTY_PRINT);
}

public function getIterator(): \ArrayIterator
{
return new \ArrayIterator($this->hits);
}

public function count(): int
{
return $this->hitsCount;
}
}
44 changes: 44 additions & 0 deletions tests/Endpoints/SimilarDocumentsTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
<?php

declare(strict_types=1);

namespace Tests\Endpoints;

use Meilisearch\Contracts\SimilarDocumentsQuery;
use Meilisearch\Endpoints\Indexes;
use Tests\TestCase;

final class SimilarDocumentsTest extends TestCase
{
private Indexes $index;

protected function setUp(): void
{
parent::setUp();

$this->index = $this->createEmptyIndex($this->safeIndexName());
$this->index->updateDocuments(self::VECTOR_MOVIES);
}

public function testBasicSearchWithSimilarDocuments(): void
{
$task = $this->index->updateSettings(['embedders' => ['manual' => ['source' => 'userProvided', 'dimensions' => 3]]]);
$this->client->waitForTask($task['taskUid']);

$response = $this->index->search('room');

self::assertEquals(1, $response->getHitsCount());

$documentId = $response->getHit(0)['id'];
$response = $this->index->searchSimilarDocuments(
(new SimilarDocumentsQuery())
->setId($documentId)
);

self::assertNotNull($response);

Check failure on line 38 in tests/Endpoints/SimilarDocumentsTest.php

View workflow job for this annotation

GitHub Actions / phpstan-tests

Call to static method PHPUnit\Framework\Assert::assertNotNull() with Meilisearch\Search\SimilarDocumentsSearchResult will always evaluate to true.
self::assertGreaterThanOrEqual(4, $response->getHitsCount());
self::assertArrayHasKey('_vectors', $response->getHit(0));
self::assertArrayHasKey('id', $response->getHit(0));
self::assertSame($documentId, $response->getId());
}
}
33 changes: 33 additions & 0 deletions tests/TestCase.php
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,39 @@

abstract class TestCase extends BaseTestCase
{
protected const VECTOR_MOVIES = [
[
'title' => 'Shazam!',
'release_year' => 2019,
'id' => '287947',
'_vectors' => ['manual' => [0.8, 0.4, -0.5]],
],
[
'title' => 'Captain Marvel',
'release_year' => 2019,
'id' => '299537',
'_vectors' => ['manual' => [0.6, 0.8, -0.2]],
],
[
'title' => 'Escape Room',
'release_year' => 2019,
'id' => '522681',
'_vectors' => ['manual' => [0.1, 0.6, 0.8]],
],
[
'title' => 'How to Train Your Dragon: The Hidden World',
'release_year' => 2019,
'id' => '166428',
'_vectors' => ['manual' => [0.7, 0.7, -0.4]],
],
[
'title' => 'All Quiet on the Western Front',
'release_year' => 1930,
'id' => '143',
'_vectors' => ['manual' => [-0.5, 0.3, 0.85]],
],
];

protected const DOCUMENTS = [
['id' => 123, 'title' => 'Pride and Prejudice', 'comment' => 'A great book', 'genre' => 'romance'],
['id' => 456, 'title' => 'Le Petit Prince', 'comment' => 'A french book', 'genre' => 'adventure'],
Expand Down

0 comments on commit eb312ed

Please sign in to comment.