Fondamenti di Elasticsearch

Elasticsearch è un motore di ricerca con API REST JSON, basato su Lucene e scritto in Java. La descrizione di tutti i vantaggi di questo motore è disponibile su sito ufficiale. In seguito, ci riferiremo a Elasticsearch come ES.

Motori simili vengono utilizzati per ricerche avanzate in basi di documenti. Ad esempio, ricerche che tengono conto della morfologia della lingua o ricerche in base a coordinate geografiche.

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

Per non dipendere dal sistema operativo, eseguirò tutte le richieste a ES utilizzando CURL. È inoltre disponibile un plugin per Google Chrome chiamato sense.

Nel testo sono presenti link alla documentazione e ad altre fonti. Alla fine sono stati inseriti link per un accesso rapido alla documentazione. Le definizioni dei termini sconosciuti possono essere lette nel glossario.

Installazione di ES

Per questo avremo prima bisogno di Java. Gli sviluppatori La filosofia del DevOps. L'arte di gestire l'IT dovrebbero installare versioni di Java più recenti di Java 8 update 20 o Java 7 update 55.

Il pacchetto ES è disponibile su sito dello sviluppatore. Dopo aver estratto l'archivio, è necessario eseguire bin/elasticsearch. Sono inoltre disponibili pacchetti per apt e yum. C'è immagine ufficiale per Docker. Maggiori dettagli sull'installazione.

Dopo l'installazione e l'avvio, verifichiamo se tutto funziona:

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

curl -X GET $ES_URL

Riceveremo una risposta simile a questa:

{
  "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>Una storia divertente sui gatti<p>",
  "tags": [
    "gatti",
    "storia divertente"
  ],
  "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 ha creato automaticamente indice blog e tipo post. Si può fare una parziale analogia: un indice è un database, mentre un tipo è una tabella in quel database. Ogni tipo ha il proprio schema — mapping, proprio come una tabella relazionale. Il mapping viene generato automaticamente durante l'indicizzazione di un documento:

# Получим 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>Una storia divertente sui gatti<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": ["gattini", "storia divertente"] */
          "tags" : {
            "type" : "string"
          },
          /*  "title": "Gattini divertenti" */
          "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 il titolo, mentre il campo tags è un array di stringhe, anche se sono presentati nel mapping allo stesso modo.
Più avanti parleremo di mapping in modo più dettagliato.

Richieste

Estrazione del documento per 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": "\u0412\u0435\u0441\u0435\u043b\u044b\u0435 \u043a\u043e\u0442\u044f\u0442\u0430",
        "content": "<p>Una storia divertente sui gatti<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"
    }
}

Nella risposta sono apparsi nuovi chiavi: _version e _source. In generale, tutte le chiavi che iniziano con _ si riferiscono a elementi di servizio.

Chiave _version mostra la versione del documento. È necessaria per il funzionamento del meccanismo di blocchi ottimisti. Ad esempio, vogliamo modificare un documento che ha la versione 1. Inviamo il documento modificato e indichiamo che si tratta della revisione del documento con la versione 1. Se qualcun altro ha anche modificato il documento con la versione 1 e ha inviato le modifiche prima di noi, ES non accetterà le nostre modifiche, poiché 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, poiché per la ricerca vengono utilizzati gli indici. Per risparmiare spazio, ES memorizza il documento originale compresso. Se abbiamo bisogno solo dell'id, e non dell'intero documento originale, è possibile disabilitare la memorizzazione dell'originale.

Se non abbiamo bisogno di ulteriori informazioni, possiamo ottenere solo il contenuto di _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>Una storia divertente sui gatti<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"
}

È anche possibile selezionare solo determinati campi:

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

Indicizziamo ancora alcuni post ed eseguiamo query più complesse.

curl -XPUT "$ES_URL/blog/post/2" -d'
{
  "title": "Cuccioli divertenti",
  "content": "<p>Una storia divertente sui cuccioli<p>",
  "tags": [
    "cuccioli",
    "storia divertente"
  ],
  "published_at": "2014-08-12T20:44:42+00:00"
}'
curl -XPUT "$ES_URL/blog/post/3" -d'
{
  "title": "Come ho adottato un gattino",
  "content": "<p>Una storia straziante di un povero gattino della strada<p>"
  "tags": [
    "gattini"
  ],
  "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" : "Gatti divertenti",
        "published_at" : "2014-09-12T20:44:42+00:00"
      },
      "sort" : [ 1410554682000 ]
    } ]
  }
}

Abbiamo selezionato l'ultimo post. size limita il numero di documenti nei risultati. totale mostra il numero totale di documenti che corrispondono alla query. sort nei risultati contiene un array di numeri interi, su cui viene effettuato l'ordinamento. Cioè, la data è stata convertita in un numero intero. Maggiori informazioni sull'ordinamento possono essere trovate in documentazione.

Filtri e query

ES dalla versione 2 non distingue tra filtri e query, invece viene introdotto il concetto di contesti Il contesto della query è diverso dal contesto del filtro in quanto la query genera _score e non viene memorizzata nella cache. Cosa sia _score lo mostrerò più tardi..
Filtraggio per data

Usiamo la query

nel contesto del filtro: range term query

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

Filtraggio per tag

Usa per cercare gli id dei documenti che contengono la parola specificata: per trovare l'id dei documenti che contengono la parola richiesta:

# найдем все документы, в поле 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" : "Gattini divertenti",
        "tags" : [ "gattini", "storia divertente" ]
      }
    }, {
      "_index" : "blog",
      "_type" : "post",
      "_id" : "3",
      "_score" : 1.0,
      "_source" : {
        "title" : "Come ho avuto un gattino",
        "tags" : [ "gattini" ]
      }
    } ]
  }
}

Ricerca full-text

Tre dei nostri documenti contengono nel campo content quanto segue:

  • <p>Una storia divertente sui gatti<p>
  • <p>Una storia divertente sui cuccioli<p>
  • <p>Una storia straziante di un povero gattino della strada<p>

Usa query di corrispondenza per trovare l'id dei documenti che contengono la parola richiesta:

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

Tuttavia, se cerchiamo «storie» nel campo contenuto, non troveremo nulla poiché nell'indice ci sono solo le parole originali e non le loro radici. Per effettuare una ricerca di qualità, è necessario configurare l'analizzatore.

Campo _score mostra pertinenza. Se la query viene eseguita nel contesto del filtro, il valore _score sarà sempre pari a 1, il che significa una corrispondenza completa con il filtro.

Analizzatori

Analizzatori sono necessari per trasformare il testo originale in un insieme di token.
Gli analizzatori sono composti da uno Tokenizer e diversi opzionali TokenFilters. Il Tokenizer può essere preceduto da diversi CharFilters. Il Tokenizer suddivide la stringa originale in token, ad esempio, per spazi e caratteri di punteggiatura. Il TokenFilter può modificare i token, rimuovere o aggiungere nuovi, ad esempio, mantenere solo la radice della parola, rimuovere le preposizioni, aggiungere sinonimi. Il CharFilter modifica l'intera stringa originale, ad esempio, rimuovendo i tag html.

In ES ci sono diversi analizzatori standard. Ad esempio, l'analizzatore russian.

Approfondiamo api e vediamo come gli analizzatori standard e russian 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" : "divertenti",
    "start_offset" : 0,
    "end_offset" : 9,
    "type" : "",
    "position" : 0
  }, {
    "token" : "storie",
    "start_offset" : 10,
    "end_offset" : 16,
    "type" : "",
    "position" : 1
  }, {
    "token" : "sui",
    "start_offset" : 17,
    "end_offset" : 20,
    "type" : "",
    "position" : 2
  }, {
    "token" : "gattini",
    "start_offset" : 21,
    "end_offset" : 28,
    "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" : "allegra",
    "start_offset" : 0,
    "end_offset" : 7,
    "type" : "",
    "position" : 0
  }, {
    "token" : "storia",
    "start_offset" : 8,
    "end_offset" : 15,
    "type" : "",
    "position" : 1
  }, {
    "token" : "gatto",
    "start_offset" : 20,
    "end_offset" : 25,
    "type" : "",
    "position" : 3
  } ]
}

L'analizzatore standard ha diviso la stringa in base agli spazi e ha convertito tutto in minuscolo. L'analizzatore russo ha rimosso le parole non significative, le ha trasformate in minuscole e ha mantenuto le radici delle parole.

Esaminiamo quali Tokenizer, TokenFilters e 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 non presenti */
    }
  }
}

Descriviamo il nostro analizzatore basato su russo, che rimuoverà i tag html. Lo chiameremo default, poiché un analizzatore con questo nome sarà utilizzato di default.

{
  "filter": {
    "ru_stop": {
      "type":       "stop",
      "stopwords":  "_russian_"
    },
    "ru_stemmer": {
      "type":       "stemmer",
      "language":   "russian"
    }
  },
  "analyzer": {
    "default": {
      /* aggiungiamo la rimozione dei tag html */
      "char_filter": ["html_strip"],
      "tokenizer":  "standard",
      "filter": [
        "lowercase",
        "ru_stop",
        "ru_stemmer"
      ]
    }
  }
}

Innanzitutto, verranno rimossi tutti i tag html dalla stringa iniziale, poi sarà divisa in token dal tokenizer standard, i token ottenuti verranno convertiti in minuscolo, verranno eliminati i termini non significativi e rimarranno solo le radici delle parole.

Creazione dell'indice

Abbiamo appena descritto l'analizzatore default. Questo sarà applicato a tutti i campi di tipo stringa. Il nostro post contiene un array di tag, pertanto anche i tag saranno elaborati dall'analizzatore. Poiché cerchiamo post per corrispondenza esatta del tag, è necessario disabilitare l'analisi per il campo tags.

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

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). Salterò questo processo, poiché è analogo all'aggiunta di documenti all'indice blog.

Ricerca a testo completo con supporto per espressioni

Esaminiamo un altro tipo di query:

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

Poiché utilizziamo un analizzatore con stemming russo, questa query restituirà tutti i documenti, anche se contengono solo la parola 'storia'.

La query può contenere simboli speciali, ad esempio:

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

Sintassi della query:

+ indica l'operazione AND
| indica l'operazione OR
- nega un singolo token
" avvolge un certo numero di token per indicare una frase da cercare
* alla fine di un termine indica una query di prefisso
( e ) indicano precedenza
~N dopo una parola indica la distanza di modifica (fuzzy)
~N dopo una frase indica il livello di slop
# найдем документы без слова 'щенки'
curl -XPOST "$ES_URL/blog2/post/_search?pretty" -d'
{
  "query": {
    "simple_query_string": {
      "query": "-щенки",
      "fields": [
        "title^3",
        "tags^2",
        "content"
      ]
    }
  }
}'

# получим 2 поста про котиков

Link

PS

Se sei interessato a articoli-insegnamenti simili, hai idee per nuovi articoli o hai suggerimenti di collaborazione, sarò felice di ricevere un messaggio in privato o via e-mail a m.kuzmin+habr@darkleaf.ru.

Fonte: habr.com

Acquista hosting affidabile per siti web con protezione DDoS, VPS VDS server 🔥 Acquista hosting affidabile per siti web con protezione DDoS, VPS VDS server | ProHoster