Elasticsearch — un moteur de recherche avec une API REST JSON, utilisant Lucene et écrit en Java. La description de tous les avantages de ce moteur est disponible sur . Nous appellerons Elasticsearch ES dans le reste du texte.
Ces moteurs sont utilisés pour des recherches complexes dans une base de documents. Par exemple, la recherche tenant compte de la morphologie de la langue ou la recherche par coordonnées géographiques.
Dans cet article, je vais parler des bases d'ES à travers l'exemple d'indexation de publications de blog. Je montrerai comment filtrer, trier et rechercher des documents.
Pour ne pas dépendre du système d'exploitation, toutes les requêtes à ES seront effectuées avec CURL. Il existe également un plugin pour Google Chrome nommé .
Des liens vers la documentation et d'autres sources sont insérés dans le texte. À la fin, des liens pour un accès rapide à la documentation sont fournis. Les définitions des termes inconnus peuvent être lues dans le .
Installation d'ES
Pour cela, nous aurons d'abord besoin de Java. Les développeurs doivent installer une version de Java supérieure à Java 8 update 20 ou Java 7 update 55.
Le package ES est disponible sur . Après avoir extrait l'archive, il faut lancer bin/elasticsearch. Des . Il y a . .
Après l'installation et le lancement, vérifions le bon fonctionnement :
# для удобства запомним адрес в переменную
#export ES_URL=$(docker-machine ip dev):9200
export ES_URL=localhost:9200
curl -X GET $ES_URLNous devrions recevoir une réponse approximativement de ce type :
{
"name" : "Heimdall",
"cluster_name" : "elasticsearch",
"version" : {
"number" : "2.2.1",
"build_hash" : "d045fc29d1932bce18b2e65ab8b297fbf6cd41a1",
"build_timestamp" : "2016-03-09T09:38:54Z",
"build_snapshot" : false,
"lucene_version" : "5.4.1"
},
"tagline" : "You Know, for Search"
}Indexation
Ajoutons un post dans ES :
# Добавим документ c id 1 типа post в индекс blog.
# ?pretty указывает, что вывод должен быть человеко-читаемым.
curl -XPUT "$ES_URL/blog/post/1?pretty" -d'
{
"title": "Веселые котята",
"content": "<p>Une histoire drôle sur des chatons<p>",
"tags": [
"chatons",
"histoire drôle"
],
"published_at": "2014-09-12T20:44:42+00:00"
}'
réponse du serveur :
{
"_index" : "blog",
"_type" : "post",
"_id" : "1",
"_version" : 1,
"_shards" : {
"total" : 2,
"successful" : 1,
"failed" : 0
},
"created" : false
}
ES a automatiquement créé blog et post. On peut faire une analogie conditionnelle : un index est une base de données, et un type est une table dans cette base de données. Chaque type a son propre schéma — , tout comme une table relationnelle. Le mapping est généré automatiquement lors de l'indexation d'un document :
# Получим mapping всех типов индекса blog
curl -XGET "$ES_URL/blog/_mapping?pretty"Dans la réponse du serveur, j'ai ajouté en commentaires les valeurs des champs du document indexé :
{
"blog" : {
"mappings" : {
"post" : {
"properties" : {
/* "content": "<p>Une histoire drôle sur des chatons<p>", /* */
"content" : {
"type" : "string"
},
/* "published_at": "2014-09-12T20:44:42+00:00" */
"published_at" : {
"type" : "date",
"format" : "strict_date_optional_time||epoch_millis"
},
/* "tags": ["chatons", "histoire drôle"] */
"tags" : {
"type" : "string"
},
/* "title": "Chatons amusants" */
"title" : {
"type" : "string"
}
}
}
}
}
}Il convient de noter qu'ES ne fait pas de distinction entre une valeur unique et un tableau de valeurs. Par exemple, le champ title contient simplement le titre, tandis que le champ tags — un tableau de chaînes, bien qu'ils soient présentés dans le mapping de la même manière.
Plus tard, nous parlerons du mapping de manière plus approfondie.
Requêtes
Extraction d'un document par son ID :
# извлечем документ с id 1 типа post из индекса blog
curl -XGET "$ES_URL/blog/post/1?pretty"{
"_index": "blog",
"_type": "post",
"_id": "1",
"_version": 1,
"found": true,
"_source": {
"title": "\u0412\u0435\u0441\u0435\u043b\u044b\u0435 \u043a\u043e\u0442\u044f\u0442\u0430",
"content": "<p>Une histoire dr\u00f4le sur des chatons<p>",
"tags": [
"\u043a\u043e\u0442\u044f\u0442\u0430",
"\u0441\u043c\u0435\u0448\u043d\u0430\u044f \u0438\u0441\u0442\u043e\u0440\u0438\u044f"
],
"published_at": "2014-09-12T20:44:42+00:00"
}
}De nouvelles clés sont apparues dans la réponse : _version et _source. En général, toutes les clés commençant par _ sont liées aux systèmes.
Clé _version affiche la version du document. Cela est nécessaire pour le fonctionnement du mécanisme de verrouillage optimiste. Par exemple, nous voulons modifier un document ayant la version 1. Nous envoyons le document modifié et indiquons qu'il s'agit d'une révision du document avec la version 1. Si quelqu'un d'autre a également modifié le document avec la version 1 et a soumis ses changements avant nous, alors ES ne saura pas accepter nos changements, car il conserve le document avec la version 2.
Clé _source contient le document que nous avons indexé. ES n'utilise pas cette valeur pour les opérations de recherche, car les index sont utilisés pour la recherche. Pour économiser de l'espace, ES stocke le document source compressé. Si nous avons seulement besoin de l'id, et non de l'intégralité du document source, nous pouvons désactiver le stockage de la source.
Si nous n'avons pas besoin d'informations supplémentaires, nous pouvons obtenir uniquement le contenu _source :
curl -XGET "$ES_URL/blog/post/1/_source?pretty"{
"title": "\u0412\u0435\u0441\u0435\u043b\u044b\u0435 \u043a\u043e\u0442\u044f\u0442\u0430",
"content": "<p>Une histoire dr\u00f4le sur des chatons<p>",
"tags": [
"\u043a\u043e\u0442\u044f\u0442\u0430",
"\u0441\u043c\u0435\u0448\u043d\u0430\u044f \u0438\u0441\u0442\u043e\u0440\u0438\u044f"
],
"published_at": "2014-09-12T20:44:42+00:00"
}
Nous pouvons également sélectionner uniquement certains champs :
# извлечем только поле title
curl -XGET "$ES_URL/blog/post/1?_source=title&pretty"{
"_index" : "blog",
"_type" : "post",
"_id" : "1",
"_version" : 1,
"found" : true,
"_source" : {
"title" : "Joyeux chatons"
}
}Indexons quelques autres publications et effectuons des requêtes plus complexes.
curl -XPUT "$ES_URL/blog/post/2" -d'
{
"title": "Chiens amusants",
"content": "<p>Une histoire drôle sur des chiots<p>",
"tags": [
"chiots",
"histoire drôle"
],
"published_at": "2014-08-12T20:44:42+00:00"
}'curl -XPUT "$ES_URL/blog/post/3" -d'
{
"title": "Comment j'ai eu un chaton",
"content": "<p>Une histoire déchirante sur un pauvre chaton des rues<p>"
"tags": [
"chatons"
],
"published_at": "2014-07-21T20:44:42+00:00"
}'Tri
# найдем последний пост по дате публикации и извлечем поля title и published_at
curl -XGET "$ES_URL/blog/post/_search?pretty" -d'
{
"size": 1,
"_source": ["title", "published_at"],
"sort": [{"published_at": "desc"}]
}'{
"took" : 8,
"timed_out" : false,
"_shards" : {
"total" : 5,
"successful" : 5,
"failed" : 0
},
"hits" : {
"total" : 3,
"max_score" : null,
"hits" : [ {
"_index" : "blog",
"_type" : "post",
"_id" : "1",
"_score" : null,
"_source" : {
"title" : "Joyeux chatons",
"published_at" : "2014-09-12T20:44:42+00:00"
},
"sort" : [ 1410554682000 ]
} ]
}
}Nous avons sélectionné le dernier post. taille limite le nombre de documents dans les résultats. total montre le nombre total de documents correspondant à la requête. sort les résultats contiennent un tableau d'entiers par lesquels le tri est effectué. C'est-à-dire que la date a été transformée en un entier. Vous pouvez en savoir plus sur le tri dans .
Filtres et requêtes
Depuis la version 2, ES ne fait pas de distinction entre les filtres et les requêtes, au lieu de cela .
Le contexte d'une requête est différent du contexte d'un filtre dans le sens où la requête génère un _score et n'est pas mise en cache. Je vais vous expliquer plus tard ce qu'est un _score.
Filtrage par date
Utilisons la requête dans le contexte filter :
# получим посты, опубликованные 1ого сентября или позже
curl -XGET "$ES_URL/blog/post/_search?pretty" -d'
{
"filter": {
"range": {
"published_at": { "gte": "2014-09-01" }
}
}
}'Filtrage par étiquettes
Utiliser pour rechercher les id des documents contenant le mot spécifié :
# найдем все документы, в поле tags которых есть элемент 'котята'
curl -XGET "$ES_URL/blog/post/_search?pretty" -d'
{
"_source": [
"title",
"tags"
],
"filter": {
"term": {
"tags": "котята"
}
}
}'{
"took" : 9,
"timed_out" : false,
"_shards" : {
"total" : 5,
"successful" : 5,
"failed" : 0
},
"hits" : {
"total" : 2,
"max_score" : 1.0,
"hits" : [ {
"_index" : "blog",
"_type" : "post",
"_id" : "1",
"_score" : 1.0,
"_source" : {
"title" : "Chatons amusants",
"tags" : [ "chatons", "histoire amusante" ]
}
}, {
"_index" : "blog",
"_type" : "post",
"_id" : "3",
"_score" : 1.0,
"_source" : {
"title" : "Comment j'ai eu un chaton",
"tags" : [ "chatons" ]
}
} ]
}
}Recherche en texte intégral
Trois de nos documents contiennent dans le champ content ce qui suit :
<p>Une histoire drôle sur des chatons<p><p>Une histoire drôle sur des chiots<p><p>Une histoire déchirante sur un pauvre chaton des rues<p>
Utiliser pour rechercher les id des documents contenant le mot spécifié :
# source: false означает, что не нужно извлекать _source найденных документов
curl -XGET "$ES_URL/blog/post/_search?pretty" -d'
{
"_source": false,
"query": {
"match": {
"content": "история"
}
}
}'{
"took" : 13,
"timed_out" : false,
"_shards" : {
"total" : 5,
"successful" : 5,
"failed" : 0
},
"hits" : {
"total" : 3,
"max_score" : 0.11506981,
"hits" : [ {
"_index" : "blog",
"_type" : "post",
"_id" : "2",
"_score" : 0.11506981
}, {
"_index" : "blog",
"_type" : "post",
"_id" : "1",
"_score" : 0.11506981
}, {
"_index" : "blog",
"_type" : "post",
"_id" : "3",
"_score" : 0.095891505
} ]
}
}Cependant, si nous cherchons «histoires» dans le champ contenu, nous ne trouvons rien, car l'index ne contient que des mots originaux, et non leurs bases. Pour effectuer une recherche de qualité, il est nécessaire de configurer l'analyseur.
Champ _score la quantité de données reçues et transmises, ainsi que le format ASP.NET Core SignalR. . Si la requête est exécutée dans le contexte du filtre, la valeur _score sera toujours égale à 1, ce qui signifie correspondance totale avec le filtre.
Analyseurs
sont nécessaires pour transformer le texte source en un ensemble de jetons.
Les analyseurs se composent d'un et de plusieurs . Le Tokenizer peut être précédé de plusieurs . Le Tokenizer divise la chaîne source en jetons, par exemple, par des espaces et des signes de ponctuation. Le TokenFilter peut modifier les jetons, supprimer ou en ajouter de nouveaux, par exemple, ne garder que la base des mots, enlever les prépositions, ajouter des synonymes. CharFilter modifie entièrement la chaîne source, par exemple, il coupe les balises html.
Il y a plusieurs . Par exemple, l'analyseur .
Utilisons et voyons comment les analyseurs standard et russian transforment la chaîne «Histoires amusantes de chatons» :
# используем анализатор standard
# обязательно нужно перекодировать не ASCII символы
curl -XGET "$ES_URL/_analyze?pretty&analyzer=standard&text=%D0%92%D0%B5%D1%81%D0%B5%D0%BB%D1%8B%D0%B5%20%D0%B8%D1%81%D1%82%D0%BE%D1%80%D0%B8%D0%B8%20%D0%BF%D1%80%D0%BE%20%D0%BA%D0%BE%D1%82%D1%8F%D1%82"{
"tokens" : [ {
"token" : "amusantes",
"start_offset" : 0,
"end_offset" : 7,
"type" : "",
"position" : 0
}, {
"token" : "histoires",
"start_offset" : 8,
"end_offset" : 15,
"type" : "",
"position" : 1
}, {
"token" : "de",
"start_offset" : 16,
"end_offset" : 18,
"type" : "",
"position" : 2
}, {
"token" : "chatons",
"start_offset" : 19,
"end_offset" : 26,
"type" : "",
"position" : 3
} ]
}# используем анализатор russian
curl -XGET "$ES_URL/_analyze?pretty&analyzer=russian&text=%D0%92%D0%B5%D1%81%D0%B5%D0%BB%D1%8B%D0%B5%20%D0%B8%D1%81%D1%82%D0%BE%D1%80%D0%B8%D0%B8%20%D0%BF%D1%80%D0%BE%20%D0%BA%D0%BE%D1%82%D1%8F%D1%82"{
"tokens" : [ {
"token" : "joyeux",
"start_offset" : 0,
"end_offset" : 7,
"type" : "",
"position" : 0
}, {
"token" : "histoire",
"start_offset" : 8,
"end_offset" : 15,
"type" : "",
"position" : 1
}, {
"token" : "chat",
"start_offset" : 20,
"end_offset" : 25,
"type" : "",
"position" : 3
} ]
}L'analyseur standard a découpé la chaîne par les espaces et a tout converti en minuscules, l'analyseur russe a éliminé les mots insignifiants, les a mis en minuscules et a conservé la racine des mots.
Voyons quels Tokenizers, TokenFilters et CharFilters utilise l'analyseur russe :
{
"filter": {
"russian_stop": {
"type": "stop",
"stopwords": "_russian_"
},
"russian_keywords": {
"type": "keyword_marker",
"keywords": []
},
"russian_stemmer": {
"type": "stemmer",
"language": "russian"
}
},
"analyzer": {
"russian": {
"tokenizer": "standard",
"filter": [
"lowercase",
"russian_stop",
"russian_keywords",
"russian_stemmer"
]
}
}
}Décrivons notre analyseur basé sur le russe, qui supprimera les balises HTML. Appelons-le par défaut, car un analyseur avec ce nom sera utilisé par défaut.
{
"filter": {
"ru_stop": {
"type": "stop",
"stopwords": "_russian_"
},
"ru_stemmer": {
"type": "stemmer",
"language": "russian"
}
},
"analyzer": {
"default": {
"char_filter": ["html_strip"],
"tokenizer": "standard",
"filter": [
"lowercase",
"ru_stop",
"ru_stemmer"
]
}
}
}Tout d'abord, toutes les balises HTML seront supprimées de la chaîne d'origine, ensuite elle sera divisée en tokens par le tokenizer standard, les tokens obtenus seront convertis en minuscules, les mots insignifiants seront éliminés et il ne restera que la racine des mots.
Création de l'index
Nous avons décrit l'analyseur par défaut ci-dessus. Il sera appliqué à tous les champs de type chaîne. Notre post contient un tableau de balises, donc les balises seront également traitées par l'analyseur. Étant donné que nous recherchons des posts par correspondance exacte des balises, il est nécessaire de désactiver l'analyse pour le champ des balises.
Créons l'index blog2 avec un analyseur et un mapping, dans lequel l'analyse du champ des balises est désactivée :
curl -XPOST "$ES_URL/blog2" -d'
{
"settings": {
"analysis": {
"filter": {
"ru_stop": {
"type": "stop",
"stopwords": "_russian_"
},
"ru_stemmer": {
"type": "stemmer",
"language": "russian"
}
},
"analyzer": {
"default": {
"char_filter": [
"html_strip"
],
"tokenizer": "standard",
"filter": [
"lowercase",
"ru_stop",
"ru_stemmer"
]
}
}
}
},
"mappings": {
"post": {
"properties": {
"content": {
"type": "string"
},
"published_at": {
"type": "date"
},
"tags": {
"type": "string",
"index": "not_analyzed"
},
"title": {
"type": "string"
}
}
}
}
}'Ajoutons les mêmes 3 articles dans cet index (blog2). Je vais omettre ce processus car il est similaire à l’ajout de documents dans l’index blog.
Recherche en texte intégral avec support des expressions
Familiarisons-nous avec un autre type de requête :
# найдем документы, в которых встречается слово 'истории'
# query -> simple_query_string -> query содержит поисковый запрос
# поле title имеет приоритет 3
# поле tags имеет приоритет 2
# поле content имеет приоритет 1
# приоритет используется при ранжировании результатов
curl -XPOST "$ES_URL/blog2/post/_search?pretty" -d'
{
"query": {
"simple_query_string": {
"query": "истории",
"fields": [
"title^3",
"tags^2",
"content"
]
}
}
}'Puisque nous utilisons un analyseur avec stemmer russe, cette requête retournera tous les documents, même si seulement le mot 'histoire' y apparaît.
Une requête peut contenir des caractères spéciaux, par exemple :
""fried eggs" +(aubergine | pomme de terre) -frittata"Syntaxe de la requête :
+ signifie l'opération AND
| signifie l'opération OR
- négatif un seul token
" encadre un nombre de tokens pour indiquer une phrase à rechercher
* à la fin d'un terme signifie une requête de préfixe
( et ) indiquent la priorité
~N après un mot signifie la distance d’édition (fuzzy)
~N après une phrase signifie le montant de slop# найдем документы без слова 'щенки'
curl -XPOST "$ES_URL/blog2/post/_search?pretty" -d'
{
"query": {
"simple_query_string": {
"query": "-щенки",
"fields": [
"title^3",
"tags^2",
"content"
]
}
}
}'
# получим 2 поста про котиковLiens
PS
Si ces articles-tutoriels vous intéressent, si vous avez des idées pour de nouveaux articles ou si vous avez des suggestions de collaboration, je serai ravi de recevoir un message en privé ou par e-mail à m.kuzmin+habr@darkleaf.ru.
Source : habr.com
