From 58e00e33448518549ea690967699b725cf8b2269 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adam=20W=C3=B3js?= Date: Wed, 20 Sep 2023 12:25:20 +0200 Subject: [PATCH] IBX-6649: Added support for spell checking --- .../Repository/Values/Content/Query.php | 7 ++-- .../Values/Content/Query/Spellcheck.php | 31 ++++++++++++++ .../Values/Content/Search/SearchResult.php | 14 ++++++- .../Content/Search/SpellcheckResult.php | 42 +++++++++++++++++++ .../AbstractSearchResultAdapter.php | 28 ++++++++++++- .../Core/Repository/SearchServiceTest.php | 39 +++++++++++++++++ 6 files changed, 155 insertions(+), 6 deletions(-) create mode 100644 src/contracts/Repository/Values/Content/Query/Spellcheck.php create mode 100644 src/contracts/Repository/Values/Content/Search/SpellcheckResult.php diff --git a/src/contracts/Repository/Values/Content/Query.php b/src/contracts/Repository/Values/Content/Query.php index 9d7cdebbe0..ebcb9aebc7 100644 --- a/src/contracts/Repository/Values/Content/Query.php +++ b/src/contracts/Repository/Values/Content/Query.php @@ -8,6 +8,7 @@ namespace Ibexa\Contracts\Core\Repository\Values\Content; +use Ibexa\Contracts\Core\Repository\Values\Content\Query\Spellcheck; use Ibexa\Contracts\Core\Repository\Values\ValueObject; /** @@ -91,11 +92,9 @@ class Query extends ValueObject public $limit = 25; /** - * If true spellcheck suggestions are returned. - * - * @var bool + * Spellcheck suggestions are returned. */ - public $spellcheck; + public ?Spellcheck $spellcheck = null; /** * If true, search engine should perform count even if that means extra lookup. diff --git a/src/contracts/Repository/Values/Content/Query/Spellcheck.php b/src/contracts/Repository/Values/Content/Query/Spellcheck.php new file mode 100644 index 0000000000..82f556e463 --- /dev/null +++ b/src/contracts/Repository/Values/Content/Query/Spellcheck.php @@ -0,0 +1,31 @@ +query = $query; + } + + public function getQuery(): string + { + return $this->query; + } +} diff --git a/src/contracts/Repository/Values/Content/Search/SearchResult.php b/src/contracts/Repository/Values/Content/Search/SearchResult.php index 0a75598d52..11c9c117ff 100644 --- a/src/contracts/Repository/Values/Content/Search/SearchResult.php +++ b/src/contracts/Repository/Values/Content/Search/SearchResult.php @@ -44,9 +44,13 @@ class SearchResult extends ValueObject implements IteratorAggregate, Aggregation * criterions the wrong spelled value is replaced by a corrected one (TBD). * * @var \Ibexa\Contracts\Core\Repository\Values\Content\Query\Criterion + * + * @deprecated since Ibexa 4.6.0, to be removed in Ibexa 5.0.0. */ public $spellSuggestion; + public ?SpellcheckResult $spellcheck = null; + /** * The duration of the search processing in ms. * @@ -69,7 +73,10 @@ class SearchResult extends ValueObject implements IteratorAggregate, Aggregation public $maxScore; /** - * The total number of searchHits. + * The total number of searchHits. public function getSpellSuggestion(): ?SpellcheckResult. + { + return $this->spellSuggestion; + } * * `null` if Query->performCount was set to false and search engine avoids search lookup. * @@ -86,6 +93,11 @@ public function __construct(array $properties = []) parent::__construct($properties); } + public function getSpellcheck(): ?SpellcheckResult + { + return $this->spellcheck; + } + public function getAggregations(): ?AggregationResultCollection { return $this->aggregations; diff --git a/src/contracts/Repository/Values/Content/Search/SpellcheckResult.php b/src/contracts/Repository/Values/Content/Search/SpellcheckResult.php new file mode 100644 index 0000000000..5023f237ce --- /dev/null +++ b/src/contracts/Repository/Values/Content/Search/SpellcheckResult.php @@ -0,0 +1,42 @@ +query = $query; + $this->incorrect = $incorrect; + } + + public function getQuery(): ?string + { + return $this->query; + } + + public function isIncorrect(): bool + { + return $this->incorrect; + } +} diff --git a/src/lib/Pagination/Pagerfanta/AbstractSearchResultAdapter.php b/src/lib/Pagination/Pagerfanta/AbstractSearchResultAdapter.php index c8b2ba0e13..fb274cc338 100644 --- a/src/lib/Pagination/Pagerfanta/AbstractSearchResultAdapter.php +++ b/src/lib/Pagination/Pagerfanta/AbstractSearchResultAdapter.php @@ -12,6 +12,7 @@ use Ibexa\Contracts\Core\Repository\Values\Content\Query; use Ibexa\Contracts\Core\Repository\Values\Content\Search\AggregationResultCollection; use Ibexa\Contracts\Core\Repository\Values\Content\Search\SearchResult; +use Ibexa\Contracts\Core\Repository\Values\Content\Search\SpellcheckResult; use Pagerfanta\Adapter\AdapterInterface; abstract class AbstractSearchResultAdapter implements AdapterInterface, SearchResultAdapter @@ -40,6 +41,8 @@ abstract class AbstractSearchResultAdapter implements AdapterInterface, SearchRe /** @var int|null */ private $totalCount; + private ?SpellcheckResult $spellcheck = null; + public function __construct(Query $query, SearchService $searchService, array $languageFilter = []) { $this->query = $query; @@ -60,9 +63,10 @@ public function getNbResults() $countQuery = clone $this->query; $countQuery->limit = 0; - // Skip facets/aggregations computing + // Skip facets/aggregations & spellcheck computing $countQuery->facetBuilders = []; $countQuery->aggregations = []; + $countQuery->spellcheck = null; $searchResults = $this->executeQuery( $this->searchService, @@ -98,6 +102,7 @@ public function getSlice($offset, $length) $this->time = $searchResult->time; $this->timedOut = $searchResult->timedOut; $this->maxScore = $searchResult->maxScore; + $this->spellcheck = $searchResult->getSpellcheck(); // Set count for further use if returned by search engine despite !performCount (Solr, ES) if (!isset($this->totalCount) && isset($searchResult->totalCount)) { @@ -113,6 +118,7 @@ public function getAggregations(): AggregationResultCollection $aggregationQuery = clone $this->query; $aggregationQuery->offset = 0; $aggregationQuery->limit = 0; + $aggregationQuery->spellcheck = null; $searchResults = $this->executeQuery( $this->searchService, @@ -126,6 +132,26 @@ public function getAggregations(): AggregationResultCollection return $this->aggregations; } + public function getSpellcheck(): ?SpellcheckResult + { + if ($this->spellcheck === null) { + $spellcheckQuery = clone $this->query; + $spellcheckQuery->offset = 0; + $spellcheckQuery->limit = 0; + $spellcheckQuery->aggregations = []; + + $searchResults = $this->executeQuery( + $this->searchService, + $spellcheckQuery, + $this->languageFilter + ); + + $this->spellcheck = $searchResults->spellcheck; + } + + return $this->spellcheck; + } + public function getTime(): ?float { return $this->time; diff --git a/tests/integration/Core/Repository/SearchServiceTest.php b/tests/integration/Core/Repository/SearchServiceTest.php index e68cdf56e2..49cf2585f3 100644 --- a/tests/integration/Core/Repository/SearchServiceTest.php +++ b/tests/integration/Core/Repository/SearchServiceTest.php @@ -9,6 +9,7 @@ use function count; use Ibexa\Contracts\Core\Repository\Exceptions\InvalidArgumentException; use Ibexa\Contracts\Core\Repository\Exceptions\NotImplementedException; +use Ibexa\Contracts\Core\Repository\SearchService; use Ibexa\Contracts\Core\Repository\Values\Content\Content; use Ibexa\Contracts\Core\Repository\Values\Content\ContentInfo; use Ibexa\Contracts\Core\Repository\Values\Content\Location; @@ -4658,6 +4659,44 @@ public function testFulltextLocationTranslationSearch(array $data): void $this->assertFulltextSearchForTranslations(self::FIND_LOCATION_METHOD, $query); } + public function testSpellcheckWithIncorrectQuery(): void + { + $searchService = $this->getRepository()->getSearchService(); + + if (!$searchService->supports(SearchService::CAPABILITY_SPELLCHECK)) { + self::markTestSkipped("Search engine doesn't support spellchecking"); + } + + $query = new Query(); + // Search phrase with typo: "Ibexa Platfomr" instead of "Ibexa Platform": + $query->spellcheck = new Query\Spellcheck('Ibexa Platfomr'); + + $results = $searchService->findContent($query); + + self::assertNotNull($results->spellcheck); + self::assertTrue($results->spellcheck->isIncorrect()); + self::assertEquals('Ibexa Platform', $results->spellcheck->getQuery()); + } + + public function testSpellcheckWithCorrectQuery(): void + { + $searchService = $this->getRepository()->getSearchService(); + + if (!$searchService->supports(SearchService::CAPABILITY_SPELLCHECK)) { + self::markTestSkipped("Search engine doesn't support spellchecking"); + } + + $query = new Query(); + // Search phrase without typo + $query->spellcheck = new Query\Spellcheck('Ibexa Platform'); + + $results = $searchService->findContent($query); + + self::assertNotNull($results->spellcheck); + self::assertFalse($results->spellcheck->isIncorrect()); + self::assertEquals('Ibexa Platform', $results->spellcheck->getQuery()); + } + /** * Assert that query result matches the given fixture. *