From f4eab657b84b9fb15271b0e9024c70ef80d96c45 Mon Sep 17 00:00:00 2001 From: meili-bot <74670311+meili-bot@users.noreply.github.com> Date: Wed, 5 Jun 2024 17:09:14 +0200 Subject: [PATCH 1/6] Update README.md From 9645ddfdee83e54ced3dafe80c8667e7bb1938c0 Mon Sep 17 00:00:00 2001 From: Shalabh Agarwal <34604329+the-sinner@users.noreply.github.com> Date: Thu, 6 Jun 2024 13:29:01 +0530 Subject: [PATCH 2/6] Add ranking score threshold (#644) --- src/Contracts/SearchQuery.php | 9 +++++++++ tests/Endpoints/SearchTest.php | 12 ++++++++++++ 2 files changed, 21 insertions(+) diff --git a/src/Contracts/SearchQuery.php b/src/Contracts/SearchQuery.php index 6a9c5fe7..88c7eeee 100644 --- a/src/Contracts/SearchQuery.php +++ b/src/Contracts/SearchQuery.php @@ -29,6 +29,7 @@ class SearchQuery private ?array $attributesToSearchOn = null; private ?bool $showRankingScore = null; private ?bool $showRankingScoreDetails = null; + private ?float $rankingScoreThreshold = null; public function setQuery(string $q): SearchQuery { @@ -130,6 +131,13 @@ public function setShowRankingScoreDetails(?bool $showRankingScoreDetails): Sear return $this; } + public function setRankingScoreThreshold(?float $rankingScoreThreshold): SearchQuery + { + $this->rankingScoreThreshold = $rankingScoreThreshold; + + return $this; + } + public function setSort(array $sort): SearchQuery { $this->sort = $sort; @@ -230,6 +238,7 @@ public function toArray(): array 'attributesToSearchOn' => $this->attributesToSearchOn, 'showRankingScore' => $this->showRankingScore, 'showRankingScoreDetails' => $this->showRankingScoreDetails, + 'rankingScoreThreshold' => $this->rankingScoreThreshold ?? null, ], function ($item) { return null !== $item; }); } } diff --git a/tests/Endpoints/SearchTest.php b/tests/Endpoints/SearchTest.php index 789d3b23..f695743d 100644 --- a/tests/Endpoints/SearchTest.php +++ b/tests/Endpoints/SearchTest.php @@ -763,6 +763,18 @@ public function testSearchWithShowRankingScore(): void self::assertArrayHasKey('_rankingScore', $response->getHits()[0]); } + public function testSearchWithRankingScoreThreshold(): void + { + $response = $this->index->search('the', ['showRankingScore' => true, 'rankingScoreThreshold' => 0.9]); + + self::assertArrayHasKey('_rankingScore', $response->getHits()[0]); + self::assertSame(3, $response->getHitsCount()); + + $response = $this->index->search('the', ['showRankingScore' => true, 'rankingScoreThreshold' => 0.99]); + + self::assertSame(0, $response->getHitsCount()); + } + public function testBasicSearchWithTransformFacetsDritributionOptionToMap(): void { $response = $this->index->updateFilterableAttributes(['genre']); From bbc11161bc45f0834ecf76ff85799e85d860bc01 Mon Sep 17 00:00:00 2001 From: Shalabh Agarwal <34604329+the-sinner@users.noreply.github.com> Date: Tue, 18 Jun 2024 22:01:50 +0530 Subject: [PATCH 3/6] Add get similar document method (#645) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Add get similar documents method * Remove null from rankingScoreThreshold * Update src/Contracts/SimilarDocumentsQuery.php Co-authored-by: Tomas Norkūnas * Update src/Contracts/SimilarDocumentsQuery.php Co-authored-by: Tomas Norkūnas --------- Co-authored-by: Clémentine Co-authored-by: Tomas Norkūnas --- src/Contracts/SearchQuery.php | 2 +- src/Contracts/SimilarDocumentsQuery.php | 117 ++++++++++++++++++++ src/Endpoints/Indexes.php | 9 ++ src/Search/SimilarDocumentsSearchResult.php | 110 ++++++++++++++++++ tests/Endpoints/SimilarDocumentsTest.php | 40 +++++++ tests/TestCase.php | 33 ++++++ 6 files changed, 310 insertions(+), 1 deletion(-) create mode 100644 src/Contracts/SimilarDocumentsQuery.php create mode 100644 src/Search/SimilarDocumentsSearchResult.php create mode 100644 tests/Endpoints/SimilarDocumentsTest.php diff --git a/src/Contracts/SearchQuery.php b/src/Contracts/SearchQuery.php index 88c7eeee..2cdb78d7 100644 --- a/src/Contracts/SearchQuery.php +++ b/src/Contracts/SearchQuery.php @@ -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; }); } } diff --git a/src/Contracts/SimilarDocumentsQuery.php b/src/Contracts/SimilarDocumentsQuery.php new file mode 100644 index 00000000..597d651a --- /dev/null +++ b/src/Contracts/SimilarDocumentsQuery.php @@ -0,0 +1,117 @@ +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|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 $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|string>, embedder: non-empty-string, attributesToRetrieve: list, 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; + }); + } +} diff --git a/src/Endpoints/Indexes.php b/src/Endpoints/Indexes.php index 3d5f8bb9..969fcb3c 100644 --- a/src/Endpoints/Indexes.php +++ b/src/Endpoints/Indexes.php @@ -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; @@ -18,6 +19,7 @@ use Meilisearch\Exceptions\ApiException; use Meilisearch\Search\FacetSearchResult; use Meilisearch\Search\SearchResult; +use Meilisearch\Search\SimilarDocumentsSearchResult; class Indexes extends Endpoint { @@ -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 diff --git a/src/Search/SimilarDocumentsSearchResult.php b/src/Search/SimilarDocumentsSearchResult.php new file mode 100644 index 00000000..bfcacc44 --- /dev/null +++ b/src/Search/SimilarDocumentsSearchResult.php @@ -0,0 +1,110 @@ +> + */ + 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|null + */ + public function getHit(int $key): ?array + { + return $this->hits[$key]; + } + + /** + * @return 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; + } + + /** + * Converts the SimilarDocumentsSearchResult to an array representation. + * + * @return array + */ + 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; + } +} diff --git a/tests/Endpoints/SimilarDocumentsTest.php b/tests/Endpoints/SimilarDocumentsTest.php new file mode 100644 index 00000000..abddf359 --- /dev/null +++ b/tests/Endpoints/SimilarDocumentsTest.php @@ -0,0 +1,40 @@ +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()); + } +} diff --git a/tests/TestCase.php b/tests/TestCase.php index be414382..4124be99 100644 --- a/tests/TestCase.php +++ b/tests/TestCase.php @@ -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'], From 0b89ce92ca6fc8bbead24e77e8df508ecf198007 Mon Sep 17 00:00:00 2001 From: Many the fish Date: Tue, 25 Jun 2024 15:12:00 +0200 Subject: [PATCH 4/6] V1.9 hybrid search (#649) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Add retrieveVector method to getSimilarDocuments * Update vector search test checking `_vector` field * Make document query request the vector * Rollback compose version * linter * Update src/Contracts/DocumentsQuery.php Co-authored-by: Tomas Norkūnas --------- Co-authored-by: Tomas Norkūnas --- src/Contracts/DocumentsQuery.php | 14 +++++++++++++- src/Contracts/SimilarDocumentsQuery.php | 14 +++++++++++++- tests/Endpoints/DocumentsTest.php | 23 +++++++++++++++++++++++ tests/Endpoints/SearchTest.php | 16 ++++++++++++---- tests/Endpoints/SimilarDocumentsTest.php | 7 +++++++ 5 files changed, 68 insertions(+), 6 deletions(-) diff --git a/src/Contracts/DocumentsQuery.php b/src/Contracts/DocumentsQuery.php index 2f92cb33..223986ba 100644 --- a/src/Contracts/DocumentsQuery.php +++ b/src/Contracts/DocumentsQuery.php @@ -10,6 +10,7 @@ class DocumentsQuery private int $limit; private array $fields; private array $filter; + private ?bool $retrieveVectors = null; public function setOffset(int $offset): DocumentsQuery { @@ -46,6 +47,16 @@ public function setFilter(array $filter): DocumentsQuery return $this; } + /** + * @param bool|null $retrieveVectors boolean value to show _vector details + */ + public function setRetrieveVectors(?bool $retrieveVectors): DocumentsQuery + { + $this->retrieveVectors = $retrieveVectors; + + return $this; + } + /** * Checks if the $filter attribute has been set. * @@ -84,6 +95,7 @@ public function toArray(): array 'limit' => $this->limit ?? null, 'filter' => $this->filter ?? null, 'fields' => $this->fields(), - ], function ($item) { return null != $item || is_numeric($item); }); + 'retrieveVectors' => (null !== $this->retrieveVectors ? ($this->retrieveVectors ? 'true' : 'false') : null), + ], function ($item) { return null !== $item; }); } } diff --git a/src/Contracts/SimilarDocumentsQuery.php b/src/Contracts/SimilarDocumentsQuery.php index 597d651a..9dbe3a2f 100644 --- a/src/Contracts/SimilarDocumentsQuery.php +++ b/src/Contracts/SimilarDocumentsQuery.php @@ -16,6 +16,7 @@ class SimilarDocumentsQuery private ?array $attributesToRetrieve = null; private ?bool $showRankingScore = null; private ?bool $showRankingScoreDetails = null; + private ?bool $retrieveVectors = null; private ?array $filter = null; /** @@ -97,7 +98,17 @@ public function setShowRankingScoreDetails(?bool $showRankingScoreDetails): Simi } /** - * @return array{id: int|string, offset: non-negative-int, limit: positive-int, filter: array|string>, embedder: non-empty-string, attributesToRetrieve: list, showRankingScore: bool, showRankingScoreDetails: bool} SimilarDocumentsQuery converted to an array with non null fields + * @param bool|null $retrieveVectors boolean value to show _vector details + */ + public function setRetrieveVectors(?bool $retrieveVectors): SimilarDocumentsQuery + { + $this->retrieveVectors = $retrieveVectors; + + return $this; + } + + /** + * @return array{id: int|string, offset: non-negative-int, limit: positive-int, filter: array|string>, embedder: non-empty-string, attributesToRetrieve: list, showRankingScore: bool, showRankingScoreDetails: bool, retrieveVectors: bool} SimilarDocumentsQuery converted to an array with non null fields */ public function toArray(): array { @@ -110,6 +121,7 @@ public function toArray(): array 'attributesToRetrieve' => $this->attributesToRetrieve, 'showRankingScore' => $this->showRankingScore, 'showRankingScoreDetails' => $this->showRankingScoreDetails, + 'retrieveVectors' => $this->retrieveVectors, ], function ($item) { return null !== $item; }); diff --git a/tests/Endpoints/DocumentsTest.php b/tests/Endpoints/DocumentsTest.php index b253bdb0..0b1e3463 100644 --- a/tests/Endpoints/DocumentsTest.php +++ b/tests/Endpoints/DocumentsTest.php @@ -11,6 +11,7 @@ use Meilisearch\Exceptions\InvalidArgumentException; use Meilisearch\Exceptions\InvalidResponseBodyException; use Meilisearch\Exceptions\JsonEncodingException; +use Meilisearch\Http\Client; use Psr\Http\Message\ResponseInterface; use Tests\TestCase; @@ -624,6 +625,28 @@ public function testGetDocumentsWithoutFilterCorrectFieldsFormat(): void ); } + public function testGetDocumentsWithVector(): void + { + $http = new Client($this->host, getenv('MEILISEARCH_API_KEY')); + $http->patch('/experimental-features', ['vectorStore' => true]); + $index = $this->createEmptyIndex($this->safeIndexName('movies')); + + $promise = $index->updateEmbedders(['manual' => ['source' => 'userProvided', 'dimensions' => 3]]); + $this->assertIsValidPromise($promise); + $index->waitForTask($promise['taskUid']); + $promise = $index->updateDocuments(self::VECTOR_MOVIES); + $this->assertIsValidPromise($promise); + $index->waitForTask($promise['taskUid']); + + $response = $index->getDocuments(new DocumentsQuery()); + self::assertArrayNotHasKey('_vectors', $response->getResults()[0]); + $query = (new DocumentsQuery())->setRetrieveVectors(true); + $response = $index->getDocuments($query); + self::assertArrayHasKey('_vectors', $response->getResults()[0]); + + self::assertCount(5, $response); + } + public function testGetDocumentsMessageHintException(): void { $responseMock = $this->createMock(ResponseInterface::class); diff --git a/tests/Endpoints/SearchTest.php b/tests/Endpoints/SearchTest.php index f695743d..60f1eaab 100644 --- a/tests/Endpoints/SearchTest.php +++ b/tests/Endpoints/SearchTest.php @@ -694,14 +694,22 @@ public function testVectorSearch(): void $http->patch('/experimental-features', ['vectorStore' => true]); $index = $this->createEmptyIndex($this->safeIndexName()); - $promise = $index->updateEmbedders(['default' => ['source' => 'userProvided', 'dimensions' => 1]]); + $promise = $index->updateEmbedders(['manual' => ['source' => 'userProvided', 'dimensions' => 3]]); $this->assertIsValidPromise($promise); $index->waitForTask($promise['taskUid']); + $promise = $index->updateDocuments(self::VECTOR_MOVIES); + $this->assertIsValidPromise($promise); + $index->waitForTask($promise['taskUid']); + + $response = $index->search('', ['vector' => [-0.5, 0.3, 0.85], 'hybrid' => ['semanticRatio' => 1.0]]); + + self::assertSame(5, $response->getSemanticHitCount()); + self::assertArrayNotHasKey('_vectors', $response->getHit(0)); - $response = $index->search('', ['vector' => [1], 'hybrid' => ['semanticRatio' => 1.0]]); + $response = $index->search('', ['vector' => [-0.5, 0.3, 0.85], 'hybrid' => ['semanticRatio' => 1.0], 'retrieveVectors' => true]); - self::assertSame(0, $response->getSemanticHitCount()); - self::assertEmpty($response->getHits()); + self::assertSame(5, $response->getSemanticHitCount()); + self::assertArrayHasKey('_vectors', $response->getHit(0)); } public function testShowRankingScoreDetails(): void diff --git a/tests/Endpoints/SimilarDocumentsTest.php b/tests/Endpoints/SimilarDocumentsTest.php index abddf359..20ce0048 100644 --- a/tests/Endpoints/SimilarDocumentsTest.php +++ b/tests/Endpoints/SimilarDocumentsTest.php @@ -32,6 +32,13 @@ public function testBasicSearchWithSimilarDocuments(): void $documentId = $response->getHit(0)['id']; $response = $this->index->searchSimilarDocuments(new SimilarDocumentsQuery($documentId)); + self::assertGreaterThanOrEqual(4, $response->getHitsCount()); + self::assertArrayNotHasKey('_vectors', $response->getHit(0)); + self::assertArrayHasKey('id', $response->getHit(0)); + self::assertSame($documentId, $response->getId()); + + $similarQuery = new SimilarDocumentsQuery($documentId); + $response = $this->index->searchSimilarDocuments($similarQuery->setRetrieveVectors(true)); self::assertGreaterThanOrEqual(4, $response->getHitsCount()); self::assertArrayHasKey('_vectors', $response->getHit(0)); self::assertArrayHasKey('id', $response->getHit(0)); From 707e6e9f36ed578f1886f74fb6b5c617cabfcf5e Mon Sep 17 00:00:00 2001 From: curquiza Date: Wed, 26 Jun 2024 16:23:13 +0200 Subject: [PATCH 5/6] Update code samples --- .code-samples.meilisearch.yaml | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/.code-samples.meilisearch.yaml b/.code-samples.meilisearch.yaml index 8b086156..48ca9d52 100644 --- a/.code-samples.meilisearch.yaml +++ b/.code-samples.meilisearch.yaml @@ -718,3 +718,29 @@ negative_search_1: |- $client->index('movies')->search('-escape'); negative_search_2: |- $client->index('movies')->search('-"escape"'); +search_parameter_reference_retrieve_vectors_1: |- + $client->index('INDEX_NAME')->search('kitchen utensils', [ + 'retrieveVectors' => true, + 'hybrid' => [ + 'embedder': 'default' + ] + ]); +search_parameter_reference_ranking_score_threshold_1: |- + $client->index('INDEX_NAME')->search('badman', [ + 'rankingScoreThreshold' => 0.2 + ]); +search_parameter_reference_distinct_1: |- + $client->index('INDEX_NAME')->search('QUERY TERMS', [ + 'distinct' => 'ATTRIBUTE_A' + ]); +distinct_attribute_guide_filterable_1: |- + $client->index('products')->updateFilterableAttributes(['product_id', 'sku', 'url']); +distinct_attribute_guide_distinct_parameter_1: |- + $client->index('products')->search('white shirt', [ + 'distinct' => 'sku' + ]); +search_parameter_guide_matching_strategy_3: |- + $client->index('movies')->search('white shirt', ['matchingStrategy' => 'frequency']); +get_similar_post_1: |- + $similarQuery = new SimilarDocumentsQuery('TARGET_DOCUMENT_ID'); + $client->index('INDEX_NAME')->getSimilar($similarQuery); From b7138db4d582c327a9116565443ce502da57111f Mon Sep 17 00:00:00 2001 From: Shalabh Agarwal <34604329+the-sinner@users.noreply.github.com> Date: Thu, 27 Jun 2024 16:24:48 +0530 Subject: [PATCH 6/6] Add distinct attribute at search (#648) * Add distinct attribute at search * Update tests/Endpoints/SearchTest.php --------- Co-authored-by: Many the fish --- src/Contracts/SearchQuery.php | 12 ++++++++++++ tests/Endpoints/MultiSearchTest.php | 27 ++++++++++++++++++++++++++- tests/Endpoints/SearchTest.php | 17 +++++++++++++++++ 3 files changed, 55 insertions(+), 1 deletion(-) diff --git a/src/Contracts/SearchQuery.php b/src/Contracts/SearchQuery.php index 2cdb78d7..0d72c982 100644 --- a/src/Contracts/SearchQuery.php +++ b/src/Contracts/SearchQuery.php @@ -30,6 +30,7 @@ class SearchQuery private ?bool $showRankingScore = null; private ?bool $showRankingScoreDetails = null; private ?float $rankingScoreThreshold = null; + private ?string $distinct = null; public function setQuery(string $q): SearchQuery { @@ -138,6 +139,16 @@ public function setRankingScoreThreshold(?float $rankingScoreThreshold): SearchQ return $this; } + /** + * @param non-empty-string|null $distinct + */ + public function setDistinct(?string $distinct): SearchQuery + { + $this->distinct = $distinct; + + return $this; + } + public function setSort(array $sort): SearchQuery { $this->sort = $sort; @@ -239,6 +250,7 @@ public function toArray(): array 'showRankingScore' => $this->showRankingScore, 'showRankingScoreDetails' => $this->showRankingScoreDetails, 'rankingScoreThreshold' => $this->rankingScoreThreshold, + 'distinct' => $this->distinct, ], function ($item) { return null !== $item; }); } } diff --git a/tests/Endpoints/MultiSearchTest.php b/tests/Endpoints/MultiSearchTest.php index 402d2b59..cbc57ebc 100644 --- a/tests/Endpoints/MultiSearchTest.php +++ b/tests/Endpoints/MultiSearchTest.php @@ -18,6 +18,7 @@ protected function setUp(): void parent::setUp(); $this->booksIndex = $this->createEmptyIndex($this->safeIndexName('books')); $this->booksIndex->updateSortableAttributes(['author']); + $this->booksIndex->updateFilterableAttributes(['genre']); $promise = $this->booksIndex->updateDocuments(self::DOCUMENTS); $this->booksIndex->waitForTask($promise['taskUid']); @@ -64,7 +65,7 @@ public function testMultiSearch(): void self::assertArrayHasKey('estimatedTotalHits', $response['results'][0]); self::assertCount(2, $response['results'][0]['hits']); - self::assertArrayHasKey('indexUid', $response['results'][0]); + self::assertArrayHasKey('indexUid', $response['results'][1]); self::assertArrayHasKey('hits', $response['results'][1]); self::assertArrayHasKey('query', $response['results'][1]); self::assertArrayHasKey('page', $response['results'][1]); @@ -90,4 +91,28 @@ public function testSupportedQueryParams(): void self::assertTrue($result['showRankingScore']); self::assertTrue($result['showRankingScoreDetails']); } + + public function testMultiSearchWithDistinctAttribute(): void + { + $response = $this->client->multiSearch([ + (new SearchQuery())->setIndexUid($this->booksIndex->getUid()) + ->setFilter(['genre = fantasy']), + (new SearchQuery())->setIndexUid($this->booksIndex->getUid()) + ->setFilter(['genre = fantasy']) + ->setDistinct('genre'), + ]); + + self::assertCount(2, $response['results']); + + self::assertArrayHasKey('hits', $response['results'][0]); + self::assertCount(2, $response['results'][0]['hits']); + self::assertSame('fantasy', $response['results'][0]['hits'][0]['genre']); + self::assertSame('fantasy', $response['results'][0]['hits'][1]['genre']); + + self::assertArrayHasKey('indexUid', $response['results'][1]); + self::assertArrayHasKey('hits', $response['results'][1]); + self::assertArrayHasKey('query', $response['results'][1]); + self::assertCount(1, $response['results'][1]['hits']); + self::assertSame('fantasy', $response['results'][1]['hits'][0]['genre']); + } } diff --git a/tests/Endpoints/SearchTest.php b/tests/Endpoints/SearchTest.php index 60f1eaab..a7d1e17e 100644 --- a/tests/Endpoints/SearchTest.php +++ b/tests/Endpoints/SearchTest.php @@ -862,4 +862,21 @@ public function testSearchAndRetrieveFacetStats(): void self::assertSame(['info.reviewNb' => ['min' => 50.0, 'max' => 1000.0]], $response->getFacetStats()); } + + public function testSearchWithDistinctAttribute(): void + { + $this->index = $this->createEmptyIndex($this->safeIndexName()); + $this->index->updateFilterableAttributes(['genre']); + + $promise = $this->index->updateDocuments(self::DOCUMENTS); + $this->index->waitForTask($promise['taskUid']); + + $response = $this->index->search(null, [ + 'distinct' => 'genre', + 'filter' => ['genre = fantasy'], + ])->toArray(); + + self::assertArrayHasKey('title', $response['hits'][0]); + self::assertCount(1, $response['hits']); + } }