ข้อมูลเบื้องต้นเกี่ยวกับ Elasticsearch

Elasticsearch เป็นเครื่องมือค้นหาที่มี json rest api โดยใช้ Lucene และเขียนด้วยภาษา Java สามารถดูคำอธิบายข้อดีทั้งหมดของเครื่องยนต์นี้ได้ที่ เว็บไซต์อย่างเป็นทางการ. ต่อไปนี้เราจะเรียก Elasticsearch ว่า ES

เอ็นจิ้นที่คล้ายกันใช้สำหรับการค้นหาที่ซับซ้อนในฐานข้อมูลเอกสาร ตัวอย่างเช่น การค้นหาโดยคำนึงถึงสัณฐานวิทยาของภาษา หรือการค้นหาตามพิกัดทางภูมิศาสตร์

ในบทความนี้ ฉันจะพูดถึงพื้นฐานของ ES โดยใช้ตัวอย่างการจัดทำดัชนีโพสต์ในบล็อก ฉันจะแสดงวิธีการกรอง จัดเรียง และค้นหาเอกสาร

เพื่อไม่ให้ขึ้นอยู่กับระบบปฏิบัติการ ฉันจะส่งคำขอทั้งหมดไปยัง ES โดยใช้ CURL นอกจากนี้ยังมีปลั๊กอินสำหรับ Google Chrome ที่เรียกว่า ความรู้สึก.

ข้อความนี้มีลิงก์ไปยังเอกสารประกอบและแหล่งข้อมูลอื่นๆ ในตอนท้ายจะมีลิงก์สำหรับการเข้าถึงเอกสารอย่างรวดเร็ว คำจำกัดความของคำศัพท์ที่ไม่คุ้นเคยสามารถพบได้ใน อภิธานศัพท์.

การติดตั้ง ES

เพื่อที่จะทำสิ่งนี้ได้ อันดับแรกเราจำเป็นต้องมี Java นักพัฒนา แนะนำ ติดตั้ง Java เวอร์ชันใหม่กว่า Java 8 อัปเดต 20 หรือ Java 7 อัปเดต 55

มีจำหน่าย ES ได้ที่ เว็บไซต์ผู้พัฒนา. หลังจากคลายไฟล์เก็บถาวรแล้วคุณจะต้องเรียกใช้ bin/elasticsearch. มีจำหน่ายเช่นกัน แพ็คเกจสำหรับฉลาดและยำ. มี ภาพอย่างเป็นทางการสำหรับนักเทียบท่า. ข้อมูลเพิ่มเติมเกี่ยวกับการติดตั้ง.

หลังจากการติดตั้งและเปิดใช้งาน เรามาตรวจสอบฟังก์ชันกัน:

# для удобства запомним адрес в переменную
#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 สร้างขึ้นโดยอัตโนมัติ ดัชนี บล็อกและ ชนิด โพสต์. เราสามารถวาดการเปรียบเทียบแบบมีเงื่อนไขได้: ดัชนีคือฐานข้อมูล และประเภทคือตารางในฐานข้อมูลนี้ แต่ละประเภทมีโครงร่างของตัวเอง - การทำแผนที่เช่นเดียวกับตารางเชิงสัมพันธ์ การแมปจะถูกสร้างขึ้นโดยอัตโนมัติเมื่อมีการจัดทำดัชนีเอกสาร:

# Получим 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 จะจัดเก็บเอกสารต้นฉบับที่บีบอัด หากเราต้องการเพียง ID ไม่ใช่เอกสารต้นฉบับทั้งหมด เราก็สามารถปิดการใช้งานที่เก็บข้อมูลต้นทางได้

หากเราไม่ต้องการข้อมูลเพิ่มเติม เราจะได้เฉพาะเนื้อหาของ _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. โทเค็นแบ่งสตริงต้นฉบับออกเป็นโทเค็น เช่น ช่องว่างและอักขระเครื่องหมายวรรคตอน TokenFilter สามารถเปลี่ยนโทเค็น ลบ หรือเพิ่มโทเค็นใหม่ได้ เช่น เหลือเพียงต้นกำเนิดของคำ ลบคำบุพบท เพิ่มคำพ้องความหมาย CharFilter - เปลี่ยนสตริงต้นฉบับทั้งหมด เช่น ตัดแท็ก html ออก

ES มีหลายอย่าง เครื่องวิเคราะห์มาตรฐาน. เช่น เครื่องวิเคราะห์ รัสเซีย.

มาใช้ประโยชน์กันเถอะ API และมาดูกันว่าเครื่องวิเคราะห์มาตรฐานและรัสเซียแปลงสตริง "เรื่องตลกเกี่ยวกับลูกแมว" อย่างไร:

# используем анализатор 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 ทั้งหมดจะถูกลบออกจากสตริงต้นฉบับ จากนั้นมาตรฐานโทเค็นไนเซอร์จะแยกออกเป็นโทเค็น โทเค็นผลลัพธ์จะย้ายไปเป็นตัวพิมพ์เล็ก คำที่ไม่มีนัยสำคัญจะถูกลบออก และโทเค็นที่เหลือจะยังคงเป็นต้นกำเนิดของคำ

การสร้างดัชนี

ข้างต้นเราได้อธิบายเกี่ยวกับตัววิเคราะห์เริ่มต้น มันจะใช้กับฟิลด์สตริงทั้งหมด โพสต์ของเรามีอาร์เรย์ของแท็ก ดังนั้นเครื่องมือวิเคราะห์จะประมวลผลแท็กด้วย เพราะ เรากำลังมองหาโพสต์โดยการจับคู่แบบตรงทั้งหมดกับแท็ก จากนั้นเราจำเป็นต้องปิดการวิเคราะห์สำหรับฟิลด์แท็ก

มาสร้างดัชนี blog2 ด้วยตัววิเคราะห์และการแมป ซึ่งการวิเคราะห์ฟิลด์แท็กถูกปิดใช้งาน:

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

หากคุณสนใจบทความ-บทเรียนที่คล้ายกัน มีแนวคิดสำหรับบทความใหม่ หรือมีข้อเสนอความร่วมมือ ฉันยินดีที่จะรับข้อความทางข้อความส่วนตัวหรือทางอีเมล [ป้องกันอีเมล].

ที่มา: will.com