Skip to content

Commit

Permalink
Add get similar document method (#645)
Browse files Browse the repository at this point in the history
* Add get similar documents method

* Remove null from rankingScoreThreshold

* Update src/Contracts/SimilarDocumentsQuery.php

Co-authored-by: Tomas Norkūnas <[email protected]>

* Update src/Contracts/SimilarDocumentsQuery.php

Co-authored-by: Tomas Norkūnas <[email protected]>

---------

Co-authored-by: Clémentine <[email protected]>
Co-authored-by: Tomas Norkūnas <[email protected]>
  • Loading branch information
3 people authored Jun 18, 2024
1 parent 9645ddf commit bbc1116
Show file tree
Hide file tree
Showing 6 changed files with 310 additions and 1 deletion.
2 changes: 1 addition & 1 deletion src/Contracts/SearchQuery.php
Original file line number Diff line number Diff line change
Expand Up @@ -238,7 +238,7 @@ public function toArray(): array
'attributesToSearchOn' => $this->attributesToSearchOn,
'showRankingScore' => $this->showRankingScore,
'showRankingScoreDetails' => $this->showRankingScoreDetails,
'rankingScoreThreshold' => $this->rankingScoreThreshold ?? null,
'rankingScoreThreshold' => $this->rankingScoreThreshold,
], function ($item) { return null !== $item; });
}
}
117 changes: 117 additions & 0 deletions src/Contracts/SimilarDocumentsQuery.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
<?php

declare(strict_types=1);

namespace Meilisearch\Contracts;

class SimilarDocumentsQuery
{
/**
* @var int|string
*/
private $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;

/**
* @param int|string $id
*/
public function __construct($id)
{
$this->id = $id;
}

/**
* @param non-negative-int $offset
*/
public function setOffset(?int $offset): SimilarDocumentsQuery
{
$this->offset = $offset;

return $this;
}

/**
* @param positive-int $limit
*/
public function setLimit(?int $limit): SimilarDocumentsQuery
{
$this->limit = $limit;

return $this;
}

/**
* @param array<int, array<int, string>|string> $filter an array of arrays representing filter conditions
*/
public function setFilter(array $filter): SimilarDocumentsQuery
{
$this->filter = $filter;

return $this;
}

/**
* @param non-empty-string $embedder
*/
public function setEmbedder(string $embedder): SimilarDocumentsQuery
{
$this->embedder = $embedder;

return $this;
}

/**
* @param list<non-empty-string> $attributesToRetrieve an array of attribute names to retrieve
*/
public function setAttributesToRetrieve(array $attributesToRetrieve): SimilarDocumentsQuery
{
$this->attributesToRetrieve = $attributesToRetrieve;

return $this;
}

/**
* @param bool|null $showRankingScore boolean value to show ranking score
*/
public function setShowRankingScore(?bool $showRankingScore): SimilarDocumentsQuery
{
$this->showRankingScore = $showRankingScore;

return $this;
}

/**
* @param bool|null $showRankingScoreDetails boolean value to show ranking score details
*/
public function setShowRankingScoreDetails(?bool $showRankingScoreDetails): SimilarDocumentsQuery
{
$this->showRankingScoreDetails = $showRankingScoreDetails;

return $this;
}

/**
* @return array{id: int|string, offset: non-negative-int, limit: positive-int, filter: array<int, array<int, string>|string>, embedder: non-empty-string, attributesToRetrieve: list<non-empty-string>, showRankingScore: bool, showRankingScoreDetails: bool} SimilarDocumentsQuery converted to an array with non null fields
*/
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
110 changes: 110 additions & 0 deletions src/Search/SimilarDocumentsSearchResult.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
<?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 array<string, mixed>|null
*/
public function getHit(int $key): ?array
{
return $this->hits[$key];
}

/**
* @return array<int, array<string, mixed>>
*/
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;
}

/**
* Converts the SimilarDocumentsSearchResult to an array representation.
*
* @return array<string, mixed>
*/
public function toArray(): array
{
return [
'id' => $this->id,
'hits' => $this->hits,
'hitsCount' => $this->hitsCount,
'processingTimeMs' => $this->processingTimeMs,
'offset' => $this->offset,
'limit' => $this->limit,
'estimatedTotalHits' => $this->estimatedTotalHits,
];
}

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

public function count(): int
{
return $this->hitsCount;
}
}
40 changes: 40 additions & 0 deletions tests/Endpoints/SimilarDocumentsTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
<?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::assertSame(1, $response->getHitsCount());

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

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 bbc1116

Please sign in to comment.