Пошук за великим документами в ElasticSearch


Продовжуємо цикл статей про те, як ми осягали ES в процесі створення Ambar. Перша стаття циклу була про Хайлайтинге великих текстових полів ElasticSearch.
У цій статті ми розповімо про те, як змусити ES працювати швидко з документами більш 100 Мб. Пошук в таких документах при підході "в лоб" займає десятки секунд. У нас вийшло зменшити цей час до 6 мс.
Зацікавлених просимо під кат.
Проблема пошуку з великим документів
Як відомо, все дійство пошуку ES будується навколо поля
_source
— вихідного документа, що прийшов у ES і потім проіндексованого Lucene.
Згадаймо приклад документа, який ми зберігаємо в ES:
{
sha256: "1a4ad2c5469090928a318a4d9e4f3b21cf1451c7fdc602480e48678282ced02c",
meta: [
{
id: "21264f64460498d2d3a7ab4e1d8550e4b58c0469744005cd226d431d7a5828d0",
short_name: "quarter.pdf",
full_name: "//winserver/store/reports/quarter.pdf",
source_id: "crReports",
extension: ".pdf",
created_datetime: "2017-01-14 14:49:36.788",
updated_datetime: "2017-01-14 14:49:37.140",
extra: [],
indexed_datetime: "2017-01-16 18:32:03.712"
}
],
content: {
size: 112387192,
indexed_datetime: "2017-01-16 18:32:33.321",
author: "John Smith",
processed_datetime: "2017-01-16 18:32:33.321",
length: "",
language: "",
state: "processed",
title: "Quarter Report (Q4Y2016)",
type: "application/pdf",
text: ".... дуже багато тексту тут ...."
}
}

_source
для Lucene це атомарна одиниця, яка за замовчуванням містить у собі всі поля документа. Індекс Lucene являє собою послідовність токенів з усіх полів всіх документів.
Отже, індекс містить
N
документів. Документ містить близько двох десятків полів, при цьому всі поля досить короткі, в основному типів
keyword
та
date
, за винятком довгого текстового поля
content.text
.
Тепер спробуємо в першому наближенні зрозуміти, що буде відбуватися, коли ви спробуєте виконати пошук по якому-небудь з полів у наведених вище документах. Наприклад, ми хочемо знайти документи з датою створення більше 14 січня 2017 року. Для цього виконаємо наступний запит:
curl -X POST -H "Content-Type: application/json" -d '{ range: { 'meta.created_datetime': { gt: '2017-01-14 00:00:00.000' } } }' "http://ambar:9200/ambar_file_data/_search"

Результат цього запиту ви побачите дуже нескоро, з кількох причин:
По-перше, в пошуку будуть брати участь всі поля всіх документів, хоча здавалося б, навіщо вони нам потрібні, якщо ми робимо фільтрацію тільки за датою створення. Це відбувається, т. к. атомарна одиниця для Lucene це
_source
, а індекс за замовчуванням складається з послідовності слів з всі полів документів.
По-друге, ES в процесі формування результатів пошуку вивантажить в пам'ять з індексу всі документи цілком з величезним і не потрібне нам
content.text
.
В-третіх, ES зібравши ці величезні документи в пам'яті буде намагатися надіслати їх нам єдиним відповіддю.
Добре, третю причину легко вирішити включивши
source filtering
в запит. Як бути з рештою?
Прискорюємо пошук
Очевидно, що пошук, вивантаження в пам'ять і серіалізація результатів з участю більшої поля
content.text
— це погана ідея. Щоб уникнути цього необхідно змусити Lucene окремо зберігати і обробляти великі поля окремо від інших полів документів. Опишемо необхідні для цього кроки.
По-перше, в маппинге для великого поля слід вказати параметр
store: true
. Так ви скажете Lucene що зберігати це поле необхідно окремо від
_source
, тобто від решти документа. При цьому важливо розуміти, що на рівні логіки, з
_source
це поле не виключиться! Просто Lucene при зверненні до документа буде збирати його в два прийоми: беремо
_source
і додаємо до нього збережене поле
content.text
.
По-друге, треба вказати Lucene що "важке" поле більше немає необхідності включати в
_source
. Таким чином при пошуку ми більше не будемо вивантажувати великі 100 Мб документи в пам'ять. Для цього в маппінг треба додати наступні рядки:
_source: {
excludes: [
"content.text"
]
}

Отже, що ми отримуємо в підсумку: при додаванні документа індекс,
_source
індексується без "важкого" поля
content.text
. Вона індексується окремо. В пошуку по будь-якому "легкому" полю,
content.text
ніякої участі не приймає, відповідно Lucene при цьому запиті працює з обрізаними документами, розміром не 100Мб, а пару сотень байт і пошук відбувається дуже швидко. Пошук по "важкого" поля можливий і ефективний, тепер він проводиться по масиву полів одного типу. Пошук одночасно "важкого" і "легкому" полям одного документа також можливий і ефективний. Він робиться в три етапи:
  • легкий пошук по обрізаним документами (
    _source
    )
  • пошук у масиві "важких полів" (
    content.text
    )
  • швидкий merge результатів без повернення всього поля
    content.text
Для оцінки швидкості роботи будемо шукати фразу "Іванов Іван" у полі
content.text
з фільтрацією по полю
content.size
в індексі з документів розміром більше 100 Мб. Приклад запиту наведено нижче:
curl -X POST -H "Content-Type: application/json" -d '{
"з": 0,
"size": 10,
"query": {
"bool": {
"must": [
{ "range": { "content.size": { "gte": 100000000 } } },
{ "match_phrase": { "content.text": "іванов іван"} }
]
}
}
}' "http://ambar:9200/ambar_file_data/_search"

Наш тестовий індекс містить близько 3.5 млн документів. Все це працює на одній машині невеликої потужності (16Гб RAM, звичайне сховище на RAID 10 з SATA дисків). Результати наступні:
  • Базовий маппінг "в лоб" — 6.8 секунд
  • Наш варіант — 6 мс
Отже, виграш в продуктивності приблизно в 1 100 разів. Погодьтеся, заради такого результат варто витратити кілька вечорів на дослідження роботи Lucene і ElasticSearch, і ще кілька днів на написання цієї статті. Але є у нашого підходу і один підводний камінь.
Побічні ефекти
У разі, якщо ви зберігаєте яке-небудь поле окремо і виключаєте його з
_source
на вас чекає один досить неприємний підводний камінь, про який зовсім немає інформації у відкритому доступі або в мануалах ES.
Проблема наступна: ви не можете частково оновити поле документа
_source
з допомогою
update scipt
не втративши окремо збережене полі! Якщо ви, приміром, скриптом додасте в масив
meta
новий об'єкт, то ES буде змушений переіндексувати весь документ (що природно), однак при цьому окремо збережене поле
content.text
буде втрачено. На виході ви отримаєте оновлений документ, але в
stored_fields
у нього не буде нічого, крім
_source
. Таким чином, якщо вам необхідно оновлювати будь-яке з полів
_source
— вам доведеться разом з ним переписувати і збережене поле.
Підсумок
Для нас це другий використання ES у великому проекті, і знову ми змогли вирішити всі наші завдання зберігши швидкість і ефективність пошуку. ES дійсно дуже хороший, потрібно лише бути терплячим і вміти його правильно налаштувати.
Джерело: Хабрахабр

0 коментарів

Тільки зареєстровані та авторизовані користувачі можуть залишати коментарі.