Як ми робили пошук в elasticsearch на vulners.com


Як ми писали раніше, в якості основної бази для пошуку на сайті використовується elasticsearch. Пошук в elastic працює дуже швидко і з коробки доступно багато корисних функцій для роботи з даними — повнотекстовий пошук, неточний пошук, всілякі методи агрегації і тд.

І на відміну від класичних SQL баз даних або noSQL типу MongoDB тут дуже зручно робити неточний пошук по всьому документу. Для цього використовується синтаксис Query DSL. Для повнотекстового пошуку по всьому документу є кілька пошукових запитів. У себе на сайті ми використовуємо тип query_string. Цей запит підтримує Lucene синтаксис, який дозволяє і нам, і користувачеві створювати складні запити в google-style. Ось приклади таких запитів:

title:apache AND title:vulnerability
type:centos cvss.score:[8 TO 10]
Можна зробити ось такий простий запит і все:

{
"query": {
"query_string": {
"query": "pws wordpress"
}
}
}

Але почавши вперше використовувати query_string, ви зіткнетеся з тим, що пошук видає не те, що ви хочете бачити. Як же домогтися від elasticsearch виразного результату пошуку?

І тут ми вперше стикаємося з таким поняттям в elasticsearch, як релевантність, вона ж score. Дуже докладний опис є на офіційному сайтіті, я ж просто скажу, що score показує, наскільки документ відповідає вашому запиту. Кожен знайдений документ містить поле _score і результати пошуку автоматично сортуються але нього. У більшості випадків це доречно, але якщо користувач хоче відсортувати за датою? Тоді потрібно в зазначеному json-запиті передати додаткове поле sort, ось так:

{
"query": {
"query_string": {
"query": "hackapp"
}
},
"sort": "published"
}

Означає, що в post-запиті треба послати це поле окремо, змусивши користувача його кудись ввести або вибрати, наприклад, з випадаючого списку. Але чому б не зробити це прямо в пошуковому запиті? І ми приходимо до костылю №1. Ми шукаємо регуляркой в поиской рядку фразу вигляду (order|sort):\w+, її выцепляем і зазначене поле передаємо в додатковому полі в json.

Також ми підгледіли у чудових товаришів з Wallarm ось такий dork — last N days. Нам відразу сподобався, так як можна дуже швидко дивитися уразливості за останній місяць, наприклад. Як ви можете самі здогадатися, це теж можна писати прямо в пошуковому рядку. Регуляркой це выцепляется і підставляється в запит. При цьому не потрібно робити хитрих виду розрахунків для обчислення дат. Можна задати умову в такому вигляді — {«gte»: «now-3d/d»}. Також відкриваємо для себе новий тип запиту в elasticsearch — bool та filter. У підсумку після двох хаків маємо вже такий запит:

{
"query": {
"bool": {
"filter": {
"range": {
"published": {
"gte": "now-3d/d"
}
}
},
"should": {
"query_string": {
"query": "wordpress"
}
}
}
}
}

Пошук начебто працює, але ця горезвісна релевантність залишає бажати кращого. Хочеться її подтюнить. Відкриваємо для себе поняття boost. В залежності від того чи іншого критерію можемо впливати на підсумковий score. Найпростіший спосіб у випадку з пошуком через query_string — це задати поля із завданням коефіцієнта. Задається це так з допомогою вказівки додаткового параметра fields:

{
"query": {
"bool": {
"filter": {
"range": {
"published": {
"gte": "now-3d/d"
}
}
},
"should": {
"query_string": {
"query": "wordpress",
"fields": [
"title^2",
"type^3",
"affectedPackage.packageName^3",
"affectedSoftware.name^3",
"_all"
],
"default_operator": "AND"
}
}
}
}
}

При цьому, якщо завдання коефіцієнта 2 не збільшить score в 2 рази, воно зробить документ релевантні в 2 рази ).

Також попутно визначаємо параметр default_operator, щоб слова перелічені в запиті за замовчуванням шукалися з умова AND.

Пошук став кращим, але ми стикаємося з випадками, коли статті з великою кількістю згадок якоїсь теми вилазять в топ, повністю прибираючи від користувача більш важливі нові уразливості або експлоїти. Вирішено виправити це в двох напрямках. Додаємо boost на основі типу документа. При цьому ми хочемо перерахувати тільки ті типи, які повинні знижувати або підвищувати підсумковий рейтинг, тобто умова може і не виконуватися. Для цього необхідно використовувати пошук за типом bool, ставлячи умову must яке точно має виконуватися (користувальницький запит) і необов'язкова умова should, в якому ми перерахуємо необхідні типи.

{
"query": {
"bool": {
"must": [
{
"bool": {
"minimum_should_match": 1,
"should": [
{
"query_string": {
"default_operator": "AND",
"fields": [
"id^4",
"title^3",
"_all"
],
"query": "wordpress"
}
}
]
}
}
],
"should": [
{
"bool": {
"minimum_should_match": 0,
"should": [
{
"term": {
"boost": 2.5,
"type": "virus"
}
},
{
"term": {
"boost": 2,
"type": "software"
}
},
{
"term": {
"boost": 0.3,
"type": "info"
}
}
]
}
}
]
}
}
}

І крім цього додається бажання більше нові документи витягнути на початок знайденого. Тут вже лінійним множником не обійтися, тому замість звичайної query використовуємо function_score. Спочатку створену query необхідно підставити в query всередині function_score і також задати саму функцію. Ми використовуємо поле modified і розподіл Гауса. При цьому в якості початкової відміткою вважається поточна дата. Такий множник в elasticsearch можна використовувати для числових типів, дат і геопозиций, при цьому можна задати будь-яку початкову точку і в цьому величезний плюс від використання elasticsearch. Разом наш запит набуває наступний вигляд:

{
"з": 0,
"query": {
"function_score": {
"functions": [
{
"weight": 1
},
{
"гаусса": {
"modified": {
"scale": "12w"
}
}
}
],
"query": {
"bool": {
"must": [
{
"bool": {
"minimum_should_match": 1,
"should": [
{
"query_string": {
"default_operator":"AND",
"fields": [
"id^4",
"title^3",
"_all"
],
"query": "wordpress"
}
}
]
}
}
],
"should": [
{
"bool": {
"minimum_should_match": 0,
"should": [
{
"term": {
"boost": 2,
"type": "unix"
}
},
{
"term": {
"boost": 2.5,
"type": "virus"
}
},
{
"term": {
"boost": 2,
"type": "software"
}
},
{
"term": {
"boost": 2,
"type": "nvd"
}
},
{
"term": {
"boost": 0.3,
"type": "info"
}
}
]
}
}
]
}
}
}
},
"size": 20,
"sort": [
{
"_score": {
"order": "desc"
}
},
{
"published": {
"order": "desc"
}
}
]
}

Залишається заключний штрих — додаємо highlight, щоб було простіше визначити, чому був обраний саме цей документ, і на цьому наш невеликий запит готовий:

{
"з": 0,
"query": {
"function_score": {
"functions": [
{
"weight": 1
},
{
"гаусса": {
"modified": {
"scale": "12w"
}
}
}
],
"query": {
"bool": {
"must": [
{
"bool": {
"minimum_should_match": 1,
"should": [
{
"query_string": {
"default_operator": "AND",
"fields": [
"id^4",
"title^3",
"_all"
],
"query": "wordpress"
}
}
]
}
}
],
"should": [
{
"bool": {
"minimum_should_match": 0,
"should": [
{
"term": {
"boost": 2,
"type": "unix"
}
},
{
"term": {
"boost": 2.5,
"type": "virus"
}
},
{
"term": {
"boost": 2,
"type": "software"
}
},
{
"term": {
"boost": 2,
"type": "nvd"
}
},
{
"term": {
"boost": 0.3,
"type": "info"
}
}
]
}
}
]
}
}
}
},
"size": 20,
"sort": [
{
"_score": {
"order": "desc"
}
},
{
"published": {
"order": "desc"
}
}
],
"highlight": {
"fields": {
".*": {
"fragment_size": 100,
"number_of_fragments": 4,
"post_tags": [
"</span>"
],
"pre_tags": [
"<span class=\"vulners-highlight\">"
],
"require_field_match": false
}
}
}
}

Якщо підвести сухий залишок — elasticsearch простий в освоєнні для базових запитів, але якщо необхідно зробити релевантний і гнучкий пошук за документами з повністю різним змістом, то варто запастися терпінням.
Джерело: Хабрахабр

0 коментарів

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