2021-04-17 00:26:33 +02:00
|
|
|
<?php
|
|
|
|
namespace WapplerSystems\Meilisearch\Domain\Search\ResultSet;
|
|
|
|
|
|
|
|
/*
|
|
|
|
* This file is part of the TYPO3 CMS project.
|
|
|
|
*
|
|
|
|
* It is free software; you can redistribute it and/or modify it under
|
|
|
|
* the terms of the GNU General Public License, either version 2
|
|
|
|
* of the License, or any later version.
|
|
|
|
*
|
|
|
|
* For the full copyright and license information, please read the
|
|
|
|
* LICENSE.txt file that was distributed with this source code.
|
|
|
|
*
|
|
|
|
* The TYPO3 project - inspiring people to share!
|
|
|
|
*/
|
|
|
|
|
|
|
|
use WapplerSystems\Meilisearch\Domain\Search\ResultSet\Facets\AbstractFacet;
|
|
|
|
use WapplerSystems\Meilisearch\Domain\Search\ResultSet\Facets\FacetRegistry;
|
|
|
|
use WapplerSystems\Meilisearch\Domain\Search\ResultSet\Facets\RequirementsService;
|
|
|
|
use WapplerSystems\Meilisearch\Domain\Search\ResultSet\Sorting\Sorting;
|
|
|
|
use WapplerSystems\Meilisearch\Domain\Search\ResultSet\Spellchecking\Suggestion;
|
|
|
|
use TYPO3\CMS\Core\Utility\GeneralUtility;
|
|
|
|
use TYPO3\CMS\Extbase\Object\ObjectManager;
|
|
|
|
use TYPO3\CMS\Extbase\Object\ObjectManagerInterface;
|
|
|
|
use TYPO3\CMS\Frontend\ContentObject\ContentObjectRenderer;
|
|
|
|
|
|
|
|
/**
|
2021-04-17 21:20:54 +02:00
|
|
|
* This processor is used to transform the meilisearch response into a
|
2021-04-17 00:26:33 +02:00
|
|
|
* domain object hierarchy that can be used in the application (controller and view).
|
|
|
|
*
|
|
|
|
* @author Frans Saris <frans@beech.it>
|
|
|
|
* @author Timo Hund <timo.hund@dkd.de>
|
|
|
|
*/
|
|
|
|
class ResultSetReconstitutionProcessor implements SearchResultSetProcessor
|
|
|
|
{
|
|
|
|
/**
|
|
|
|
* @var ObjectManagerInterface
|
|
|
|
*/
|
|
|
|
protected $objectManager;
|
|
|
|
|
|
|
|
/**
|
|
|
|
* @return ObjectManagerInterface
|
|
|
|
*/
|
|
|
|
public function getObjectManager()
|
|
|
|
{
|
|
|
|
if ($this->objectManager === null) {
|
|
|
|
$this->objectManager = GeneralUtility::makeInstance(ObjectManager::class);
|
|
|
|
}
|
|
|
|
return $this->objectManager;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* @param ObjectManagerInterface $objectManager
|
|
|
|
*/
|
|
|
|
public function setObjectManager($objectManager)
|
|
|
|
{
|
|
|
|
$this->objectManager = $objectManager;
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
* @return FacetRegistry
|
|
|
|
*/
|
|
|
|
protected function getFacetRegistry()
|
|
|
|
{
|
|
|
|
// @extensionScannerIgnoreLine
|
|
|
|
return $this->getObjectManager()->get(FacetRegistry::class);
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* The implementation can be used to influence a SearchResultSet that is
|
|
|
|
* created and processed in the SearchResultSetService.
|
|
|
|
*
|
|
|
|
* @param SearchResultSet $resultSet
|
|
|
|
* @return SearchResultSet
|
|
|
|
*/
|
|
|
|
public function process(SearchResultSet $resultSet)
|
|
|
|
{
|
|
|
|
if (!$resultSet instanceof SearchResultSet) {
|
|
|
|
return $resultSet;
|
|
|
|
}
|
|
|
|
|
|
|
|
$resultSet = $this->parseSpellCheckingResponseIntoObjects($resultSet);
|
|
|
|
$resultSet = $this->parseSortingIntoObjects($resultSet);
|
|
|
|
|
2021-04-17 21:20:54 +02:00
|
|
|
// here we can reconstitute other domain objects from the meilisearch response
|
2021-04-17 00:26:33 +02:00
|
|
|
$resultSet = $this->parseFacetsIntoObjects($resultSet);
|
|
|
|
|
|
|
|
return $resultSet;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* @param SearchResultSet $resultSet
|
|
|
|
* @return SearchResultSet
|
|
|
|
*/
|
|
|
|
protected function parseSortingIntoObjects(SearchResultSet $resultSet)
|
|
|
|
{
|
|
|
|
$configuration = $resultSet->getUsedSearchRequest()->getContextTypoScriptConfiguration();
|
|
|
|
$hasSorting = $resultSet->getUsedSearchRequest()->getHasSorting();
|
|
|
|
$activeSortingName = $resultSet->getUsedSearchRequest()->getSortingName();
|
|
|
|
$activeSortingDirection = $resultSet->getUsedSearchRequest()->getSortingDirection();
|
|
|
|
|
|
|
|
// no configuration available
|
|
|
|
if (!isset($configuration)) {
|
|
|
|
return $resultSet;
|
|
|
|
}
|
|
|
|
|
|
|
|
// no sorting enabled
|
|
|
|
if (!$configuration->getSearchSorting()) {
|
|
|
|
return $resultSet;
|
|
|
|
}
|
|
|
|
foreach ($configuration->getSearchSortingOptionsConfiguration() as $sortingKeyName => $sortingOptions) {
|
|
|
|
$sortingName = rtrim($sortingKeyName, '.');
|
|
|
|
$selected = false;
|
|
|
|
$direction = $configuration->getSearchSortingDefaultOrderBySortOptionName($sortingName);
|
|
|
|
|
|
|
|
// when we have an active sorting in the request we compare the sortingName and mark is as active and
|
|
|
|
// use the direction from the request
|
|
|
|
if ($hasSorting && $activeSortingName == $sortingName) {
|
|
|
|
$selected = true;
|
|
|
|
$direction = $activeSortingDirection;
|
|
|
|
}
|
|
|
|
|
|
|
|
$field = $sortingOptions['field'];
|
|
|
|
$label = $sortingOptions['label'];
|
|
|
|
|
|
|
|
$isResetOption = $field === 'relevance';
|
|
|
|
|
|
|
|
// Allow stdWrap on label:
|
|
|
|
$labelHasSubConfiguration = is_array($sortingOptions['label.']);
|
|
|
|
if ($labelHasSubConfiguration) {
|
|
|
|
$cObj = GeneralUtility::makeInstance(ContentObjectRenderer::class);
|
|
|
|
$label = $cObj->stdWrap($label, $sortingOptions['label.']);
|
|
|
|
}
|
|
|
|
|
|
|
|
$sorting = $this->getObjectManager()->get(Sorting::class, $resultSet, $sortingName, $field, $direction, $label, $selected, $isResetOption);
|
|
|
|
$resultSet->addSorting($sorting);
|
|
|
|
}
|
|
|
|
|
|
|
|
return $resultSet;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* @param SearchResultSet $resultSet
|
|
|
|
* @return SearchResultSet
|
|
|
|
*/
|
|
|
|
private function parseSpellCheckingResponseIntoObjects(SearchResultSet $resultSet)
|
|
|
|
{
|
|
|
|
//read the response
|
|
|
|
$response = $resultSet->getResponse();
|
|
|
|
|
|
|
|
if (!is_array($response->spellcheck->suggestions)) {
|
|
|
|
return $resultSet;
|
|
|
|
}
|
|
|
|
|
|
|
|
$misspelledTerm = '';
|
|
|
|
foreach ($response->spellcheck->suggestions as $key => $suggestionData) {
|
|
|
|
if (is_string($suggestionData)) {
|
|
|
|
$misspelledTerm = $key;
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
|
|
|
|
if ($misspelledTerm === '') {
|
|
|
|
throw new \UnexpectedValueException('No missspelled term before suggestion');
|
|
|
|
}
|
|
|
|
|
|
|
|
if (!is_object($suggestionData) && !is_array($suggestionData->suggestion)) {
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
|
|
|
|
foreach ($suggestionData->suggestion as $suggestedTerm) {
|
|
|
|
$suggestion = $this->createSuggestionFromResponseFragment($suggestionData, $suggestedTerm, $misspelledTerm);
|
|
|
|
//add it to the resultSet
|
|
|
|
$resultSet->addSpellCheckingSuggestion($suggestion);
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
return $resultSet;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* @param \stdClass $suggestionData
|
|
|
|
* @param string $suggestedTerm
|
|
|
|
* @param string $misspelledTerm
|
|
|
|
* @return Suggestion
|
|
|
|
*/
|
|
|
|
private function createSuggestionFromResponseFragment($suggestionData, $suggestedTerm, $misspelledTerm)
|
|
|
|
{
|
|
|
|
$numFound = isset($suggestionData->numFound) ? $suggestionData->numFound : 0;
|
|
|
|
$startOffset = isset($suggestionData->startOffset) ? $suggestionData->startOffset : 0;
|
|
|
|
$endOffset = isset($suggestionData->endOffset) ? $suggestionData->endOffset : 0;
|
|
|
|
|
|
|
|
// by now we avoid to use GeneralUtility::makeInstance, since we only create a value object
|
|
|
|
// and the usage might be a overhead.
|
|
|
|
$suggestion = new Suggestion($suggestedTerm, $misspelledTerm, $numFound, $startOffset, $endOffset);
|
|
|
|
return $suggestion;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Parse available facets into objects
|
|
|
|
*
|
|
|
|
* @param SearchResultSet $resultSet
|
|
|
|
* @return SearchResultSet
|
|
|
|
*/
|
|
|
|
private function parseFacetsIntoObjects(SearchResultSet $resultSet)
|
|
|
|
{
|
|
|
|
// Make sure we can access the facet configuration
|
|
|
|
if (!$resultSet->getUsedSearchRequest() || !$resultSet->getUsedSearchRequest()->getContextTypoScriptConfiguration()) {
|
|
|
|
return $resultSet;
|
|
|
|
}
|
|
|
|
|
|
|
|
// Read the response
|
|
|
|
$response = $resultSet->getResponse();
|
|
|
|
if (!is_object($response->facet_counts) && !is_object($response->facets)) {
|
|
|
|
return $resultSet;
|
|
|
|
}
|
|
|
|
|
|
|
|
/** @var FacetRegistry $facetRegistry */
|
|
|
|
$facetRegistry = $this->getFacetRegistry();
|
|
|
|
$facetsConfiguration = $resultSet->getUsedSearchRequest()->getContextTypoScriptConfiguration()->getSearchFacetingFacets();
|
|
|
|
|
|
|
|
foreach ($facetsConfiguration as $name => $options) {
|
|
|
|
if (!is_array($options)) {
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
$facetName = rtrim($name, '.');
|
|
|
|
$type = !empty($options['type']) ? $options['type'] : '';
|
|
|
|
|
|
|
|
$parser = $facetRegistry->getPackage($type)->getParser();
|
|
|
|
$facet = $parser->parse($resultSet, $facetName, $options);
|
|
|
|
if ($facet !== null) {
|
|
|
|
$resultSet->addFacet($facet);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
$this->applyRequirements($resultSet);
|
|
|
|
|
|
|
|
return $resultSet;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* @param SearchResultSet $resultSet
|
|
|
|
*/
|
|
|
|
protected function applyRequirements(SearchResultSet $resultSet)
|
|
|
|
{
|
|
|
|
$requirementsService = $this->getRequirementsService();
|
|
|
|
$facets = $resultSet->getFacets();
|
|
|
|
foreach ($facets as $facet) {
|
|
|
|
/** @var $facet AbstractFacet */
|
|
|
|
$requirementsMet = $requirementsService->getAllRequirementsMet($facet);
|
|
|
|
$facet->setAllRequirementsMet($requirementsMet);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* @return RequirementsService
|
|
|
|
*/
|
|
|
|
protected function getRequirementsService()
|
|
|
|
{
|
|
|
|
// @extensionScannerIgnoreLine
|
|
|
|
return $this->getObjectManager()->get(RequirementsService::class);
|
|
|
|
}
|
|
|
|
}
|