Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add get similar document method #645

Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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;
the-sinner marked this conversation as resolved.
Show resolved Hide resolved
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
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
public function setOffset(?int $offset): SimilarDocumentsQuery
/**
* @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
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
public function setLimit(?int $limit): SimilarDocumentsQuery
/**
* @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
the-sinner marked this conversation as resolved.
Show resolved Hide resolved
{
$this->filter = $filter;

return $this;
}

/**
* @param non-empty-string $embedder
*/
public function setEmbedder(string $embedder): SimilarDocumentsQuery
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
public function setEmbedder(string $embedder): SimilarDocumentsQuery
/**
* @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
the-sinner marked this conversation as resolved.
Show resolved Hide resolved
{
$this->attributesToRetrieve = $attributesToRetrieve;

return $this;
}

/**
* @param bool $showRankingScore boolean value to show ranking score
curquiza marked this conversation as resolved.
Show resolved Hide resolved
*/
public function setShowRankingScore(?bool $showRankingScore): SimilarDocumentsQuery
{
$this->showRankingScore = $showRankingScore;

return $this;
}

/**
* @param bool $showRankingScoreDetails boolean value to show ranking score details
curquiza marked this conversation as resolved.
Show resolved Hide resolved
*/
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
the-sinner marked this conversation as resolved.
Show resolved Hide resolved
{
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
Loading