Elasticsearch est un moteur de recherche avec api json rest, utilisant Lucene et écrit en Java. Une description de tous les avantages de ce moteur est disponible sur . Dans ce qui suit, nous appellerons Elasticsearch ES.
Des moteurs similaires sont utilisés pour des recherches complexes dans une base de données documentaires. Par exemple, recherche en tenant compte de la morphologie de la langue ou recherche par coordonnées géographiques.
Dans cet article, je parlerai des bases d'ES en utilisant l'exemple de l'indexation des articles de blog. Je vais vous montrer comment filtrer, trier et rechercher des documents.
Afin de ne pas dépendre du système d'exploitation, je ferai toutes les requêtes à ES en utilisant CURL. Il existe également un plugin pour Google Chrome appelé .
Le texte contient des liens vers de la documentation et d'autres sources. À la fin, vous trouverez des liens pour un accès rapide à la documentation. Des définitions de termes inconnus peuvent être trouvées dans .
Installation
Pour ce faire, nous avons d’abord besoin de Java. Développeurs installez les versions Java plus récentes que Java 8 mise à jour 20 ou Java 7 mise à jour 55.
La distribution ES est disponible sur . Après avoir décompressé l'archive, vous devez exécuter bin/elasticsearch. Aussi disponible . Il y a . .
Après l'installation et le lancement, vérifions la fonctionnalité :
# для удобства запомним адрес в переменную
#export ES_URL=$(docker-machine ip dev):9200
export ES_URL=localhost:9200
curl -X GET $ES_URLNous recevrons quelque chose comme ceci :
{
"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 article à ES :
# Добавим документ c id 1 типа post в индекс blog.
# ?pretty указывает, что вывод должен быть человеко-читаемым.
curl -XPUT "$ES_URL/blog/post/1?pretty" -d'
{
"title": "Веселые котята",
"content": "<p>Смешная история про котят<p>",
"tags": [
"котята",
"смешная история"
],
"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 créé automatiquement blog et poste. 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 mappage est généré automatiquement lorsque le document est indexé :
# Получим mapping всех типов индекса blog
curl -XGET "$ES_URL/blog/_mapping?pretty"Dans la réponse du serveur, j'ai ajouté les valeurs des champs du document indexé dans les commentaires :
{
"blog" : {
"mappings" : {
"post" : {
"properties" : {
/* "content": "<p>Смешная история про котят<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": ["котята", "смешная история"] */
"tags" : {
"type" : "string"
},
/* "title": "Веселые котята" */
"title" : {
"type" : "string"
}
}
}
}
}
}Il convient de noter que ES ne fait pas de différence entre une valeur unique et un tableau de valeurs. Par exemple, le champ title contient simplement un titre et le champ tags contient un tableau de chaînes, bien qu'elles soient représentées de la même manière dans le mappage.
Nous parlerons davantage de cartographie plus tard.
demandes
Récupérer un document par son identifiant :
# извлечем документ с id 1 типа post из индекса blog
curl -XGET "$ES_URL/blog/post/1?pretty"{
"_index" : "blog",
"_type" : "post",
"_id" : "1",
"_version" : 1,
"found" : true,
"_source" : {
"title" : "Веселые котята",
"content" : "<p>Смешная история про котят<p>",
"tags" : [ "котята", "смешная история" ],
"published_at" : "2014-09-12T20:44:42+00:00"
}
}De nouvelles clés sont apparues dans la réponse : _version и _source. En général, toutes les clés commençant par _ sont classés comme officiels.
clé _version affiche la version du document. C’est nécessaire pour que le mécanisme de verrouillage optimiste fonctionne. Par exemple, nous souhaitons modifier un document qui a la version 1. Nous soumettons le document modifié et indiquons qu'il s'agit d'une modification d'un document avec la version 1. Si quelqu'un d'autre a également modifié un document avec la version 1 et a soumis des modifications avant nous, alors ES n'acceptera pas nos modifications, car il stocke 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 un document source compressé. Si nous n'avons besoin que de l'identifiant, et non de l'intégralité du document source, nous pouvons alors désactiver le stockage source.
Si nous n'avons pas besoin d'informations supplémentaires, nous pouvons obtenir uniquement le contenu de _source :
curl -XGET "$ES_URL/blog/post/1/_source?pretty"{
"title" : "Веселые котята",
"content" : "<p>Смешная история про котят<p>",
"tags" : [ "котята", "смешная история" ],
"published_at" : "2014-09-12T20:44:42+00:00"
}
Vous pouvez é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" : "Веселые котята"
}
}Indexons quelques articles supplémentaires et exécutons des requêtes plus complexes.
curl -XPUT "$ES_URL/blog/post/2" -d'
{
"title": "Веселые щенки",
"content": "<p>Смешная история про щенков<p>",
"tags": [
"щенки",
"смешная история"
],
"published_at": "2014-08-12T20:44:42+00:00"
}'curl -XPUT "$ES_URL/blog/post/3" -d'
{
"title": "Как у меня появился котенок",
"content": "<p>Душераздирающая история про бедного котенка с улицы<p>",
"tags": [
"котята"
],
"published_at": "2014-07-21T20:44:42+00:00"
}'Сортировка
# найдем последний пост по дате публикации и извлечем поля 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" : "Веселые котята",
"published_at" : "2014-09-12T20:44:42+00:00"
},
"sort" : [ 1410554682000 ]
} ]
}
}Nous avons choisi le dernier post. size limite le nombre de documents à délivrer. total indique le nombre total de documents correspondant à la demande. sort dans la sortie contient un tableau d'entiers par lequel le tri est effectué. Ceux. la date a été convertie en un nombre entier. Plus d’informations sur le tri peuvent être trouvées dans .
Filtres et requêtes
ES depuis la version 2 ne fait pas de distinction entre les filtres et les requêtes, mais .
Un contexte de requête diffère d'un contexte de filtre dans le sens où la requête génère un _score et n'est pas mise en cache. Je vous montrerai ce qu'est _score plus tard.
Filtrer par date
Nous utilisons la demande dans le contexte du filtre :
# получим посты, опубликованные 1ого сентября или позже
curl -XGET "$ES_URL/blog/post/_search?pretty" -d'
{
"filter": {
"range": {
"published_at": { "gte": "2014-09-01" }
}
}
}'Filtrer par balises
Nous utilisons pour rechercher des identifiants de documents contenant un mot donné :
# найдем все документы, в поле 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" : "Веселые котята",
"tags" : [ "котята", "смешная история" ]
}
}, {
"_index" : "blog",
"_type" : "post",
"_id" : "3",
"_score" : 1.0,
"_source" : {
"title" : "Как у меня появился котенок",
"tags" : [ "котята" ]
}
} ]
}
}Recherche en texte intégral
Trois de nos documents contiennent les éléments suivants dans le champ de contenu :
<p>Смешная история про котят<p><p>Смешная история про щенков<p><p>Душераздирающая история про бедного котенка с улицы<p>
Nous utilisons pour rechercher des identifiants de documents contenant un mot donné :
# 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 recherchons « histoires » dans le champ de contenu, nous ne trouverons rien, car L'index contient uniquement les mots originaux, pas leurs radicaux. Afin d'effectuer une recherche de haute qualité, vous devez configurer l'analyseur.
Champ _score spectacles . Si la requête est exécutée dans un contexte de filtre, alors la valeur _score sera toujours égale à 1, ce qui signifie une correspondance complète avec le filtre.
Analyseurs
sont nécessaires pour convertir le texte source en un ensemble de jetons.
Les analyseurs se composent d'un et plusieurs en option . Tokenizer peut être précédé de plusieurs . Les tokeniseurs divisent la chaîne source en jetons, tels que des espaces et des caractères de ponctuation. TokenFilter peut modifier les jetons, en supprimer ou en ajouter de nouveaux, par exemple, ne laisser que le radical du mot, supprimer les prépositions, ajouter des synonymes. CharFilter - modifie la chaîne source entière, par exemple, supprime les balises HTML.
ES a plusieurs . Par exemple, un analyseur .
utilisons et voyons comment les analyseurs standard et russes transforment la chaîne « Histoires drôles sur les 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" : "веселые",
"start_offset" : 0,
"end_offset" : 7,
"type" : "<ALPHANUM>",
"position" : 0
}, {
"token" : "истории",
"start_offset" : 8,
"end_offset" : 15,
"type" : "<ALPHANUM>",
"position" : 1
}, {
"token" : "про",
"start_offset" : 16,
"end_offset" : 19,
"type" : "<ALPHANUM>",
"position" : 2
}, {
"token" : "котят",
"start_offset" : 20,
"end_offset" : 25,
"type" : "<ALPHANUM>",
"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" : "весел",
"start_offset" : 0,
"end_offset" : 7,
"type" : "<ALPHANUM>",
"position" : 0
}, {
"token" : "истор",
"start_offset" : 8,
"end_offset" : 15,
"type" : "<ALPHANUM>",
"position" : 1
}, {
"token" : "кот",
"start_offset" : 20,
"end_offset" : 25,
"type" : "<ALPHANUM>",
"position" : 3
} ]
}L'analyseur standard a divisé la chaîne par espaces et a tout converti en minuscules, l'analyseur russe a supprimé les mots sans importance, l'a converti en minuscules et a laissé le radical des mots.
Voyons quels Tokenizer, TokenFilters, CharFilters l'analyseur russe utilise :
{
"filter": {
"russian_stop": {
"type": "stop",
"stopwords": "_russian_"
},
"russian_keywords": {
"type": "keyword_marker",
"keywords": []
},
"russian_stemmer": {
"type": "stemmer",
"language": "russian"
}
},
"analyzer": {
"russian": {
"tokenizer": "standard",
/* TokenFilters */
"filter": [
"lowercase",
"russian_stop",
"russian_keywords",
"russian_stemmer"
]
/* CharFilters отсутствуют */
}
}
}Décrivons notre analyseur basé sur le russe, qui supprimera les balises html. Appelons-le par défaut, car un analyseur portant ce nom sera utilisé par défaut.
{
"filter": {
"ru_stop": {
"type": "stop",
"stopwords": "_russian_"
},
"ru_stemmer": {
"type": "stemmer",
"language": "russian"
}
},
"analyzer": {
"default": {
/* добавляем удаление html тегов */
"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 source, puis le standard tokenizer la divisera en jetons, les jetons résultants seront mis en minuscules, les mots insignifiants seront supprimés et les jetons restants resteront la racine du mot.
Création d'un index
Ci-dessus, nous avons décrit l'analyseur par défaut. Cela s’appliquera à tous les champs de chaîne. Notre article contient un tableau de balises, donc les balises seront également traitées par l'analyseur. Parce que Nous recherchons des publications par correspondance exacte avec une balise, nous devons alors désactiver l'analyse pour le champ des balises.
Créons un index blog2 avec un analyseur et un mapping, dans lequel l'analyse du champ tags 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 3 mêmes articles à cet index (blog2). Je vais omettre ce processus parce que... cela revient à ajouter des documents à l'index du blog.
Recherche en texte intégral avec prise en charge des expressions
Jetons un coup d'œil à un autre type de demande :
# найдем документы, в которых встречается слово 'истории'
# 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"
]
}
}
}'Parce que Nous utilisons un analyseur avec une racine russe, alors cette requête renverra tous les documents, même s'ils ne contiennent que le mot « histoire ».
La requête peut contenir des caractères spéciaux, par exemple :
""fried eggs" +(eggplant | potato) -frittata"Syntaxe de la requête :
+ signifies AND operation
| signifies OR operation
- negates a single token
" wraps a number of tokens to signify a phrase for searching
* at the end of a term signifies a prefix query
( and ) signify precedence
~N after a word signifies edit distance (fuzziness)
~N after a phrase signifies slop amount# найдем документы без слова 'щенки'
curl -XPOST "$ES_URL/blog2/post/_search?pretty" -d'
{
"query": {
"simple_query_string": {
"query": "-щенки",
"fields": [
"title^3",
"tags^2",
"content"
]
}
}
}'
# получим 2 поста про котиковréférences
PS
Si vous êtes intéressé par de tels articles-leçons, avez des idées pour de nouveaux articles ou avez des propositions de coopération, je serai heureux de recevoir un message dans un message personnel ou par mail m.kuzmin+habr@darkleaf.ru.
Source: habr.com
