🎨
\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/serializer
JMSSerializer
Jane
...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.