\file_get_contents()\json_decode()
$results = \json_decode(
file_get_contents('http://localhost:9200/_search'),
true
);
Merci ! Des questions 🙏🏽 ? Il nous reste 30 minutes.
elasticsearch/elasticsearch-php = Officiel ;friendsofsymfony/elastica-bundle ;ruflin/elastica ;madewithlove/elasticsearcher (5.x) ;doctrine/search Surprise !Expose toutes les API et gère le réseau avec RingPHP.
$params = [
'index' => 'app',
'body' => [
'query' => [
'match' => [ 'framework' => 'symfony' ]
]
]
];
$response = $client->search($params);
Implémentation plutôt que librairie. Très bien pour des projets rapides...
$params = [
'query' => [
'bool' => [
'must' => [
'match' => [
'category' =>
'Beurre'
]
]
]
]
];
$bool = new BoolQuery();
$bool->addMust(
new Match(
'category',
'Beurre'
)
);
Le travail est commencé pour mieux intégrer Elastica
avec le client officiel.
$index = $client->getIndex('app');
$doc = new Document(
42, // Document ID
['username' => 'hans', 'likes' => ['2', '3', '5']]
);
$index->addDocuments([$doc]);
If you're still using nested associative arrays for that, You're Doing It Wrong(tm).Use associative arrays basically never / Never type hint on arrays
Manipuler des objets en entrée et en résultat,
comme avec Doctrine.
// Création du DTO
$product = new Product();
$product->setName('Beurre Salé');
$product->setCategory('Produits vitaux');
// Indexation
$doc = new Document(43, $product);
$index->addDocuments([$doc]);
Nous allons devoir coder ! C'est pour ça qu'on est là !
PUT /app
{
"settings": {
"number_of_shards": 1,
"analysis": {
"analyzer": {
"yolo": {
"tokenizer": "standard"
}
}
}
},
"mappings": {
"properties": {
"name": { "type": "text", "analyzer": "yolo" },
"ref": { "type": "text", "analyzer": "yolo" }
}
}
}
↙️ ️Beaucoup de répétitions
filter:
app_french_stemmer:
type: stemmer
language: light_french
analyzer:
app_french_heavy:
tokenizer: icu_tokenizer
filter:
- app_french_elision
- icu_folding
- app_french_stemmer
settings:
number_of_shards: 1
# Include analyzers.yaml here
mappings:
properties:
name:
type: text
analyzer: app_standard
mappings:
properties:
first_name: &simple_text
type: text
fields:
raw: { type: keyword }
stemmed: { type: text, analyzer: french }
last_name: *simple_text
company: *simple_text
city:
<<: *simple_text
fields:
raw: { type: keyword }
mappings:
properties:
first_name:
type: text
fields:
raw: { type: keyword }
stemmed: { type: text, analyzer: french }
last_name:
type: text
fields:
raw: { type: keyword }
stemmed: { type: text, analyzer: french }
company:
type: text
fields:
raw: { type: keyword }
stemmed: { type: text, analyzer: french }
city:
type: text
fields:
raw: { type: keyword }
public function createIndex($indexName): Index
{
// Read the YAML's
$mapping = Yaml::parse(file_get_contents($indexName .'_mapping.yaml'));
$analyzer = Yaml::parse(file_get_contents('/analyzers.yaml'));
// Merge the YAML's
$mapping['settings']['analysis'] = array_merge_recursive(
$mapping['settings']['analysis'] ?? [],
$analyzer
);
// Build Index name
$realName = sprintf('%s_%s', $indexName, date('Y-m-d-His'));
$index = $this->client->getIndex($realName);
// Actually create the Index with Mapping
$index->create($mapping);
return $index;
}
Et voilà index créé !
Versionnez vos index !
Elasticsearch propose des alias d'index.
$realName = sprintf('%s_%s', $indexName, date('Y-m-d-His'));
$index = $this->client->getIndex($realName);
$index->create($mapping);
public function markAsLive(Index $index, $indexName): Response
{
$data = ['actions' => []];
$data['actions'][] = ['remove' => ['index' => '*', 'alias' => $indexName]];
$data['actions'][] = ['add' => ['index' => $index->getName(), 'alias' => $indexName]];
return $this->client->request('_aliases', Request::POST, $data);
}
Par défaut le mapping est dynamique,
il se crée tout seul ! 🎉
PUT /app/_doc/1
{ "rating": 9 }
PUT /app/_doc/2
{ "rating": 9.9 }
GET /app/_mapping
> { "rating": { "type": "long" }}
Oops, mon 9.9 perd sa décimale !
Désactivez le mapping dynamique. C'est bon
pour les tutos, pas pour les pros.
mappings:
dynamic: false
properties:
rating:
...
dynamic: true, c'est LE MAL ! 🔥🔥👿Indexer un Document avec Elastica
// Via Array
$doc = new Document(43, ['name' => 'Beurre salé']);
$index->addDocuments([$doc]);
// Or via JSON
$doc = new Document(43, '{"name": "Beurre salé"}');
$index->addDocuments([$doc]);
Pour passer d'un DTO PHP à un JSON :
\json_encode (coucou JsonSerializable)ObjectNormalizer de symfony/serializerJMSSerializerJane...Nous devons pouvoir dénormaliser aussi,
en résultat de recherche !
use Symfony\Component\Serializer as Serializer;
$serializer = new Serializer\Serializer([
new Serializer\Normalizer\ArrayDenormalizer(),
new Serializer\Normalizer\ObjectNormalizer(),
], [
new Serializer\Encoder\JsonEncoder()
]);
$serializer->serialize($product, 'json');
{"name":"WashWash 3000","category":"Dentifrice"}
// Super DTO
$product = new Product();
$product->setName('Pizza Poutine');
// Document Elastica
$doc = new Document(
43,
$serializer->serialize($product, 'json')
);
// Indexation
$index->addDocuments([$doc]);
Le DTO est indexé 👌🏽
$docs[] ;$index->addDocuments($docs); qui produit l'appel HTTP à l'API Bulk.Ça va pas du tout scaler 💥
$docs[] ;$index->addDocuments($docs);$index->addDocuments($docs); final.Vous aurez besoin d'une queue d'indexation locale :
$client = new Client();
$indexer = new \JoliCode\Elastically\Indexer($client);
$index = $client->getIndex('app');
$indexer->scheduleIndex($index, $doc1);
$indexer->scheduleIndex($index, $doc2);
$indexer->scheduleIndex($index, $doc3); // GO BULK
$indexer->scheduleIndex($index, $doc4);
$indexer->scheduleIndex($index, $doc5);
$indexer->flush(); // Et ici on force
Elastically c'est une petite lib pour simplier nos implémentations...
Elastica répond avec des Elastica\Result,
dont "data" est un Array associatif.
$results = $index->search(
new \Elastica\Query\Match('name', 'washwash')
);
// Elastica\ResultSet:
// results: Elastica\Result[]
namespace JoliCode\Elastically;
use Elastica\Document;
use Elastica\Result as ElasticaResult;
class Result extends ElasticaResult
{
protected $model;
// Getter + Setter
}
Extension du Result d'origine pour ajouter un "model".
Nous pouvons avoir notre propre builder de résultats.
$result = new Result($hit);
$result->setModel(
$this->serializer->denormalize(
$result->getSource(),
\Product::class
);
);
Et voilà nous avons des Product dans nos résultats !
À chaque recherche :
use \JoliCode\Elastically\ResultSetBuilder;
$search = $index->createSearch(
$query,
null,
new ResultSetBuilder($serializer)
);
Notre Search va répondre des Product !
flush() 💥.{
"op": "delete",
"class": "Author",
"id": 123
}
Un DTO pour notre "Message" :
namespace JoliCode\Elastically\Messenger;
final class IndexationRequest
{
private $operation;
private $type;
private $id;
public function __construct(string $type, string $id, string $operation = IndexationRequestHandler::OP_INDEX)
{
$this->type = $type;
$this->id = $id;
$this->operation = $operation;
}
...
Un Handler qui va traiter le message :
class IndexationRequestHandler implements MessageHandlerInterface
{
private $client;
public function __invoke(IndexationRequest $message)
{
$model = // todo fetch the model
$indexer->scheduleIndex($indexName, new Document($message->getId(), $model, '_doc'));
$indexer->flush();
}
}
Envoyer une demande d'indexation :
$this->bus->dispatch(
new IndexationRequest(Product::class, 123, 'index')
);
Traiter les messages :
bin/console messenger:consume-messages
Il est possible de changer le Client HTTP d'Elastica !
Elastica\Transport\AbstractTransport
Une simple classe à implémenter 🎉
/**
* Executes the transport request.
*
* @param Request $request Request object
* @param array $params Hostname, port, path, ...
*/
abstract public function
exec(Request $request, array $params): Response;

Placez un Node Elasticsearch sur vos frontaux PHP.
analysis-icu ;Œ indexé OE ;Ç indexé C ;🥔 => 🥔, potato, vegetable, food
app_elasticsearch:
type: elasticsearch:6.5 # Pas encore Elasticsearch 7 :(
configuration:
plugins:
- analysis-icu
$ symfony tunnel:open -f
$ docker run -it --rm --network host --name kibana_sfcloud \
-e ELASTICSEARCH_URL=http://127.0.0.1:30002 \
docker.elastic.co/kibana/kibana:6.5.4
https://github.com/jolicode/elastically
@damienalexandre
coucou@jolicode.com
Priscilla Du Preez
Aaron Burden
Suzanne D. Williams
Anthony Martino
Alexandre Godreau
David Clode
Vincent Botta
Brooke Lark
Winner ShutterStock
Merci à la team JoliCode et à Perrine.
Nicolas Ruflin pour avoir créé Elastica
et Joël Wurtz pour Jane.
ObjectNormalizer est plutôt lent car il va lire
votre DTO via \Reflection.
Jane va générer le DTO et ses Normalizer
en pure PHP via un JSON Schema.
composer require --dev jane-php/json-schema "^4.0"
composer require jane-php/json-schema-runtime "^4.0"
composer require --dev friendsofphp/php-cs-fixer "^2.7.3"
# Run the code generator
php vendor/bin/jane generate \
--config-file=jane-elasticsearch-dto-config.php
return [
'json-schema-file' => '/es.json',
'root-class' => 'Model',
'namespace' => 'Elasticsearch',
'directory' => '/generated',
];
{
"$schema": "http://json-schema.org/draft-07/schema#",
"definitions": {
"Product": {
"type": "object",
"properties": {
"name": {
"type": "string"
},
"category": {
"type": "string"
}
}
}
}
}
$ tree generated/
generated
├── Model
│ └── Product.php
└── Normalizer
├── NormalizerFactory.php
└── ProductNormalizer.php
namespace Elasticsearch\Model;
class Product
{
protected $name;
protected $category;
// + getter and setters
}
namespace Elasticsearch\Normalizer;
class ProductNormalizer implements DenormalizerInterface, NormalizerInterface, DenormalizerAwareInterface, NormalizerAwareInterface
{
use DenormalizerAwareTrait;
use NormalizerAwareTrait;
public function supportsDenormalization($data, $type, $format = null)
{
return $type === 'Elasticsearch\\Model\\Product';
}
public function supportsNormalization($data, $format = null)
{
return $data instanceof \Elasticsearch\Model\Product;
}
public function denormalize($data, $class, $format = null, array $context = [])
{
$object = new \Elasticsearch\Model\Product();
if (property_exists($data, 'name')) {
$object->setName($data->{'name'});
}
if (property_exists($data, 'category')) {
$object->setCategory($data->{'category'});
}
return $object;
}
public function normalize($object, $format = null, array $context = [])
{
$data = new \stdClass();
if (null !== $object->getName()) {
$data->{'name'} = $object->getName();
}
if (null !== $object->getCategory()) {
$data->{'category'} = $object->getCategory();
}
return $data;
}
}
namespace Elasticsearch\Normalizer;
class NormalizerFactory
{
public static function create()
{
$normalizers = [];
$normalizers[] = new \Symfony\Component\Serializer\Normalizer\ArrayDenormalizer();
$normalizers[] = new \Jane\JsonSchemaRuntime\Normalizer\ReferenceNormalizer();
$normalizers[] = new ProductNormalizer();
return $normalizers;
}
}
$normalizers = \Elasticsearch\Normalizer\NormalizerFactory::create();
$encoders = [
new JsonEncoder(
new JsonEncode([JsonEncode::OPTIONS => \JSON_UNESCAPED_SLASHES]),
new JsonDecode([JsonDecode::ASSOCIATIVE => false])
)
];
$serializer = new Serializer($normalizers, $encoders);
// Super fast Product serializer!
Vous pouvez l'utiliser pour tous vos besoins
DTO / Normalisation.
{
title: "Vive la Guinness",
url: "https://joli.beer/"
}
title: [vive] [guinness]
"url" n'est pas indexé !
Ne mettez pas de valeurs en clé de JSON.
{
"title": "Denver, le dernier dinosaure",
"comments": {
"340": { "text": "Pourquoi seulement 52 épisodes !" },
"341": { "text": "Rendez nous les Minikeums !" }
}
}
"Mapping explosion" en devenir ! 💥
Elasticsearch limite à 1000 le nombre de champs.
{
"title": "Denver, le dernier dinosaure",
"comments": [
{ "id": "340", "text": "Pourquoi seulement 52 épisodes !" },
{ "id": "341", "text": "Rendez nous les Minikeums !" }
]
}
Et vous gagnez la possibilité de chercher par Id.