first commit

This commit is contained in:
Sven Wappler
2021-04-17 00:26:33 +02:00
commit 866c63cc63
813 changed files with 100696 additions and 0 deletions

View File

@@ -0,0 +1,69 @@
<?php
namespace WapplerSystems\Meilisearch\Domain\Search\ResultSet\Facets\OptionBased;
/*
* 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\AbstractFacetItem;
/**
* Base class for all facet items that are represented as option
*
* @author Frans Saris <frans@beech.it>
* @author Timo Hund <timo.hund@dkd.de>
*/
class AbstractOptionFacetItem extends AbstractFacetItem
{
/**
* @var string
*/
protected $value = '';
/**
* @param AbstractFacet $facet
* @param string $label
* @param string $value
* @param int $documentCount
* @param bool $selected
* @param array $metrics
*/
public function __construct(AbstractFacet $facet, $label = '', $value = '', $documentCount = 0, $selected = false, $metrics = [])
{
$this->value = $value;
parent::__construct($facet, $label, $documentCount, $selected, $metrics);
}
/**
* @return string
*/
public function getValue()
{
return $this->value;
}
/**
* @return string
*/
public function getUriValue()
{
return $this->getValue();
}
/**
* @return string
*/
public function getCollectionKey()
{
return $this->getValue();
}
}

View File

@@ -0,0 +1,92 @@
<?php
namespace WapplerSystems\Meilisearch\Domain\Search\ResultSet\Facets\OptionBased;
/*
* 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\SearchResultSet;
/**
* Class AbstractOptionsFacet
*
* @author Frans Saris <frans@beech.it>
* @author Timo Hund <timo.hund@dkd.de>
*/
class AbstractOptionsFacet extends AbstractFacet
{
/**
* @var OptionCollection
*/
protected $options;
/**
* OptionsFacet constructor
*
* @param SearchResultSet $resultSet
* @param string $name
* @param string $field
* @param string $label
* @param array $configuration Facet configuration passed from typoscript
*/
public function __construct(SearchResultSet $resultSet, $name, $field, $label = '', array $configuration = [])
{
parent::__construct($resultSet, $name, $field, $label, $configuration);
$this->options = new OptionCollection();
}
/**
* @return OptionCollection
*/
public function getOptions()
{
return $this->options;
}
/**
* @param OptionCollection $options
*/
public function setOptions($options)
{
$this->options = $options;
}
/**
* @param AbstractOptionFacetItem $option
*/
public function addOption(AbstractOptionFacetItem $option)
{
$this->options->add($option);
}
/**
* The implementation of this method should return a "flatten" collection of all items.
*
* @return OptionCollection
*/
public function getAllFacetItems()
{
return $this->options;
}
/**
* Get facet partial name used for rendering the facet
*
* @return string
*/
public function getPartialName()
{
return !empty($this->configuration['partialName']) ? $this->configuration['partialName'] : 'Options';
}
}

View File

@@ -0,0 +1,130 @@
<?php
namespace WapplerSystems\Meilisearch\Domain\Search\ResultSet\Facets\OptionBased\Hierarchy;
/*
* 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\AbstractFacetItemCollection;
use WapplerSystems\Meilisearch\Domain\Search\ResultSet\SearchResultSet;
/**
* Value object that represent a options facet.
*
* @author Frans Saris <frans@beech.it>
* @author Timo Hund <timo.hund@dkd.de>
*/
class HierarchyFacet extends AbstractFacet
{
const TYPE_HIERARCHY = 'hierarchy';
/**
* String
* @var string
*/
protected static $type = self::TYPE_HIERARCHY;
/**
* @var NodeCollection
*/
protected $childNodes;
/**
* @var NodeCollection
*/
protected $allNodes;
/**
* @var array
*/
protected $nodesByKey = [];
/**
* OptionsFacet constructor
*
* @param SearchResultSet $resultSet
* @param string $name
* @param string $field
* @param string $label
* @param array $configuration Facet configuration passed from typoscript
*/
public function __construct(SearchResultSet $resultSet, $name, $field, $label = '', array $configuration = [])
{
parent::__construct($resultSet, $name, $field, $label, $configuration);
$this->childNodes = new NodeCollection();
$this->allNodes = new NodeCollection();
}
/**
* @param Node $node
*/
public function addChildNode(Node $node)
{
$this->childNodes->add($node);
}
/**
* @return NodeCollection
*/
public function getChildNodes()
{
return $this->childNodes;
}
/**
* Creates a new node on the right position with the right parent node.
*
* @param string $parentKey
* @param string $key
* @param string $label
* @param string $value
* @param integer $count
* @param boolean $selected
*/
public function createNode($parentKey, $key, $label, $value, $count, $selected)
{
/** @var $parentNode Node|null */
$parentNode = isset($this->nodesByKey[$parentKey]) ? $this->nodesByKey[$parentKey] : null;
/** @var Node $node */
$node = $this->objectManager->get(Node::class, $this, $parentNode, $key, $label, $value, $count, $selected);
$this->nodesByKey[$key] = $node;
if ($parentNode === null) {
$this->addChildNode($node);
} else {
$parentNode->addChildNode($node);
}
$this->allNodes->add($node);
}
/**
* Get facet partial name used for rendering the facet
*
* @return string
*/
public function getPartialName()
{
return !empty($this->configuration['partialName']) ? $this->configuration['partialName'] : 'Hierarchy';
}
/**
* The implementation of this method should return a "flatten" collection of all items.
*
* @return AbstractFacetItemCollection
*/
public function getAllFacetItems()
{
return $this->allNodes;
}
}

View File

@@ -0,0 +1,159 @@
<?php
namespace WapplerSystems\Meilisearch\Domain\Search\ResultSet\Facets\OptionBased\Hierarchy;
/*
* 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\AbstractFacetParser;
use WapplerSystems\Meilisearch\Domain\Search\ResultSet\SearchResultSet;
use WapplerSystems\Meilisearch\System\Solr\ParsingUtil;
/**
* Class HierarchyFacetParser
*/
class HierarchyFacetParser extends AbstractFacetParser
{
/**
* @param SearchResultSet $resultSet
* @param string $facetName
* @param array $facetConfiguration
* @return HierarchyFacet|null
*/
public function parse(SearchResultSet $resultSet, $facetName, array $facetConfiguration)
{
$response = $resultSet->getResponse();
$fieldName = $facetConfiguration['field'];
$label = $this->getPlainLabelOrApplyCObject($facetConfiguration);
$optionsFromSolrResponse = isset($response->facet_counts->facet_fields->{$fieldName}) ? ParsingUtil::getMapArrayFromFlatArray($response->facet_counts->facet_fields->{$fieldName}) : [];
$optionsFromRequest = $this->getActiveFacetValuesFromRequest($resultSet, $facetName);
$hasOptionsInResponse = !empty($optionsFromSolrResponse);
$hasSelectedOptionsInRequest = count($optionsFromRequest) > 0;
$hasNoOptionsToShow = !$hasOptionsInResponse && !$hasSelectedOptionsInRequest;
$hideEmpty = !$resultSet->getUsedSearchRequest()->getContextTypoScriptConfiguration()->getSearchFacetingShowEmptyFacetsByName($facetName);
if ($hasNoOptionsToShow && $hideEmpty) {
return null;
}
/** @var $facet HierarchyFacet */
$facet = $this->objectManager->get(HierarchyFacet::class, $resultSet, $facetName, $fieldName, $label, $facetConfiguration);
$hasActiveOptions = count($optionsFromRequest) > 0;
$facet->setIsUsed($hasActiveOptions);
$facet->setIsAvailable($hasOptionsInResponse);
$nodesToCreate = $this->getMergedFacetValueFromSearchRequestAndSolrResponse($optionsFromSolrResponse, $optionsFromRequest);
if ($this->facetOptionsMustBeResorted($facetConfiguration)) {
$nodesToCreate = $this->sortFacetOptionsInNaturalOrder($nodesToCreate);
}
foreach ($nodesToCreate as $value => $count) {
if ($this->getIsExcludedFacetValue($value, $facetConfiguration)) {
continue;
}
$isActive = in_array($value, $optionsFromRequest);
$delimiterPosition = strpos($value, '-');
$path = substr($value, $delimiterPosition + 1);
$pathArray = $this->getPathAsArray($path);
$key = array_pop($pathArray);
$parentKey = array_pop($pathArray);
$value = '/' . $path;
$label = $this->getLabelFromRenderingInstructions($key, $count, $facetName, $facetConfiguration);
$facet->createNode($parentKey, $key, $label, $value, $count, $isActive);
}
return $facet;
}
/**
* Sorts facet options in natural order.
* Options must be sorted in natural order,
* because lower nesting levels must be instantiated first, to serve as parents for higher nested levels.
* See implementation of HierarchyFacet::createNode().
*
* @param array $flatOptionsListForFacet
* @return void sorted list of facet options
*/
protected function sortFacetOptionsInNaturalOrder(array $flatOptionsListForHierarchyFacet)
{
uksort($flatOptionsListForHierarchyFacet, "strnatcmp");
return $flatOptionsListForHierarchyFacet;
}
/**
* Checks if options must be resorted.
*
* Apache Solr facet.sort can be set globally or per facet.
* Relevant TypoScript paths:
* plugin.tx_meilisearch.search.faceting.sortBy causes facet.sort Apache Solr parameter
* plugin.tx_meilisearch.search.faceting.facets.[facetName].sortBy causes f.<fieldname>.facet.sort parameter
*
* see: https://lucene.apache.org/solr/guide/6_6/faceting.html#Faceting-Thefacet.sortParameter
* see: https://wiki.apache.org/solr/SimpleFacetParameters#facet.sort : "This parameter can be specified on a per field basis."
*
* @param array $facetConfiguration
* @return bool
*/
protected function facetOptionsMustBeResorted(array $facetConfiguration)
{
if (isset($facetConfiguration['sortBy']) && $facetConfiguration['sortBy'] === 'index') {
return true;
}
return false;
}
/**
* This method is used to get the path array from a hierarchical facet. It substitutes escaped slashes to keep them
* when they are used inside a facetValue.
*
* @param string $path
* @return array
*/
protected function getPathAsArray($path)
{
$path = str_replace('\/', '@@@', $path);
$path = rtrim($path, "/");
$segments = explode('/', $path);
return array_map(function($item) {
return str_replace('@@@', '/', $item);
}, $segments);
}
/**
* Retrieves the active facetValue for a facet from the search request.
* @param SearchResultSet $resultSet
* @param string $facetName
* @return array
*/
protected function getActiveFacetValuesFromRequest(SearchResultSet $resultSet, $facetName)
{
$activeFacetValues = [];
$values = $resultSet->getUsedSearchRequest()->getActiveFacetValuesByName($facetName);
foreach (is_array($values) ? $values : [] as $valueFromRequest) {
// Attach the 'depth' param again to the value
if (strpos($valueFromRequest, '-') === false) {
$valueFromRequest = HierarchyTool::substituteSlashes($valueFromRequest);
$valueFromRequest = trim($valueFromRequest, '/');
$valueFromRequest = (count(explode('/', $valueFromRequest)) - 1) . '-' . $valueFromRequest . '/';
$valueFromRequest = HierarchyTool::unSubstituteSlashes($valueFromRequest);
}
$activeFacetValues[] = $valueFromRequest;
}
return $activeFacetValues;
}
}

View File

@@ -0,0 +1,37 @@
<?php
namespace WapplerSystems\Meilisearch\Domain\Search\ResultSet\Facets\OptionBased\Hierarchy;
/*
* 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\AbstractFacetPackage;
/**
* Class HierarchyPackage
*/
class HierarchyPackage extends AbstractFacetPackage {
/**
* @return string
*/
public function getParserClassName() {
return (string)HierarchyFacetParser::class;
}
/**
* @return string
*/
public function getUrlDecoderClassName() {
return (string)HierarchyUrlDecoder::class;
}
}

View File

@@ -0,0 +1,41 @@
<?php
namespace WapplerSystems\Meilisearch\Domain\Search\ResultSet\Facets\OptionBased\Hierarchy;
/*
* 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!
*/
class HierarchyTool
{
/**
* Replaces all escaped slashes in a hierarchy path with @@@slash@@@ to afterwards
* only have slashes in the content that are real path separators.
*
* @param string $pathWithContentSlashes
* @return string
*/
public static function substituteSlashes(string $pathWithContentSlashes): string
{
return (string)str_replace('\/', '@@@slash@@@', $pathWithContentSlashes);
}
/**
* Replaces @@@slash@@@ with \/ to have the path usable for solr again.
*
* @param string $pathWithReplacedContentSlashes
* @return string
*/
public static function unSubstituteSlashes(string $pathWithReplacedContentSlashes): string
{
return (string)str_replace('@@@slash@@@', '\/', $pathWithReplacedContentSlashes);
}
}

View File

@@ -0,0 +1,64 @@
<?php
namespace WapplerSystems\Meilisearch\Domain\Search\ResultSet\Facets\OptionBased\Hierarchy;
/***************************************************************
* Copyright notice
*
* (c) 2012-2015 Ingo Renner <ingo@typo3.org>
* All rights reserved
*
* This script is part of the TYPO3 project. The TYPO3 project is
* free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 3 of the License, or
* (at your option) any later version.
*
* The GNU General Public License can be found at
* http://www.gnu.org/copyleft/gpl.html.
*
* This script is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* This copyright notice MUST APPEAR in all copies of the script!
***************************************************************/
use WapplerSystems\Meilisearch\Domain\Search\ResultSet\Facets\FacetUrlDecoderInterface;
/**
* Filter encoder to build Solr hierarchy queries from tx_meilisearch[filter]
*
* @author Ingo Renner <ingo@typo3.org>
*/
class HierarchyUrlDecoder implements FacetUrlDecoderInterface
{
/**
* Delimiter for hierarchies in the URL.
*
* @var string
*/
const DELIMITER = '/';
/**
* Parses the given hierarchy filter and returns a Solr filter query.
*
* @param string $hierarchy The hierarchy filter query.
* @param array $configuration Facet configuration
* @return string Lucene query language filter to be used for querying Solr
*/
public function decode($hierarchy, array $configuration = [])
{
$escapedHierarchy = HierarchyTool::substituteSlashes($hierarchy);
$escapedHierarchy = substr($escapedHierarchy, 1);
$escapedHierarchy = rtrim($escapedHierarchy, '/');
$hierarchyItems = explode(self::DELIMITER, $escapedHierarchy);
$filterContent = (count($hierarchyItems) - 1) . '-' . $escapedHierarchy . '/';
$filterContent = HierarchyTool::unSubstituteSlashes($filterContent);
return '"' . str_replace("\\", "\\\\", $filterContent) . '"';
}
}

View File

@@ -0,0 +1,119 @@
<?php
namespace WapplerSystems\Meilisearch\Domain\Search\ResultSet\Facets\OptionBased\Hierarchy;
/*
* 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\OptionBased\AbstractOptionFacetItem;
/**
* Value object that represent an option of a options facet.
*
* @author Frans Saris <frans@beech.it>
* @author Timo Hund <timo.hund@dkd.de>
*/
class Node extends AbstractOptionFacetItem
{
/**
* @var NodeCollection
*/
protected $childNodes;
/**
* @var Node
*/
protected $parentNode;
/**
* @var integer
*/
protected $depth;
/**
* @var string
*/
protected $key;
/**
* @param HierarchyFacet $facet
* @param Node $parentNode
* @param string $key
* @param string $label
* @param string $value
* @param int $documentCount
* @param bool $selected
*/
public function __construct(HierarchyFacet $facet, $parentNode = null, $key = '', $label = '', $value = '', $documentCount = 0, $selected = false)
{
parent::__construct($facet, $label, $value, $documentCount, $selected);
$this->value = $value;
$this->childNodes = new NodeCollection();
$this->parentNode = $parentNode;
$this->key = $key;
}
/**
* @return string
*/
public function getKey()
{
return $this->key;
}
/**
* @param Node $node
*/
public function addChildNode(Node $node)
{
$this->childNodes->add($node);
}
/**
* @return NodeCollection
*/
public function getChildNodes()
{
return $this->childNodes;
}
/**
* @return Node|null
*/
public function getParentNode()
{
return $this->parentNode;
}
/**
* @return bool
*/
public function getHasParentNode()
{
return $this->parentNode !== null;
}
/**
* @return bool
*/
public function getHasChildNodeSelected()
{
/** @var Node $childNode */
foreach ($this->childNodes as $childNode) {
if ($childNode->getSelected()) {
return true;
}
}
return false;
}
}

View File

@@ -0,0 +1,44 @@
<?php
namespace WapplerSystems\Meilisearch\Domain\Search\ResultSet\Facets\OptionBased\Hierarchy;
/*
* 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\AbstractFacetItemCollection;
/**
* Collection for facet options.
*
* @author Frans Saris <frans@beech.it>
* @author Timo Hund <timo.hund@dkd.de>
*/
class NodeCollection extends AbstractFacetItemCollection
{
/**
* @param Node $node
* @return NodeCollection
*/
public function add($node)
{
return parent::add($node);
}
/**
* @param int $position
* @return Node
*/
public function getByPosition($position)
{
return parent::getByPosition($position);
}
}

View File

@@ -0,0 +1,75 @@
<?php
namespace WapplerSystems\Meilisearch\Domain\Search\ResultSet\Facets\OptionBased;
/*
* 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\AbstractFacetItemCollection;
use WapplerSystems\Meilisearch\Domain\Search\ResultSet\Facets\OptionBased\Options\Option;
/**
* Collection for facet options.
*
* @author Frans Saris <frans@beech.it>
* @author Timo Hund <timo.hund@dkd.de>
*/
class OptionCollection extends AbstractFacetItemCollection
{
/**
* Returns an array of prefixes from the option labels.
*
* Red, Blue, Green => r, g, b
*
* Can be used in combination with getByPrefix() to group facet options by prefix (e.g. alphabetical).
*
* @param int $length
* @return array
*/
public function getLowercaseLabelPrefixes($length = 1)
{
$prefixes = $this->getLabelPrefixes($length);
return array_map('mb_strtolower', $prefixes);
}
/**
* @param string $filteredPrefix
* @return AbstractFacetItemCollection
*/
public function getByLowercaseLabelPrefix($filteredPrefix)
{
return $this->getFilteredCopy(function(Option $option) use ($filteredPrefix)
{
$filteredPrefixLength = mb_strlen($filteredPrefix);
$currentPrefix = mb_substr(mb_strtolower($option->getLabel()), 0, $filteredPrefixLength);
return $currentPrefix === $filteredPrefix;
});
}
/**
* @param int $length
* @return array
*/
protected function getLabelPrefixes($length = 1) : array
{
$prefixes = [];
foreach ($this->data as $option) {
/** @var $option Option */
$prefix = mb_substr($option->getLabel(), 0, $length);
$prefixes[$prefix] = $prefix;
}
return array_values($prefixes);
}
}

View File

@@ -0,0 +1,39 @@
<?php
namespace WapplerSystems\Meilisearch\Domain\Search\ResultSet\Facets\OptionBased\Options;
/*
* 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\OptionBased\AbstractOptionFacetItem;
/**
* Value object that represent an option of a options facet.
*
* @author Frans Saris <frans@beech.it>
* @author Timo Hund <timo.hund@dkd.de>
*/
class Option extends AbstractOptionFacetItem
{
/**
* @param OptionsFacet $facet
* @param string $label
* @param string $value
* @param int $documentCount
* @param bool $selected
* @param array $metrics
*/
public function __construct(OptionsFacet $facet, $label = '', $value = '', $documentCount = 0, $selected = false, $metrics = [])
{
parent::__construct($facet, $label, $value, $documentCount, $selected, $metrics);
}
}

View File

@@ -0,0 +1,49 @@
<?php
namespace WapplerSystems\Meilisearch\Domain\Search\ResultSet\Facets\OptionBased\Options;
/*
* 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\OptionBased\AbstractOptionsFacet;
use WapplerSystems\Meilisearch\Domain\Search\ResultSet\SearchResultSet;
/**
* Value object that represent a options facet.
*
* @author Frans Saris <frans@beech.it>
* @author Timo Hund <timo.hund@dkd.de>
*/
class OptionsFacet extends AbstractOptionsFacet
{
const TYPE_OPTIONS = 'options';
/**
* String
* @var string
*/
protected static $type = self::TYPE_OPTIONS;
/**
* OptionsFacet constructor
*
* @param SearchResultSet $resultSet
* @param string $name
* @param string $field
* @param string $label
* @param array $configuration Facet configuration passed from typoscript
*/
public function __construct(SearchResultSet $resultSet, $name, $field, $label = '', array $configuration = [])
{
parent::__construct($resultSet, $name, $field, $label, $configuration);
}
}

View File

@@ -0,0 +1,148 @@
<?php
namespace WapplerSystems\Meilisearch\Domain\Search\ResultSet\Facets\OptionBased\Options;
/*
* 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\AbstractFacetParser;
use WapplerSystems\Meilisearch\Domain\Search\ResultSet\SearchResultSet;
use WapplerSystems\Meilisearch\System\Solr\ResponseAdapter;
use TYPO3\CMS\Core\Utility\GeneralUtility;
use TYPO3\CMS\Extbase\SignalSlot\Dispatcher;
/**
* Class OptionsFacetParser
*/
class OptionsFacetParser extends AbstractFacetParser
{
/**
* @var Dispatcher
*/
protected $dispatcher;
/**
* @param Dispatcher $dispatcher
*/
public function injectDispatcher(Dispatcher $dispatcher)
{
$this->dispatcher = $dispatcher;
}
/**
* @param SearchResultSet $resultSet
* @param string $facetName
* @param array $facetConfiguration
* @return OptionsFacet|null
*/
public function parse(SearchResultSet $resultSet, $facetName, array $facetConfiguration)
{
$response = $resultSet->getResponse();
$fieldName = $facetConfiguration['field'];
$label = $this->getPlainLabelOrApplyCObject($facetConfiguration);
$optionsFromSolrResponse = $this->getOptionsFromSolrResponse($facetName, $response);
$metricsFromSolrResponse = $this->getMetricsFromSolrResponse($facetName, $response);
$optionsFromRequest = $this->getActiveFacetValuesFromRequest($resultSet, $facetName);
$hasOptionsInResponse = !empty($optionsFromSolrResponse);
$hasSelectedOptionsInRequest = count($optionsFromRequest) > 0;
$hasNoOptionsToShow = !$hasOptionsInResponse && !$hasSelectedOptionsInRequest;
$hideEmpty = !$resultSet->getUsedSearchRequest()->getContextTypoScriptConfiguration()->getSearchFacetingShowEmptyFacetsByName($facetName);
if ($hasNoOptionsToShow && $hideEmpty) {
return null;
}
/** @var $facet OptionsFacet */
$facet = $this->objectManager->get(
OptionsFacet::class,
$resultSet,
$facetName,
$fieldName,
$label,
$facetConfiguration
);
$hasActiveOptions = count($optionsFromRequest) > 0;
$facet->setIsUsed($hasActiveOptions);
$facet->setIsAvailable($hasOptionsInResponse);
$optionsToCreate = $this->getMergedFacetValueFromSearchRequestAndSolrResponse($optionsFromSolrResponse, $optionsFromRequest);
foreach ($optionsToCreate as $optionsValue => $count) {
if ($this->getIsExcludedFacetValue($optionsValue, $facetConfiguration)) {
continue;
}
$isOptionsActive = in_array($optionsValue, $optionsFromRequest);
$label = $this->getLabelFromRenderingInstructions($optionsValue, $count, $facetName, $facetConfiguration);
$facet->addOption($this->objectManager->get(Option::class, $facet, $label, $optionsValue, $count, $isOptionsActive, $metricsFromSolrResponse[$optionsValue]));
}
// after all options have been created we apply a manualSortOrder if configured
// the sortBy (lex,..) is done by the solr server and triggered by the query, therefore it does not
// need to be handled in the frontend.
$this->applyManualSortOrder($facet, $facetConfiguration);
$this->applyReverseOrder($facet, $facetConfiguration);
if(!is_null($this->dispatcher)) {
$this->dispatcher->dispatch(__CLASS__, 'optionsParsed', [&$facet, $facetConfiguration]);
}
return $facet;
}
/**
* @param string $facetName
* @param ResponseAdapter $response
* @return array
*/
protected function getOptionsFromSolrResponse($facetName, ResponseAdapter $response)
{
$optionsFromSolrResponse = [];
if (!isset($response->facets->{$facetName})) {
return $optionsFromSolrResponse;
}
foreach ($response->facets->{$facetName}->buckets as $bucket) {
$optionValue = $bucket->val;
$optionCount = $bucket->count;
$optionsFromSolrResponse[$optionValue] = $optionCount;
}
return $optionsFromSolrResponse;
}
/**
* @param string $facetName
* @param ResponseAdapter $response
* @return array
*/
protected function getMetricsFromSolrResponse($facetName, ResponseAdapter $response)
{
$metricsFromSolrResponse = [];
if (!isset($response->facets->{$facetName}->buckets)) {
return [];
}
foreach ($response->facets->{$facetName}->buckets as $bucket) {
$bucketVariables = get_object_vars($bucket);
foreach ($bucketVariables as $key => $value) {
if (strpos($key, 'metrics_') === 0) {
$metricsKey = str_replace('metrics_', '', $key);
$metricsFromSolrResponse[$bucket->val][$metricsKey] = $value;
}
}
}
return $metricsFromSolrResponse;
}
}

View File

@@ -0,0 +1,147 @@
<?php
namespace WapplerSystems\Meilisearch\Domain\Search\ResultSet\Facets\OptionBased\Options;
/*
* 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\DefaultFacetQueryBuilder;
use WapplerSystems\Meilisearch\Domain\Search\ResultSet\Facets\FacetQueryBuilderInterface;
use WapplerSystems\Meilisearch\Domain\Search\ResultSet\Facets\SortingExpression;
use WapplerSystems\Meilisearch\System\Configuration\TypoScriptConfiguration;
/**
* Class OptionsFacetQueryBuilder
*
* The Options facet query builder builds the facets as json structure
*
* @Todo: When we use json faceting for other facets some logic of this class can be moved to the base class.
*/
class OptionsFacetQueryBuilder extends DefaultFacetQueryBuilder implements FacetQueryBuilderInterface {
/**
* @param string $facetName
* @param TypoScriptConfiguration $configuration
* @return array
*/
public function build($facetName, TypoScriptConfiguration $configuration)
{
$facetParameters = [];
$facetConfiguration = $configuration->getSearchFacetingFacetByName($facetName);
$jsonFacetOptions = [
'type' => 'terms',
'field' => $facetConfiguration['field'],
];
$jsonFacetOptions['limit'] = $this->buildLimitForJson($facetConfiguration, $configuration);
$jsonFacetOptions['mincount'] = $this->buildMincountForJson($facetConfiguration, $configuration);
$sorting = $this->buildSortingForJson($facetConfiguration);
if (!empty($sorting)) {
$jsonFacetOptions['sort'] = $sorting;
}
if (is_array($facetConfiguration['metrics.'])) {
foreach ($facetConfiguration['metrics.'] as $key => $value) {
$jsonFacetOptions['facet']['metrics_' . $key] = $value;
}
}
$excludeTags = $this->buildExcludeTagsForJson($facetConfiguration, $configuration);
if (!empty($excludeTags)) {
$jsonFacetOptions['domain']['excludeTags'] = $excludeTags;
}
$facetParameters['json.facet'][$facetName] = $jsonFacetOptions;
return $facetParameters;
}
/**
* @param array $facetConfiguration
* @param TypoScriptConfiguration $configuration
* @return string
*/
protected function buildExcludeTagsForJson(array $facetConfiguration, TypoScriptConfiguration $configuration)
{
$excludeFields = [];
if ($configuration->getSearchFacetingKeepAllFacetsOnSelection()) {
if (!$configuration->getSearchFacetingCountAllFacetsForSelection()) {
// keepAllOptionsOnSelection globally active
foreach ($configuration->getSearchFacetingFacets() as $facet) {
$excludeFields[] = $facet['field'];
}
} else {
$excludeFields[] = $facetConfiguration['field'];
}
}
$isKeepAllOptionsActiveForSingleFacet = $facetConfiguration['keepAllOptionsOnSelection'] == 1;
if ($isKeepAllOptionsActiveForSingleFacet) {
$excludeFields[] = $facetConfiguration['field'];
}
if (!empty($facetConfiguration['additionalExcludeTags'])) {
$excludeFields[] = $facetConfiguration['additionalExcludeTags'];
}
return implode(',', array_unique($excludeFields));
}
/**
* @param array $facetConfiguration
* @param TypoScriptConfiguration $configuration
* @return int
*/
protected function buildLimitForJson(array $facetConfiguration, TypoScriptConfiguration $configuration)
{
if (isset($facetConfiguration['facetLimit'])) {
return (int)$facetConfiguration['facetLimit'];
} elseif (!is_null($configuration->getSearchFacetingFacetLimit()) && $configuration->getSearchFacetingFacetLimit() >= 0) {
return $configuration->getSearchFacetingFacetLimit();
} else {
return -1;
}
}
/**
* @param array $facetConfiguration
* @param TypoScriptConfiguration $configuration
* @return int
*/
protected function buildMincountForJson(array $facetConfiguration, TypoScriptConfiguration $configuration)
{
if (isset($facetConfiguration['minimumCount'])) {
return (int)$facetConfiguration['minimumCount'];
} elseif (!is_null($configuration->getSearchFacetingMinimumCount()) && (int)$configuration->getSearchFacetingMinimumCount() >= 0) {
return $configuration->getSearchFacetingMinimumCount();
} else {
return 1;
}
}
/**
* @param array $facetConfiguration
* @return string
*/
protected function buildSortingForJson(array $facetConfiguration) {
if (isset($facetConfiguration['sortBy'])) {
$sortingExpression = new SortingExpression();
$sorting = $facetConfiguration['sortBy'];
$direction = $facetConfiguration['sortDirection'];
return $sortingExpression->getForJsonFacet($sorting, $direction);
}
return '';
}
}

View File

@@ -0,0 +1,37 @@
<?php
namespace WapplerSystems\Meilisearch\Domain\Search\ResultSet\Facets\OptionBased\Options;
/*
* 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\AbstractFacetPackage;
/**
* Class OptionsPackage
*/
class OptionsPackage extends AbstractFacetPackage {
/**
* @return string
*/
public function getParserClassName() {
return (string)OptionsFacetParser::class;
}
/**
* @return string
*/
public function getQueryBuilderClassName() {
return (string)OptionsFacetQueryBuilder::class;
}
}

View File

@@ -0,0 +1,37 @@
<?php
namespace WapplerSystems\Meilisearch\Domain\Search\ResultSet\Facets\OptionBased\QueryGroup;
/*
* 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\OptionBased\AbstractOptionFacetItem;
/**
* Value object that represent an option of a queryGroup facet.
*
* @author Frans Saris <frans@beech.it>
* @author Timo Hund <timo.hund@dkd.de>
*/
class Option extends AbstractOptionFacetItem
{
/**
* @param QueryGroupFacet $facet
* @param string $label
* @param string $value
* @param int $documentCount
* @param bool $selected
*/
public function __construct(QueryGroupFacet $facet, $label = '', $value = '', $documentCount = 0, $selected = false)
{
parent::__construct($facet, $label, $value, $documentCount, $selected);
}
}

View File

@@ -0,0 +1,49 @@
<?php
namespace WapplerSystems\Meilisearch\Domain\Search\ResultSet\Facets\OptionBased\QueryGroup;
/*
* 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\OptionBased\AbstractOptionsFacet;
use WapplerSystems\Meilisearch\Domain\Search\ResultSet\SearchResultSet;
/**
* Class QueryGroupFacet
*
* @author Frans Saris <frans@beech.it>
* @author Timo Hund <timo.hund@dkd.de>
*/
class QueryGroupFacet extends AbstractOptionsFacet
{
const TYPE_QUERY_GROUP = 'queryGroup';
/**
* String
* @var string
*/
protected static $type = self::TYPE_QUERY_GROUP;
/**
* OptionsFacet constructor
*
* @param SearchResultSet $resultSet
* @param string $name
* @param string $field
* @param string $label
* @param array $configuration Facet configuration passed from typoscript
*/
public function __construct(SearchResultSet $resultSet, $name, $field, $label = '', array $configuration = [])
{
parent::__construct($resultSet, $name, $field, $label, $configuration);
}
}

View File

@@ -0,0 +1,143 @@
<?php
namespace WapplerSystems\Meilisearch\Domain\Search\ResultSet\Facets\OptionBased\QueryGroup;
/*
* 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\AbstractFacetParser;
use WapplerSystems\Meilisearch\Domain\Search\ResultSet\SearchResultSet;
use WapplerSystems\Meilisearch\System\Solr\ResponseAdapter;
/**
* Class QueryGroupFacetParser
*
* @author Frans Saris <frans@beech.it>
* @author Timo Hund <timo.hund@dkd.de>
*/
class QueryGroupFacetParser extends AbstractFacetParser
{
/**
* @param SearchResultSet $resultSet
* @param string $facetName
* @param array $facetConfiguration
* @return QueryGroupFacet|null
*/
public function parse(SearchResultSet $resultSet, $facetName, array $facetConfiguration)
{
$response = $resultSet->getResponse();
$fieldName = $facetConfiguration['field'];
$label = $this->getPlainLabelOrApplyCObject($facetConfiguration);
$rawOptions = $this->getRawOptions($response, $fieldName);
$noOptionsInResponse = $rawOptions === [];
$hideEmpty = !$resultSet->getUsedSearchRequest()->getContextTypoScriptConfiguration()->getSearchFacetingShowEmptyFacetsByName($facetName);
if ($noOptionsInResponse && $hideEmpty) {
return null;
}
/** @var QueryGroupFacet $facet */
$facet = $this->objectManager->get(
QueryGroupFacet::class,
$resultSet,
$facetName,
$fieldName,
$label,
$facetConfiguration
);
$activeFacets = $resultSet->getUsedSearchRequest()->getActiveFacetNames();
$facet->setIsUsed(in_array($facetName, $activeFacets, true));
if (!$noOptionsInResponse) {
$facet->setIsAvailable(true);
foreach ($rawOptions as $query => $count) {
$value = $this->getValueByQuery($query, $facetConfiguration);
// Skip unknown queries
if ($value === null) {
continue;
}
if ($this->getIsExcludedFacetValue($query, $facetConfiguration)) {
continue;
}
$isOptionsActive = $resultSet->getUsedSearchRequest()->getHasFacetValue($facetName, $value);
$label = $this->getLabelFromRenderingInstructions(
$value,
$count,
$facetName,
$facetConfiguration
);
$facet->addOption($this->objectManager->get(Option::class, $facet, $label, $value, $count, $isOptionsActive));
}
}
// after all options have been created we apply a manualSortOrder if configured
// the sortBy (lex,..) is done by the solr server and triggered by the query, therefore it does not
// need to be handled in the frontend.
$this->applyManualSortOrder($facet, $facetConfiguration);
$this->applyReverseOrder($facet, $facetConfiguration);
return $facet;
}
/**
* Get raw query options
*
* @param ResponseAdapter $response
* @param string $fieldName
* @return array
*/
protected function getRawOptions(ResponseAdapter $response, $fieldName)
{
$options = [];
foreach ($response->facet_counts->facet_queries as $rawValue => $count) {
if ((int)$count === 0) {
continue;
}
// todo: add test cases to check if this is needed https://forge.typo3.org/issues/45440
// remove tags from the facet.query response, for facet.field
// and facet.range Solr does that on its own automatically
$rawValue = preg_replace('/^\{!ex=[^\}]*\}(.*)/', '\\1', $rawValue);
list($field, $query) = explode(':', $rawValue, 2);
if ($field === $fieldName) {
$options[$query] = $count;
}
}
return $options;
}
/**
* @param string $query
* @param array $facetConfiguration
* @return string|null
*/
protected function getValueByQuery($query, array $facetConfiguration)
{
$value = null;
foreach ($facetConfiguration['queryGroup.'] as $valueKey => $config) {
if (isset($config['query']) && $config['query'] === $query) {
$value = rtrim($valueKey, '.');
break;
}
}
return $value;
}
}

View File

@@ -0,0 +1,38 @@
<?php
namespace WapplerSystems\Meilisearch\Domain\Search\ResultSet\Facets\OptionBased\QueryGroup;
/*
* 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\DefaultFacetQueryBuilder;
use WapplerSystems\Meilisearch\Domain\Search\ResultSet\Facets\FacetQueryBuilderInterface;
use WapplerSystems\Meilisearch\System\Configuration\TypoScriptConfiguration;
class QueryGroupFacetQueryBuilder extends DefaultFacetQueryBuilder implements FacetQueryBuilderInterface {
/**
* @param string $facetName
* @param TypoScriptConfiguration $configuration
* @return array
*/
public function build($facetName, TypoScriptConfiguration $configuration)
{
$facetParameters = [];
$facetConfiguration = $configuration->getSearchFacetingFacetByName($facetName);
foreach ($facetConfiguration['queryGroup.'] as $queryName => $queryConfiguration) {
$tags = $this->buildExcludeTags($facetConfiguration, $configuration);
$facetParameters['facet.query'][] = $tags . $facetConfiguration['field'] . ':' . $queryConfiguration['query'];
}
return $facetParameters;
}
}

View File

@@ -0,0 +1,46 @@
<?php
namespace WapplerSystems\Meilisearch\Domain\Search\ResultSet\Facets\OptionBased\QueryGroup;
/*
* 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\AbstractFacetPackage;
/**
* Class QueryGroupPackage
*/
class QueryGroupPackage extends AbstractFacetPackage {
/**
* @return string
*/
public function getParserClassName() {
return (string)QueryGroupFacetParser::class;
}
/**
* @return string
*/
public function getQueryBuilderClassName()
{
return (string)QueryGroupFacetQueryBuilder::class;
}
/**
* @return string
*/
public function getUrlDecoderClassName()
{
return (string)QueryGroupUrlDecoder::class;
}
}

View File

@@ -0,0 +1,50 @@
<?php
namespace WapplerSystems\Meilisearch\Domain\Search\ResultSet\Facets\OptionBased\QueryGroup;
/***************************************************************
* Copyright notice
*
* (c) 2012-2015 Ingo Renner <ingo@typo3.org>
* All rights reserved
*
* This script is part of the TYPO3 project. The TYPO3 project is
* free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 3 of the License, or
* (at your option) any later version.
*
* The GNU General Public License can be found at
* http://www.gnu.org/copyleft/gpl.html.
*
* This script is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* This copyright notice MUST APPEAR in all copies of the script!
***************************************************************/
use WapplerSystems\Meilisearch\Domain\Search\ResultSet\Facets\FacetUrlDecoderInterface;
/**
* Filter encoder to build facet query parameters
*
* @author Timo Hund <timo.hund@dkd.de>
* @author Ingo Renner <ingo@typo3.org>
*/
class QueryGroupUrlDecoder implements FacetUrlDecoderInterface
{
/**
* Parses the query filter from GET parameters in the URL and translates it
* to a Lucene filter value.
*
* @param string $filterValue the filter query from plugin
* @param array $configuration options set in a facet's configuration
* @return string Value to be used in a Lucene filter
*/
public function decode($filterValue, array $configuration = [])
{
return $configuration[$filterValue . '.']['query'];
}
}