Βασικά στοιχεία της Elasticsearch

Το Elasticsearch είναι μια μηχανή αναζήτησης με json rest api, που χρησιμοποιεί Lucene και γράφεται σε Java. Μια περιγραφή όλων των πλεονεκτημάτων αυτού του κινητήρα είναι διαθέσιμη στη διεύθυνση επίσημη ιστοσελίδα. Στη συνέχεια θα αναφερθούμε στο Elasticsearch ως ES.

Παρόμοιες μηχανές χρησιμοποιούνται για σύνθετες αναζητήσεις σε μια βάση δεδομένων εγγράφων. Για παράδειγμα, αναζήτηση λαμβάνοντας υπόψη τη μορφολογία της γλώσσας ή αναζήτηση με γεωγραφικές συντεταγμένες.

Σε αυτό το άρθρο θα μιλήσω για τα βασικά του ES χρησιμοποιώντας το παράδειγμα της ευρετηρίασης αναρτήσεων ιστολογίου. Θα σας δείξω πώς να φιλτράρετε, να ταξινομείτε και να αναζητάτε έγγραφα.

Για να μην εξαρτώμαι από το λειτουργικό σύστημα, θα κάνω όλα τα αιτήματα στην ES χρησιμοποιώντας το CURL. Υπάρχει επίσης ένα πρόσθετο για το google chrome που ονομάζεται αίσθηση.

Σε όλο το κείμενο υπάρχουν σύνδεσμοι προς τεκμηρίωση και άλλες πηγές. Στο τέλος υπάρχουν σύνδεσμοι για γρήγορη πρόσβαση στην τεκμηρίωση. Ορισμοί άγνωστων όρων μπορούν να βρεθούν στο γλωσσάρια.

Εγκατάσταση ES

Για να γίνει αυτό, χρειαζόμαστε πρώτα Java. προγραμματιστές συνιστώ εγκαταστήστε εκδόσεις Java νεότερες από την ενημερωμένη έκδοση 8 της Java 20 ή την ενημερωμένη έκδοση 7 της Java 55.

Η διανομή ES είναι διαθέσιμη στη διεύθυνση ιστότοπος προγραμματιστή. Αφού αποσυσκευάσετε το αρχείο, πρέπει να εκτελέσετε bin/elasticsearch. Επίσης διαθέσιμο πακέτα για apt και yum. υπάρχει επίσημη εικόνα για το docker. Περισσότερα για την εγκατάσταση.

Μετά την εγκατάσταση και την εκκίνηση, ας ελέγξουμε τη λειτουργικότητα:

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

curl -X GET $ES_URL

Θα λάβουμε κάτι σαν αυτό:

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

Ευρετηρίαση

Ας προσθέσουμε μια ανάρτηση στο 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"
}'

απάντηση διακομιστή:

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

Το ES δημιουργήθηκε αυτόματα δείκτη blog και τύπου Θέση. Μπορούμε να σχεδιάσουμε μια αναλογία υπό όρους: ένα ευρετήριο είναι μια βάση δεδομένων και ένας τύπος είναι ένας πίνακας σε αυτήν τη βάση δεδομένων. Κάθε τύπος έχει το δικό του σχήμα − χαρτης, ακριβώς όπως ένας σχεσιακός πίνακας. Η αντιστοίχιση δημιουργείται αυτόματα όταν το έγγραφο έχει ευρετηριαστεί:

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

Στην απόκριση διακομιστή, πρόσθεσα τις τιμές των πεδίων του εγγράφου με ευρετήριο στα σχόλια:

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

Αξίζει να σημειωθεί ότι το ES δεν κάνει διαφοροποίηση μεταξύ μιας μεμονωμένης τιμής και ενός πίνακα τιμών. Για παράδειγμα, το πεδίο τίτλου περιέχει απλώς έναν τίτλο και το πεδίο ετικετών περιέχει μια σειρά από συμβολοσειρές, αν και αναπαρίστανται με τον ίδιο τρόπο στην αντιστοίχιση.
Θα μιλήσουμε περισσότερα για τη χαρτογράφηση αργότερα.

αιτήσεις

Ανάκτηση ενός εγγράφου με το αναγνωριστικό του:

# извлечем документ с 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"
  }
}

Νέα κλειδιά εμφανίστηκαν στην απάντηση: _version и _source. Σε γενικές γραμμές, όλα τα κλειδιά ξεκινούν από _ χαρακτηρίζονται ως επίσημοι.

Κλειδί _version δείχνει την έκδοση του εγγράφου. Χρειάζεται για να λειτουργήσει ο αισιόδοξος μηχανισμός κλειδώματος. Για παράδειγμα, θέλουμε να αλλάξουμε ένα έγγραφο που έχει την έκδοση 1. Υποβάλλουμε το τροποποιημένο έγγραφο και υποδεικνύουμε ότι πρόκειται για επεξεργασία ενός εγγράφου με την έκδοση 1. Εάν κάποιος άλλος επεξεργάστηκε επίσης ένα έγγραφο με την έκδοση 1 και υπέβαλε αλλαγές ενώπιόν μας, τότε Η ES δεν θα δεχτεί τις αλλαγές μας, γιατί αποθηκεύει το έγγραφο με την έκδοση 2.

Κλειδί _source περιέχει το έγγραφο που ευρετηριάσαμε. Η ES δεν χρησιμοποιεί αυτήν την τιμή για λειτουργίες αναζήτησης επειδή Τα ευρετήρια χρησιμοποιούνται για αναζήτηση. Για εξοικονόμηση χώρου, το ES αποθηκεύει ένα έγγραφο συμπιεσμένης πηγής. Εάν χρειαζόμαστε μόνο το αναγνωριστικό και όχι ολόκληρο το έγγραφο προέλευσης, τότε μπορούμε να απενεργοποιήσουμε την αποθήκευση προέλευσης.

Εάν δεν χρειαζόμαστε πρόσθετες πληροφορίες, μπορούμε να λάβουμε μόνο τα περιεχόμενα του _source:

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

Μπορείτε επίσης να επιλέξετε μόνο ορισμένα πεδία:

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

Ας προσθέσουμε στο ευρετήριο μερικές ακόμη αναρτήσεις και ας εκτελέσουμε πιο σύνθετα ερωτήματα.

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

Επιλέξαμε την τελευταία ανάρτηση. size περιορίζει τον αριθμό των εγγράφων που πρέπει να εκδοθούν. total δείχνει τον συνολικό αριθμό των εγγράφων που αντιστοιχούν στο αίτημα. sort στην έξοδο περιέχει έναν πίνακα ακεραίων με τους οποίους εκτελείται η ταξινόμηση. Εκείνοι. η ημερομηνία μετατράπηκε σε ακέραιο. Περισσότερες πληροφορίες σχετικά με την ταξινόμηση μπορείτε να βρείτε στο τεκμηρίωση.

Φίλτρα και ερωτήματα

Το ES αφού η έκδοση 2 δεν κάνει διάκριση μεταξύ φίλτρων και ερωτημάτων εισάγεται η έννοια των πλαισίων.
Ένα περιβάλλον ερωτήματος διαφέρει από ένα περιβάλλον φίλτρου στο ότι το ερώτημα δημιουργεί ένα _score και δεν αποθηκεύεται στην κρυφή μνήμη. Θα σας δείξω τι είναι το _score αργότερα.

Φιλτράρισμα κατά ημερομηνία

Χρησιμοποιούμε το αίτημα σειρά στο πλαίσιο του φίλτρου:

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

Φιλτράρισμα κατά ετικέτες

Χρησιμοποιούμε ερώτημα όρου για να αναζητήσετε αναγνωριστικά εγγράφων που περιέχουν μια δεδομένη λέξη:

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

Αναζήτηση πλήρους κειμένου

Τρία από τα έγγραφά μας περιέχουν τα ακόλουθα στο πεδίο περιεχομένου:

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

Χρησιμοποιούμε ερώτημα αντιστοίχισης για να αναζητήσετε αναγνωριστικά εγγράφων που περιέχουν μια δεδομένη λέξη:

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

Ωστόσο, αν ψάξουμε για «ιστορίες» στο πεδίο περιεχομένου, δεν θα βρούμε τίποτα, γιατί Το ευρετήριο περιέχει μόνο τις αρχικές λέξεις, όχι τους μίσχους τους. Για να κάνετε αναζήτηση υψηλής ποιότητας, πρέπει να διαμορφώσετε τον αναλυτή.

Πεδίο _score παραστάσεις συνάφεια. Εάν το αίτημα εκτελείται σε περιβάλλον φίλτρου, τότε η τιμή _score θα είναι πάντα ίση με 1, που σημαίνει πλήρη αντιστοίχιση με το φίλτρο.

Αναλυτές

Αναλυτές απαιτούνται για τη μετατροπή του αρχικού κειμένου σε ένα σύνολο διακριτικών.
Οι αναλυτές αποτελούνται από έναν Tokenizer και πολλά προαιρετικά TokenFilters. Το Tokenizer μπορεί να προηγηθεί από πολλά CharFilters. Τα Tokenizers σπάζουν τη συμβολοσειρά πηγής σε διακριτικά, όπως κενά και χαρακτήρες στίξης. Το TokenFilter μπορεί να αλλάξει διακριτικά, να διαγράψει ή να προσθέσει νέα, για παράδειγμα, να αφήσει μόνο το στέλεχος της λέξης, να αφαιρέσει προθέσεις, να προσθέσει συνώνυμα. CharFilter - αλλάζει ολόκληρη τη συμβολοσειρά πηγής, για παράδειγμα, κόβει τις ετικέτες html.

Το ES έχει πολλά τυπικοί αναλυτές. Για παράδειγμα, ένας αναλυτής ρωσικός.

Ας εκμεταλλευτούμε api και ας δούμε πώς οι τυπικοί και οι Ρώσοι αναλυτές μεταμορφώνουν τη σειρά "Funny stories about kittens":

# используем анализатор 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
  } ]
}

Ο τυπικός αναλυτής χώρισε τη συμβολοσειρά κατά κενά και μετέτρεψε τα πάντα σε πεζά, ο ρωσικός αναλυτής αφαίρεσε ασήμαντες λέξεις, τη μετέτρεψε σε πεζά και άφησε το στέλεχος των λέξεων.

Ας δούμε ποια Tokenizer, TokenFilters, CharFilters χρησιμοποιεί ο ρωσικός αναλυτής:

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

Ας περιγράψουμε τον αναλυτή μας με βάση τα ρωσικά, ο οποίος θα κόψει ετικέτες html. Ας το ονομάσουμε προεπιλογή, γιατί ένας αναλυτής με αυτό το όνομα θα χρησιμοποιηθεί από προεπιλογή.

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

Αρχικά, όλες οι ετικέτες HTML θα αφαιρεθούν από τη συμβολοσειρά πηγής, στη συνέχεια το πρότυπο του tokenizer θα το χωρίσει σε διακριτικά, τα διακριτικά που προκύπτουν θα μετακινηθούν σε πεζά γράμματα, οι ασήμαντες λέξεις θα αφαιρεθούν και τα υπόλοιπα διακριτικά θα παραμείνουν το στέλεχος της λέξης.

Δημιουργία ευρετηρίου

Παραπάνω περιγράψαμε τον προεπιλεγμένο αναλυτή. Θα ισχύει για όλα τα πεδία συμβολοσειράς. Η ανάρτησή μας περιέχει μια σειρά από ετικέτες, επομένως οι ετικέτες θα υποβληθούν επίσης σε επεξεργασία από τον αναλυτή. Επειδή Αναζητούμε αναρτήσεις κατά ακριβή αντιστοίχιση με μια ετικέτα, τότε πρέπει να απενεργοποιήσουμε την ανάλυση για το πεδίο ετικετών.

Ας δημιουργήσουμε ένα ιστολόγιο ευρετηρίου2 με αναλυτή και αντιστοίχιση, στο οποίο η ανάλυση του πεδίου ετικετών είναι απενεργοποιημένη:

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

Ας προσθέσουμε τις ίδιες 3 αναρτήσεις σε αυτό το ευρετήριο (blog2). Θα παραλείψω αυτή τη διαδικασία γιατί... είναι παρόμοιο με την προσθήκη εγγράφων στο ευρετήριο του ιστολογίου.

Αναζήτηση πλήρους κειμένου με υποστήριξη έκφρασης

Ας ρίξουμε μια ματιά σε έναν άλλο τύπο αιτήματος:

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

Επειδή Χρησιμοποιούμε έναν αναλυτή με ρωσική προέλευση, τότε αυτό το αίτημα θα επιστρέψει όλα τα έγγραφα, αν και περιέχουν μόνο τη λέξη "ιστορικό".

Το αίτημα μπορεί να περιέχει ειδικούς χαρακτήρες, για παράδειγμα:

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

Αίτημα σύνταξης:

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

παραπομπές

PS

Αν ενδιαφέρεστε για παρόμοια άρθρα-μαθήματα, έχετε ιδέες για νέα άρθρα ή έχετε προτάσεις για συνεργασία, τότε θα χαρώ να λάβω ένα μήνυμα με προσωπικό μήνυμα ή με email [προστασία μέσω email].

Πηγή: www.habr.com