🎨
\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
= Officiel ;ruflin/elastica
;madewithlove/elasticsearcher
(5.x) ;ongr/elasticsearch-dsl
;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);
$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
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 avec des Alias
Dans Elastically :
$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é 👌🏽
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.
$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); // Envoi du bulk
$indexer->scheduleIndex($index, $doc4);
$indexer->scheduleIndex($index, $doc5);
$indexer->flush(); // Et ici on force
Elastically, c'est une petite lib pour simplifier 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
!
{
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 ! 💥
{
"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.
flush()
💥.{
"op": "delete",
"class": "Author",
"id": 123
}
iterate()
.refresh_interval
;?explain=true
dans les URL ;
POST /_sql?format=txt
{
"query": "SELECT * FROM app LIMIT 5"
}
category | name
---------------+---------------
Dentifrice |WashWash 3000
Dentifrice |WashWash 3200 S
C'est dans X-Pack Basic donc gratuit.
POST /_sql/translate
pour obtenir le Query DSL.
Placez un Node Elasticsearch sur vos frontaux PHP.
analysis-icu
;Œ
indexé OE
;Ç
indexé C
;🍿 => 🍿, pop-corn
🍕 => 🍕, fromage, part, pizza
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.
Un Bulk répond toujours le code HTTP 200 !
Il faut toujours en lire la réponse :
{
"took" : 22,
"errors" : true,
"items" : [
{
"index" : {
"_index" : "app",
"_type" : "_doc",
"_id" : "1",
"status" : 400,
"error" : { … }
}
}
]
}
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 }
En cours de développement :
https://github.com/elastic/apm-agent-phpUn 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;