Skip to content

Instantly share code, notes, and snippets.

@Nemo64
Last active April 17, 2022 17:34
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save Nemo64/b73b8fdba39b2b17e38f3da701f94213 to your computer and use it in GitHub Desktop.
Save Nemo64/b73b8fdba39b2b17e38f3da701f94213 to your computer and use it in GitHub Desktop.
Elasticsearch API Platform -> FOS Elastica configuration
<?php
namespace App\Search;
use ApiPlatform\Core\DataProvider;
use FOS\ElasticaBundle\Index\IndexManager;
use Symfony\Component\Serializer\Serializer;
/**
* This class is based on API Platforms elasticsearch implemenation.
* However, It uses the FOS Elastica Bundle connection.
*
* @see \ApiPlatform\Core\Bridge\Elasticsearch\DataProvider\CollectionDataProvider
* @see \ApiPlatform\Core\Bridge\Elasticsearch\DataProvider\ItemDataProvider
*/
class ElasticsearchProductProvider implements DataProvider\ContextAwareCollectionDataProviderInterface, DataProvider\ItemDataProviderInterface, DataProvider\RestrictedDataProviderInterface
{
private const INDEX_NAME = 'product';
private IndexManager $indexManager;
private DataProvider\Pagination $pagination;
private Serializer $serializer;
public function __construct(IndexManager $indexManager, DataProvider\Pagination $pagination, Serializer $serializer)
{
$this->indexManager = $indexManager;
$this->pagination = $pagination;
$this->serializer = $serializer;
}
public function supports(string $resourceClass, string $operationName = null, array $context = []): bool
{
return $resourceClass === \App\Entity\Product::class;
}
public function getItem(string $resourceClass, $id, string $operationName = null, array $context = [])
{
$query = [
'query' => [['term' => ['id' => $id]]],
];
$index = $this->indexManager->getIndex(self::INDEX_NAME);
$response = $index->request('_search', 'GET', $query)->getData();
if (count($response['hits']['hits']) < 1) {
return null;
}
return $this->serializer->denormalize(
$response['hits']['hits'][0]['_source'],
$resourceClass
);
}
public function getCollection(string $resourceClass, string $operationName = null, array $context = [])
{
$query = [
'from' => $this->pagination->getOffset($resourceClass, $operationName, $context),
'size' => $this->pagination->getLimit($resourceClass, $operationName, $context),
];
// $context['filters'] has the query string, you can use that to build the query
// you can also build filter classes with \ApiPlatform\Core\Api\FilterInterface
// look at existing filters to figure out how api platform intents this to be used
$index = $this->indexManager->getIndex(self::INDEX_NAME);
$response = $index->request('_search', 'GET', $query)->getData();
return new \ApiPlatform\Core\Bridge\Elasticsearch\DataProvider\Paginator(
$this->serializer,
$response,
$resourceClass,
$query['size'],
$query['from']
);
}
}
parameters:
env(ELASTICSEARCH_VERIFYPEER): yes
env(ELASTICSEARCH_VERIFYHOST): 2
fos_elastica:
# the database connection
clients:
default:
url: '%env(ELASTICSEARCH_URL)%'
curl:
# with this, you can disable ssl host check, which is useful in dev environments
!php/const CURLOPT_SSL_VERIFYPEER: '%env(bool:ELASTICSEARCH_VERIFYPEER)%'
!php/const CURLOPT_SSL_VERIFYHOST: '%env(int:ELASTICSEARCH_VERIFYHOST)%'
indexes:
product:
persistence:
driver: orm
model: App\Entity\Product
provider: ~ # you can set a provider to build your model yourself
finder: ~
# this defines how the domain model is serialized before being commited to elasticsearch
# https://symfony.com/doc/5.4/serializer.html#using-serialization-groups-annotations
serializer:
groups: [search]
# these properties are 1:1 the properties for elasticsearchs explicit mapping
# https://www.elastic.co/guide/en/elasticsearch/reference/7.17/explicit-mapping.html
# this, unintuitively, has nothing to do with serialization
properties:
title: { type: text, boost: 2.0 }
<?php
namespace App\ApiPlatform\Filter;
use ApiPlatform\Core\Bridge\Doctrine\Orm\Filter\ContextAwareFilterInterface;
use ApiPlatform\Core\Bridge\Doctrine\Orm\Util\QueryNameGeneratorInterface;
use Doctrine\ORM\QueryBuilder;
use FOS\ElasticaBundle\Index\IndexManager;
class FulltextFilter implements ContextAwareFilterInterface
{
private const INDEX_NAME = 'product';
private IndexManager $indexManager;
public function __construct(IndexManager $indexManager)
{
$this->indexManager = $indexManager;
}
public function getDescription(string $resourceClass): array
{
return [
'q' => [
'property' => 'q',
'type' => 'string',
'required' => false,
],
'order[relevance]' => [
'property' => 'order[relevance]',
'type' => 'string',
'required' => false,
],
];
}
public function apply(QueryBuilder $queryBuilder, QueryNameGeneratorInterface $queryNameGenerator, string $resourceClass, string $operationName = null, array $context = [])
{
$query = [
'_source' => false, // you don't need the source document, just the ids
'size' => 10000, // elasticsearch limit per page
'query' => [
'multi_match' => [
'query' => $context['filters']['query'],
'fields' => array_keys($this->properties),
]
],
];
$response = $this->indexManager->getIndex(self::INDEX_NAME)->request('/_search', 'GET', $query);
$ids = array_column($response->getData()['hits']['hits'] ?? [], '_id');
if (empty($ids)) {
$queryBuilder->andWhere('1 = 0'); // enforce empty result
return;
}
// search for the ids in the query
$rootAlias = $queryBuilder->getRootAliases()[0];
$queryBuilder->andWhere("$rootAlias.id IN (:fulltext_search_filter_ids)");
$queryBuilder->setParameter('fulltext_search_filter_ids', $ids);
// sort by id list aka relevance
if (in_array($context['filters']['order']['relevance'] ?? null, ['asc', 'desc'], true)) {
$queryBuilder->addSelect("FIELD($rootAlias.id, :fulltext_search_filter_ids) AS HIDDEN __order");
$queryBuilder->addOrderBy("__order", $context['filters']['order']['relevance']);
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment