-
-
Save lolautruche/111bb70fee8cc8b33cb0503b3df6c465 to your computer and use it in GitHub Desktop.
How to implement suggest/autocomplete for Solr Search Engine for eZ Platform
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?php | |
namespace Vendor\Bundle\ProjectBundle\Core\Search\Solr\Query\Content\FacetBuilder; | |
use eZ\Publish\API\Repository\Values\Content\Query\FacetBuilder; | |
class SuggestionFacetBuilder extends FacetBuilder | |
{ | |
public $prefix; | |
} | |
namespace Vendor\Bundle\ProjectBundle\Core\Search\Solr\Query\Content\FacetBuilderVisitor; | |
use Vendor\Bundle\ProjectBundle\Core\Search\Solr\Query\Content\FacetBuilder\SuggestionFacetBuilder; | |
use EzSystems\EzPlatformSolrSearchEngine\Query\FacetBuilderVisitor; | |
use eZ\Publish\API\Repository\Values\Content\Query\FacetBuilder; | |
use eZ\Publish\API\Repository\Values\Content\Search\Facet; | |
class SuggestionFacetBuilderVisitor extends FacetBuilderVisitor | |
{ | |
/** | |
* @var string | |
*/ | |
protected $fieldPath; | |
/** | |
* @param string $fieldPath | |
*/ | |
public function __construct($fieldPath) | |
{ | |
$this->fieldPath = $fieldPath; | |
} | |
public function canMap($field) | |
{ | |
return $field === $this->fieldPath; | |
} | |
public function map($field, array $data) | |
{ | |
return new Facet\ContentTypeFacet( | |
array( | |
'name' => 'type', | |
'entries' => $this->mapData($data), | |
) | |
); | |
} | |
public function canVisit(FacetBuilder $facetBuilder) | |
{ | |
return $facetBuilder instanceof SuggestionFacetBuilder; | |
} | |
public function visit(FacetBuilder $facetBuilder) | |
{ | |
/** | |
* @var $facetBuilder \Vendor\Bundle\ProjectBundle\Core\Search\Solr\Query\Content\FacetBuilder\SuggestionFacetBuilder | |
*/ | |
return array( | |
'facet.field' => $this->fieldPath, | |
"f.{$this->fieldPath}.facet.prefix" => $facetBuilder->prefix, | |
"f.{$this->fieldPath}.facet.limit" => $facetBuilder->limit, | |
"f.{$this->fieldPath}.facet.mincount" => $facetBuilder->minCount, | |
); | |
} | |
} | |
namespace Vendor\Bundle\ProjectBundle\Core\Search\Solr; | |
class SuggestionExtractor | |
{ | |
/** | |
* Extracts suggestion results from $data returned by Solr backend. | |
* | |
* @param $data | |
* | |
* @return array | |
*/ | |
public function extract($data) | |
{ | |
$suggestions = []; | |
if (isset($data->facet_counts)) { | |
foreach ($data->facet_counts->facet_fields as $field => $facet) { | |
$count = count($facet)/2; | |
for ($k = 0; $k < $count; $k++) { | |
$suggestions[$facet[$k*2]] = $facet[$k*2+1]; | |
} | |
break; | |
} | |
} | |
return $suggestions; | |
} | |
} | |
namespace Vendor\Bundle\ProjectBundle\Core\Search\Solr; | |
use eZ\Publish\API\Repository\Values\Content\Query; | |
use eZ\Publish\API\Repository\Values\Content\LocationQuery; | |
use eZ\Publish\API\Repository\Values\Content\Query\Criterion; | |
use eZ\Publish\SPI\Persistence\Content\Handler as ContentHandler; | |
use EzSystems\EzPlatformSolrSearchEngine\ResultExtractor; | |
use EzSystems\EzPlatformSolrSearchEngine\CoreFilter; | |
use EzSystems\EzPlatformSolrSearchEngine\DocumentMapper; | |
use Vendor\Bundle\ProjectBundle\Core\Search\Solr\Query\Content\FacetBuilder\SuggestionFacetBuilder; | |
class Handler | |
{ | |
/** | |
* @var \Vendor\Bundle\ProjectBundle\Core\Search\Solr\Gateway | |
*/ | |
protected $gateway; | |
/** | |
* @var \Vendor\Bundle\ProjectBundle\Core\Search\Solr\SuggestionExtractor; | |
*/ | |
protected $suggestionExtractor; | |
/** | |
* @param \Vendor\Bundle\ProjectBundle\Core\Search\Solr\Gateway $gateway | |
* @param \eZ\Publish\SPI\Persistence\Content\Handler $contentHandler | |
* @param \EzSystems\EzPlatformSolrSearchEngine\ResultExtractor $resultExtractor | |
* @param \EzSystems\EzPlatformSolrSearchEngine\CoreFilter $coreFilter | |
* @param \Vendor\Bundle\ProjectBundle\Core\Search\Solr\SuggestionExtractor $suggestionExtractor | |
*/ | |
public function __construct( | |
Gateway $gateway, | |
ContentHandler $contentHandler, | |
ResultExtractor $resultExtractor, | |
CoreFilter $coreFilter, | |
SuggestionExtractor $suggestionExtractor | |
) { | |
$this->gateway = $gateway; | |
$this->contentHandler = $contentHandler; | |
$this->resultExtractor = $resultExtractor; | |
$this->coreFilter = $coreFilter; | |
$this->suggestionExtractor = $suggestionExtractor; | |
} | |
/** | |
* Suggests a list of values for the given prefix. | |
* | |
* @param string $prefix | |
* @param int $limit | |
* @param \eZ\Publish\API\Repository\Values\Content\Query\Criterion $filter | |
* @param array $fieldFilters - a map of filters for the returned fields. | |
* Currently supported: <code>array("languages" => array(<language1>,..))</code>. | |
* | |
* @return mixed | |
*/ | |
public function suggest( | |
$prefix, | |
$limit = 10, | |
Criterion $filter = null, | |
array $fieldFilters = array() | |
) { | |
$query = new Query( | |
[ | |
'query' => new Criterion\MatchAll(), | |
'filter' => $filter, | |
'limit' => 0, | |
'facetBuilders' => [ | |
new SuggestionFacetBuilder( | |
[ | |
'name' => 'suggestion', | |
'prefix' => strtolower($prefix), | |
'limit' => $limit, | |
] | |
), | |
], | |
] | |
); | |
$this->coreFilter->apply( | |
$query, | |
$fieldFilters, | |
DocumentMapper::DOCUMENT_TYPE_IDENTIFIER_CONTENT | |
); | |
return $this->suggestionExtractor->extract( | |
$this->gateway->suggest($query, $fieldFilters) | |
); | |
} | |
} | |
namespace Vendor\Bundle\ProjectBundle\Core\Search\Solr; | |
use Vendor\Bundle\ProjectBundle\Core\Search\Solr\Query\QueryConverter; | |
use eZ\Publish\API\Repository\Values\Content\Query; | |
use eZ\Publish\API\Repository\Values\Content\Query\Criterion; | |
use eZ\Publish\API\Repository\Values\Content\LocationQuery; | |
use EzSystems\EzPlatformSolrSearchEngine\Query\QueryConverter; | |
use EzSystems\EzPlatformSolrSearchEngine\Gateway\HttpClient; | |
use EzSystems\EzPlatformSolrSearchEngine\Gateway\EndpointResolver; | |
use EzSystems\EzPlatformSolrSearchEngine\Gateway\EndpointRegistry; | |
use RuntimeException; | |
class Gateway | |
{ | |
/** | |
* HTTP client to communicate with Solr server. | |
* | |
* @var \EzSystems\EzPlatformSolrSearchEngine\Gateway\HttpClient | |
*/ | |
protected $client; | |
/** | |
* @var \EzSystems\EzPlatformSolrSearchEngine\Gateway\EndpointResolver | |
*/ | |
protected $endpointResolver; | |
/** | |
* Endpoint registry service. | |
* | |
* @var \EzSystems\EzPlatformSolrSearchEngine\Gateway\EndpointRegistry | |
*/ | |
protected $endpointRegistry; | |
/** | |
* Solr Search Engine Content Query Converter | |
* | |
* @var \EzSystems\EzPlatformSolrSearchEngine\Query\QueryConverter | |
*/ | |
protected $queryConverter; | |
/** | |
* @param HttpClient $client | |
* @param \EzSystems\EzPlatformSolrSearchEngine\Gateway\EndpointResolver $endpointResolver | |
* @param \EzSystems\EzPlatformSolrSearchEngine\Gateway\EndpointRegistry $endpointRegistry | |
* @param \EzSystems\EzPlatformSolrSearchEngine\Query\QueryConverter $queryConverter | |
*/ | |
public function __construct( | |
HttpClient $client, | |
EndpointResolver $endpointResolver, | |
EndpointRegistry $endpointRegistry, | |
QueryConverter $queryConverter | |
) { | |
$this->client = $client; | |
$this->endpointResolver = $endpointResolver; | |
$this->endpointRegistry = $endpointRegistry; | |
$this->queryConverter = $queryConverter; | |
} | |
/** | |
* Returns search targets for given language settings. | |
* | |
* @param array $languageSettings | |
* | |
* @return string | |
*/ | |
protected function getSearchTargets($languageSettings) | |
{ | |
$shards = array(); | |
$endpoints = $this->endpointResolver->getSearchTargets($languageSettings); | |
if (!empty($endpoints)) { | |
foreach ($endpoints as $endpoint) { | |
$shards[] = $this->endpointRegistry->getEndpoint($endpoint)->getIdentifier(); | |
} | |
} | |
return implode(',', $shards); | |
} | |
/** | |
* Generate URL-encoded query string. | |
* | |
* Array markers, possibly added for the facet parameters, | |
* will be removed from the result. | |
* | |
* @param array $parameters | |
* | |
* @return string | |
*/ | |
protected function generateQueryString(array $parameters) | |
{ | |
return preg_replace( | |
'/%5B[0-9]+%5D=/', | |
'=', | |
http_build_query($parameters) | |
); | |
} | |
/** | |
* Suggests a list of values for the given prefix. | |
* | |
* @param \eZ\Publish\API\Repository\Values\Content\Query $query | |
* @param array $languageSettings - a map of filters for the returned fields. | |
* Currently supported: <code>array("languages" => array(<language1>,..))</code>. | |
* | |
* @return mixed | |
*/ | |
public function suggest(Query $query, array $languageSettings = array()) | |
{ | |
$parameters = $this->queryConverter->convert($query); | |
$searchTargets = $this->getSearchTargets($languageSettings); | |
if (!empty($searchTargets)) { | |
$parameters['shards'] = $searchTargets; | |
} | |
$queryString = $this->generateQueryString($parameters); | |
$response = $this->client->request( | |
'GET', | |
$this->endpointRegistry->getEndpoint( | |
$this->endpointResolver->getEntryEndpoint() | |
), | |
"/select?{$queryString}" | |
); | |
// @todo: Error handling? | |
$result = json_decode($response->body); | |
if (!isset($result->response)) { | |
throw new RuntimeException( | |
'->response not set: ' . var_export(array($result, $parameters), true) | |
); | |
} | |
return $result; | |
} | |
} | |
namespace Vendor\Bundle\ProjectBundle\Core\Search; | |
class SearchService | |
{ | |
/** | |
* @var \Vendor\Bundle\ProjectBundle\Core\Search\Solr\Handler | |
*/ | |
protected $searchHandler; | |
/** | |
* @var \eZ\Publish\Core\Repository\PermissionsCriterionHandler | |
*/ | |
protected $permissionsCriterionHandler; | |
/** | |
* Suggests a list of values for the given prefix. | |
* | |
* @param string $prefix | |
* @param int $limit | |
* @param \eZ\Publish\API\Repository\Values\Content\Query\Criterion $filter | |
* @param array $languageFilter Configuration for specifying prioritized languages query will be performed on. | |
* Currently supports: <code>array("languages" => array(<language1>,..), "useAlwaysAvailable" => bool)</code> | |
* useAlwaysAvailable defaults to true to avoid exceptions on missing translations | |
* @param bool $filterOnUserPermissions if true only the objects which is the user allowed to read are returned. | |
* | |
* @return mixed | |
*/ | |
public function suggest( | |
$prefix, | |
$limit = 10, | |
Criterion $filter = null, | |
array $languageFilter = array(), | |
$filterOnUserPermissions = true | |
) { | |
if ($filter === null) { | |
$filter = new Criterion\MatchAll(); | |
} | |
if ($filterOnUserPermissions && !$this->permissionsCriterionHandler->addPermissionsCriterion($filter)) { | |
return []; | |
} | |
return $this->searchHandler->suggest( | |
$prefix, | |
$limit, | |
$filter, | |
$languageFilter | |
); | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment