أساسيات البحث المرنة

Elasticsearch هو محرك بحث مزود بـ json Rest API، باستخدام Lucene ومكتوب بلغة Java. يتوفر وصف لجميع مزايا هذا المحرك على الموقع الرسمي. في ما يلي سوف نشير إلى Elasticsearch باسم ES.

يتم استخدام محركات مماثلة لعمليات البحث المعقدة في قاعدة بيانات المستندات. على سبيل المثال، البحث مع الأخذ بعين الاعتبار مورفولوجية اللغة أو البحث عن طريق الإحداثيات الجغرافية.

سأتحدث في هذه المقالة عن أساسيات ES باستخدام مثال فهرسة منشورات المدونة. سأوضح لك كيفية تصفية المستندات وفرزها والبحث فيها.

لكي لا أعتمد على نظام التشغيل، سأقدم جميع الطلبات إلى ES باستخدام CURL. هناك أيضًا مكون إضافي لـ Google Chrome يسمى إحساس.

يحتوي النص على روابط للوثائق والمصادر الأخرى. وفي النهاية توجد روابط للوصول السريع إلى الوثائق. يمكن العثور على تعريفات للمصطلحات غير المألوفة في المسارد.

تثبيت

للقيام بذلك، نحتاج أولا إلى جافا. المطورين يوصي قم بتثبيت إصدارات 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 بتخزين مستند مصدر مضغوط. إذا كنا بحاجة إلى المعرف فقط، وليس المستند المصدر بأكمله، فيمكننا تعطيل تخزين المصدر.

إذا لم نكن بحاجة إلى معلومات إضافية، يمكننا الحصول فقط على محتويات _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 وعدة اختيارية مرشحات الرمز المميز. قد يسبق Tokenizer عدة مرشحات شار. تقوم أدوات الرموز المميزة بتقسيم السلسلة المصدر إلى رموز مميزة، مثل المسافات وأحرف الترقيم. يمكن لـ 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 من السلسلة المصدر، ثم سيقوم معيار الرمز المميز بتقسيمها إلى رموز مميزة، وستنتقل الرموز المميزة الناتجة إلى أحرف صغيرة، وستتم إزالة الكلمات غير المهمة، وستظل الرموز المميزة المتبقية هي أصل الكلمة.

إنشاء فهرس

أعلاه وصفنا المحلل الافتراضي. سيتم تطبيقه على جميع حقول السلسلة. يحتوي منشورنا على مجموعة من العلامات، لذلك ستتم معالجة العلامات أيضًا بواسطة المحلل. لأن نحن نبحث عن المشاركات حسب المطابقة التامة للعلامة، ثم نحتاج إلى تعطيل التحليل لحقل العلامات.

لنقم بإنشاء مدونة فهرس 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"
        }
      }
    }
  }
}'

دعونا نضيف نفس المشاركات الثلاث إلى هذا الفهرس (blog3). سأحذف هذه العملية لأن... إنه مشابه لإضافة المستندات إلى فهرس المدونة.

البحث عن النص الكامل مع دعم التعبير

دعونا نلقي نظرة على نوع آخر من الطلب:

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

إذا كنت مهتمًا بمقالات ودروس مماثلة، أو لديك أفكار لمقالات جديدة، أو لديك مقترحات للتعاون، فسأكون سعيدًا بتلقي رسالة في رسالة شخصية أو عبر البريد الإلكتروني [البريد الإلكتروني محمي].

المصدر: www.habr.com