Skip to content

Instantly share code, notes, and snippets.

@lolautruche
Forked from pspanja/autocomplete.php
Created July 16, 2017 09:31
Show Gist options
  • Save lolautruche/111bb70fee8cc8b33cb0503b3df6c465 to your computer and use it in GitHub Desktop.
Save lolautruche/111bb70fee8cc8b33cb0503b3df6c465 to your computer and use it in GitHub Desktop.
How to implement suggest/autocomplete for Solr Search Engine for eZ Platform
<?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