Nozioni di base su Elasticsearch

Elasticsearch è un motore di ricerca con json rest api, che utilizza Lucene e scritto in Java. Una descrizione di tutti i vantaggi di questo motore è disponibile su il sito ufficiale. Nel seguito faremo riferimento ad Elasticsearch come ES.

Motori simili vengono utilizzati per ricerche complesse in un database di documenti. Ad esempio, la ricerca tenendo conto della morfologia della lingua o la ricerca per coordinate geografiche.

In questo articolo parlerò delle basi di ES utilizzando l'esempio dell'indicizzazione dei post del blog. Ti mostrerò come filtrare, ordinare e cercare documenti.

Per non dipendere dal sistema operativo, farò tutte le richieste a ES utilizzando CURL. Esiste anche un plugin per Google Chrome chiamato senso.

Il testo contiene collegamenti a documentazione e altre fonti. Alla fine sono presenti i link per un rapido accesso alla documentazione. Le definizioni dei termini non familiari si trovano in лоссарии.

Installazione dell'ES

Per fare ciò, abbiamo prima bisogno di Java. Sviluppatori Raccomandare installare le versioni Java più recenti di Java 8 aggiornamento 20 o Java 7 aggiornamento 55.

La distribuzione ES è disponibile su sito dello sviluppatore. Dopo aver decompresso l'archivio è necessario eseguire bin/elasticsearch. Anche disponibile pacchetti per apt e yum. C'è immagine ufficiale per la finestra mobile. Ulteriori informazioni sull'installazione.

Dopo l'installazione e l'avvio, controlliamo la funzionalità:

# для удобства запомним адрес в переменную
#export ES_URL=$(docker-machine ip dev):9200
export ES_URL=localhost:9200

curl -X GET $ES_URL

Riceveremo qualcosa del genere:

{
  "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"
}

indicizzazione

Aggiungiamo un post a 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"
}'

risposta del server:

{
  "_index" : "blog",
  "_type" : "post",
  "_id" : "1",
  "_version" : 1,
  "_shards" : {
    "total" : 2,
    "successful" : 1,
    "failed" : 0
  },
  "created" : false
}

ES creato automaticamente indice blog e тип inviare. Possiamo tracciare un'analogia condizionale: un indice è un database e un tipo è una tabella in questo database. Ogni tipo ha il proprio schema − mappatura, proprio come una tabella relazionale. La mappatura viene generata automaticamente quando il documento viene indicizzato:

# Получим mapping всех типов индекса blog
curl -XGET "$ES_URL/blog/_mapping?pretty"

Nella risposta del server ho aggiunto nei commenti i valori dei campi del documento indicizzato:

{
  "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"
          }
        }
      }
    }
  }
}

Vale la pena notare che ES non distingue tra un singolo valore e un array di valori. Ad esempio, il campo title contiene semplicemente un titolo e il campo tags contiene un array di stringhe, sebbene siano rappresentate allo stesso modo nella mappatura.
Parleremo più approfonditamente della mappatura in seguito.

richieste

Recupero di un documento tramite il suo 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" : "Веселые котята",
    "content" : "<p>Смешная история про котят<p>",
    "tags" : [ "котята", "смешная история" ],
    "published_at" : "2014-09-12T20:44:42+00:00"
  }
}

Nella risposta sono apparse nuove chiavi: _version и _source. In generale, tutte le chiavi iniziano con _ sono classificati come ufficiali.

Chiave _version mostra la versione del documento. È necessario affinché il meccanismo di blocco ottimistico funzioni. Ad esempio, vogliamo modificare un documento che ha la versione 1. Inviamo il documento modificato e indichiamo che si tratta di una modifica di un documento con la versione 1. Se anche qualcun altro ha modificato un documento con la versione 1 e ha inviato le modifiche prima di noi, allora ES non accetterà le nostre modifiche, perché memorizza il documento con la versione 2.

Chiave _source contiene il documento che abbiamo indicizzato. ES non utilizza questo valore per le operazioni di ricerca perché Gli indici vengono utilizzati per la ricerca. Per risparmiare spazio, ES memorizza un documento sorgente compresso. Se abbiamo bisogno solo dell'ID e non dell'intero documento sorgente, possiamo disabilitare l'archiviazione del sorgente.

Se non abbiamo bisogno di informazioni aggiuntive, possiamo ottenere solo il contenuto di _source:

curl -XGET "$ES_URL/blog/post/1/_source?pretty"
{
  "title" : "Веселые котята",
  "content" : "<p>Смешная история про котят<p>",
  "tags" : [ "котята", "смешная история" ],
  "published_at" : "2014-09-12T20:44:42+00:00"
}

Puoi anche selezionare solo alcuni campi:

# извлечем только поле title
curl -XGET "$ES_URL/blog/post/1?_source=title&pretty"
{
  "_index" : "blog",
  "_type" : "post",
  "_id" : "1",
  "_version" : 1,
  "found" : true,
  "_source" : {
    "title" : "Веселые котята"
  }
}

Indicizziamo qualche altro post ed eseguiamo query più complesse.

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"
}'

Ordinamento

# найдем последний пост по дате публикации и извлечем поля 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 ]
    } ]
  }
}

Abbiamo scelto l'ultimo post. size limita il numero di documenti da rilasciare. total mostra il numero totale di documenti corrispondenti alla richiesta. sort nell'output contiene un array di numeri interi in base al quale viene eseguito l'ordinamento. Quelli. la data è stata convertita in un numero intero. Maggiori informazioni sull'ordinamento possono essere trovate in documentazione.

Filtri e query

ES a partire dalla versione 2 non distingue invece tra filtri e query viene introdotto il concetto di contesto.
Un contesto di query differisce da un contesto di filtro in quanto la query genera un _score e non viene memorizzata nella cache. Ti mostrerò cos'è _score più tardi.

Filtra per data

Usiamo la richiesta gamma nel contesto del filtro:

# получим посты, опубликованные 1ого сентября или позже
curl -XGET "$ES_URL/blog/post/_search?pretty" -d'
{
  "filter": {
    "range": {
      "published_at": { "gte": "2014-09-01" }
    }
  }
}'

Filtra per tag

Noi usiamo interrogazione sui termini per cercare gli ID dei documenti contenenti una determinata parola:

# найдем все документы, в поле 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" : [ "котята" ]
      }
    } ]
  }
}

Ricerca a testo integrale

Tre dei nostri documenti contengono quanto segue nel campo del contenuto:

  • <p>Смешная история про котят<p>
  • <p>Смешная история про щенков<p>
  • <p>Душераздирающая история про бедного котенка с улицы<p>

Noi usiamo domanda di corrispondenza per cercare gli ID dei documenti contenenti una determinata parola:

# 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
    } ]
  }
}

Se però cerchiamo “storie” nel campo del contenuto, non troveremo nulla, perché L'indice contiene solo le parole originali, non le loro radici. Per effettuare una ricerca di alta qualità, è necessario configurare l'analizzatore.

Campo _score spettacoli pertinenza. Se la richiesta viene eseguita in un contesto di filtro, il valore _score sarà sempre uguale a 1, il che significa una corrispondenza completa con il filtro.

Gli analizzatori

Gli analizzatori sono necessari per convertire il testo sorgente in un insieme di token.
Gli analizzatori sono costituiti da uno Gettoniera e diversi facoltativi Filtri token. Tokenizer può essere preceduto da diversi CharFilter. I tokenizzatori suddividono la stringa di origine in token, ad esempio spazi e caratteri di punteggiatura. TokenFilter può modificare token, eliminarne o aggiungerne di nuovi, ad esempio lasciare solo la radice della parola, rimuovere preposizioni, aggiungere sinonimi. CharFilter: modifica l'intera stringa sorgente, ad esempio, elimina i tag html.

ES ne ha diversi analizzatori standard. Ad esempio, un analizzatore russo.

Usiamo api e vediamo come gli analizzatori standard e russi trasformano la stringa “Storie divertenti sui gattini”:

# используем анализатор 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'analizzatore standard ha diviso la stringa per spazi e ha convertito tutto in minuscolo, l'analizzatore russo ha rimosso le parole non importanti, le ha convertite in minuscolo e ha lasciato la radice delle parole.

Vediamo quali Tokenizer, TokenFilters, CharFilters utilizza l'analizzatore russo:

{
  "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 отсутствуют */
    }
  }
}

Descriviamo il nostro analizzatore basato sul russo, che taglierà i tag html. Chiamiamolo predefinito, perché per impostazione predefinita verrà utilizzato un analizzatore con questo nome.

{
  "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"
      ]
    }
  }
}

Innanzitutto, tutti i tag HTML verranno rimossi dalla stringa sorgente, quindi lo standard tokenizzatore la dividerà in token, i token risultanti verranno spostati in minuscolo, le parole insignificanti verranno rimosse e i token rimanenti rimarranno la radice della parola.

Creazione di un indice

Sopra abbiamo descritto l'analizzatore predefinito. Si applicherà a tutti i campi stringa. Il nostro post contiene una serie di tag, quindi anche i tag verranno elaborati dall'analizzatore. Perché Cerchiamo i post in base alla corrispondenza esatta con un tag, quindi dobbiamo disabilitare l'analisi per il campo dei tag.

Creiamo un indice blog2 con analizzatore e mappatura, in cui l'analisi del campo tag è disabilitata:

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"
        }
      }
    }
  }
}'

Aggiungiamo gli stessi 3 post a questo indice (blog2). Ometterò questo processo perché... è simile all'aggiunta di documenti all'indice del blog.

Ricerca di testo completo con supporto delle espressioni

Diamo un'occhiata ad un altro tipo di richiesta:

# найдем документы, в которых встречается слово 'истории'
# 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"
      ]
    }
  }
}'

Perché Stiamo utilizzando un analizzatore con radice russa, quindi questa richiesta restituirà tutti i documenti, sebbene contengano solo la parola "storia".

La richiesta può contenere caratteri speciali, ad esempio:

""fried eggs" +(eggplant | potato) -frittata"

Richiedi sintassi:

+ 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 поста про котиков

riferimenti

PS

Se siete interessati a tali articoli-lezioni, avete idee per nuovi articoli o proposte di collaborazione, sarò lieto di ricevere un messaggio personale o via e-mail all'indirizzo m.kuzmin+habr@darkleaf.ru.

Fonte: habr.com