Содержание
Эта статья открывает серию из трёх материалов, посвящённых работе с поисковой системой хранения данных Vespa.
Из этой статьи вы узнаете:
Как запустить сервер конфигурации Vespa в Docker.
Как настроить конфигурацию Vespa.
Как выглядит структура схемы данных.
Как выполнить фильтрацию полей в результатах поиска.
Как отключить валидацию схемы данных и файла конфигурации для локальной отладки.
В следующих частях мы обсудим, как устроен поиск, ранжирование и группировка в Vespa, а также сравним скорость выполнения CRUD-операций в ElasticSearch и Vespa.
Поисковые системы
На данный момент существует множество различных поисковых систем, которые используются для хранения и поиска данных с релевантной выдачей. Согласно рейтингу компании solid IT, лидирующее положение занимает ElasticSearch, который значительно опережает ближайших конкурентов.
Полный список поисковых систем и информация о расчете рейтинга доступны по ссылке.
Если познакомиться с ElasticSearch ближе, становится очевидно, почему эта система так популярна:
Открытый исходный код.
Мощное API — готовые клиенты доступны на множестве популярных языков программирования, в том числе на Java и Python.
Хорошая горизонтальная масштабируемость.
Гибкая модель данных, которая позволяет не использовать схемы и обрабатывать документы с различными полями и структурами. Хотя возможность создания схем (маппинга) остается.
Поиск данных осуществляется при помощи широко используемого формата — JSON.
Репликация данных позволяет повысить отказоустойчивость систем.
Но у ElasticSearch есть один существенный недостаток: все документы являются неизменными, и при попытке частичного обновления происходит полная переиндексация всего документа. Это существенно замедляет загрузку большого количества данных.
Одной из альтернатив ElasticSearch является Vespa. Vespa — это система для высоконагруженных систем полнотекстового поиска с фильтрацией и ранжированием исходного результата. Vespa использует строгую структурированную модель данных, которая описывается в схеме.
Vespa устраняет основной недостаток ElasticSearch, обеспечивая возможность обработки больших данных с низкой задержкой и частичного обновления документов.
Стоит уточнить, что Vespa — это система компромиссов, и у нее есть ряд недостатков:
К сожалению, на момент написания статьи Vespa не имеет полноценного поискового клиента на современных языках, таких как Java и Python. Разработчики Vespa рекомендуют использовать любые доступные HTTP-клиенты для поиска.
Сложное двухэтапное развёртывание.
Из-за того, что документация недостаточно полная, а накопленных знаний о практических аспектах работы не так много, возникают трудности при попытке разобраться в деталях работы Vespa (эта статья частично это исправит). Например, найти информацию про работу ключевого слова from disk — это целый квест.
Схема данных хранится в специальном формате .sd (schema definition).
Тестовый проект
Для тестового проекта мы возьмём за основу базу данных, содержащую информацию о товарах в магазине обуви. На первом этапе в базе данных будут представлены две категории обуви: кроссовки и ботинки. Также будет присутствовать сущность, которая отражает информацию о доступности каждой из этих категорий в каждом магазине.
Характеристики товаров были выбраны максимально простые и понятные для читателей статьи.
Опишем наши продукты на схеме данных:
Vespa CLI
Перед началом работы с Vespa необходимо установить Vespa CLI. Ниже актуальная ссылка на релизную версию:
Для работы с Windows нужно скачать архив и добавить переменную среды на папку с исполняемым файлом. Если всё сделано правильно, в командной строке появится возможность использовать команду «vespa [cmd]».
Демопроект
По ссылке ниже можно склонировать демопроект хранилища продуктов:
Vespa в Docker
Чтобы развернуть Vespa в Docker, достаточно загрузить образ с конфигурационным сервером Vespa. Пример настройки можно найти в файле docker-compose:
docker-compose.yml
version: "3.8"
name: vespa
services:
vespa:
container_name: vespa
image: vespaengine/vespa
ports:
- "8080:8080"
- "19071:19071"
Настройка Vespa
У Vespa есть несколько способов развернуть настройки, но, по моему мнению, самый удобный способ — использование maven-плагина. Для этого создаем пустой проект. Затем добавляем в него плагин для сборки и библиотеку для работы с клиентом Java:
vespa-config/pom.xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>ru.sportmaster</groupId>
<artifactId>vespa</artifactId>
<version>1.0-SNAPSHOT</version>
</parent>
<artifactId>vespa-config</artifactId>
<!-- Специальная упаковка пакета для Vespa -->
<packaging>container-plugin</packaging>
<dependencies>
<!-- Библиотека для работы с Vespa -->
<dependency>
<groupId>com.yahoo.vespa</groupId>
<artifactId>container</artifactId>
<version>${vespa.version}</version>
<scope>provided</scope>
</dependency>
</dependencies>
<build>
<plugins>
<!-- Плагин используется для сборки и упаковки компонентов Vespa в контейнер -->
<plugin>
<groupId>com.yahoo.vespa</groupId>
<artifactId>bundle-plugin</artifactId>
<version>${vespa.version}</version>
<extensions>true</extensions>
<configuration>
<!-- В случае наличия предупреждений, будет ошибка сборки -->
<failOnWarnings>true</failOnWarnings>
</configuration>
</plugin>
<!-- Архивирует компоненты Vespa -->
<plugin>
<groupId>com.yahoo.vespa</groupId>
<artifactId>vespa-application-maven-plugin</artifactId>
<version>${vespa.version}</version>
<executions>
<execution>
<goals>
<goal>packageApplication</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>
</project>
Конфигурирование сервера Vespa происходит при помощи файла services.xml. Пример простой конфигурации для пользовательского документа:
src/main/application/services.xml
<?xml version="1.0" encoding="utf-8" ?>
<services version="1.0">
<container version="1.0" id="default">
<!-- Включает поисковую часть контейнера, без него не будет работать поиск -->
<search/>
<!-- Включает API для работы с документами -->
<document-api/>
</container>
<!-- Создает кластер содержимого который хранит и индексирует документы -->
<content id="product" version="1.0">
<!-- Определяет какие типы документов должны быть направлены в этот кластер -->
<documents>
<document mode="index" type="sneakers" />
<document mode="index" type="boots" />
</documents>
<!-- Количество реплик документа в кластере -->
<redundancy>2</redundancy>
<!-- Определяет набор узлов в кластере -->
<nodes>
<!-- distribution-key - идентификатор узла для алгоритма распределения данных -->
<node hostalias="node-1" distribution-key="0"/>
<node hostalias="node-2" distribution-key="1"/>
<node hostalias="node-3" distribution-key="2"/>
</nodes>
</content>
</services>
Данная конфигурация создаст 3 узла внутри кластера. Важно отметить, что свойство redundancy определяет количество копий данных, хранимых на каждом узле. В данном случае документ и его копия будут храниться на 2 из 3 доступных узлов.
В атрибуте document.mode указывается режим хранения данных, всего доступно три варианта:
index — режим индексирования, документ будет доступен для поиска.
store-only — режим обычного хранения, поиск и индексация недоступны.
streaming — потоковый режим позволяет искать информацию по необработанным данным, если поиск осуществляется на небольшом объеме информации. В этом случае индексация может быть не очень эффективной, так как требует дополнительных ресурсов для создания и поддержки индексов. Потоковый режим позволяет найти информацию быстро и без необходимости создания индексов.
Атрибут document.type должен указывать на документ из схемы данных (см. ниже).
Схема данных
Как я уже упомянул ранее, схема данных хранится в специальном формате .sd.
Для примера возьмем схему данных абстрактного документа, который описывает общие свойства обуви:
src/main/application/schemas/shoes.sd
# Схема обуви
schema shoes inherits product {
# Документ описывающий общие поля для любой обуви
document shoes inherits product {
# Сезон
field season type string {
indexing: summary | index
}
# Материал изготовления
field material type map<string, int> {
indexing: summary
struct-field key {
indexing: attribute
}
struct-field value {
indexing: attribute
}
}
# Пол
field gender type string {
indexing: summary | index
}
}
}
Стоит заметить, что мы не можем сохранить этот абстрактный документ в Vespa, так как он не указан в content.documents конфигурации сервера — services.xml.
При помощи ключевого слова inherits мы наследуем базовые поля документа product.
Поля в схеме поддерживают такие типы данных, как:
bool, byte, double, float, int, long — простые типы. Не поддерживают индексацию.
array<type> — массив простых типов или структура данных. Каждый элемент этого типа индексируется отдельно.
map<key, value> — ассоциативный массив. Не поддерживает индексацию.
position — координаты по широте и долготе. Не поддерживает индексацию.
predicate — поле с набором логических ограничений. Индексируется в бинарном формате.
raw — двоичные данные. Не поддерживает индексацию.
reference<document-type> — поле с ссылкой на глобальный документ. Запрещает индексацию на уровне развертывания приложения.
annotationreference<annotation-type> — поле с ссылкой на аннотацию.
string — строка. Индексируется.
struct — типом поля может быть любая структура данных. Не поддерживает индексацию.
tensor — поле типа тензор. Индексируется.
uri — поле для сопоставления с URL. Поддерживает индексацию с разбором адреса на составляющие.
weightedset<element-type> — поле, где каждому значению добавляется вес. Индексируется.
Отдельно стоит обсудить свойство indexing, которое задает тип индексирования. Всего на выбор три варианта:
index — для неструктурированного текста. Он создает текстовый индекс и сохраняет в него разобранную строку — токены. Это позволяет осуществлять поиск по токенам. По умолчанию имя индекса совпадает с именем поля.
attribute — для структурированных данных. Делает поле доступным для сортировки, группировки и ранжирования. Позволяет осуществлять поиск по полному совпадению.
summary — добавляет поле в сводку документов (см. ниже).
set_language — возможность задать язык для строкового анализатора. По умолчанию для токенизации используется OpenNLP, который поддерживает несколько языков: английский, немецкий, французский, испанский и итальянский. Однако есть возможность использовать Lucene Linguistics. Более подробно обсудим во второй статье, посвященной поиску.
Самое интересное, что эти типы можно объединить с помощью символа |. Если задать сразу все: summary | index | attribute, то будет использован тип index.
Сводка документа
Поговорим немного о сводке документов. По сути, это просто информация о том, какие документы в каком виде должны быть представлены в результате поиска. По умолчанию доступна сводка default, которую можно включить при помощи параметра HTTP-запроса presentation.summary. В ней будут отображаться все поля, у которых в типах индексирования присутствует summary. Но также возможно добавить описание собственных сводок в схеме данных:
src/main/application/schemas/sneakers.sd
# Сводка документов, для использования нужно добавить к запросу - "presentation.summary": "demo-summary"
document-summary demo-summary {
# Переименовывает поле pavement в pavement_demo_rename
summary pavement_demo_rename {
source: pavement
}
from-disk
}
Таким образом, можно, например, изменить результирующую информацию поиска кроссовок:
POST /search/ HTTP/1.1
Host: localhost:8080
Content-Type: application/json
Content-Length: 97
{
"yql": "select * from sneakers where true",
"presentation.summary": "demo-summary"
}
В ответе будут отображены только поле с псевдонимом pavement_demo_rename, остальные поля будут скрыты:
{
"root": {
"id": "toplevel",
"relevance": 1.0,
"fields": {
"totalCount": 1
},
"coverage": {
"coverage": 100,
"documents": 1,
"full": true,
"nodes": 3,
"results": 1,
"resultsFull": 1
},
"children": [
{
"id": "index:product/2/c4ca42387a14dc6e295d3d9d",
"relevance": 0.0,
"source": "product",
"fields": {
"sddocname": "sneakers",
"pavement_demo_rename": "ASPHALT"
}
}
]
}
}
Валидация настроек при развертывании
Допустим, мы описали конфигурацию сервера и схемы данных продуктов. Затем мы добавили несколько продуктов, и в одной из схем нам потребовалось изменить тип поля:
src/main/application/schemas/boots.sd
Было:
field moisture type bool {
indexing: summary | attribute
}
Стало:
field moisture type string {
indexing: summary | index
}
В таком случае Vespa не сможет корректно применить настройки, так как мы изменили не только тип элемента, но и тип индексации. Vespa не знает, как обработать уже сохраненные и проиндексированные элементы. Это может повлиять на целостность данных, по этой причине по умолчанию Vespa выдаст ошибку:
Error: invalid application package (400 Bad Request)
Invalid application:
indexing-change:
Document type 'boots':
Field 'moisture' changed:
add index aspect, matching:
'word' -> 'text', stemming:
'none' -> 'best', normalizing:
'LOWERCASE' -> 'ACCENT', summary field 'moisture' transform:
'attribute' -> 'none'
Однако для локальной работы это может быть не так важно, так как мы не всегда нуждаемся в целостности данных для отладки. Поэтому мы можем отключить некоторые правила, создав специальный файл, который переопределит валидацию:
src/main/application/validation-overrides.xml
<validation-overrides>
<!-- Изменение типа индексирования полей в схеме данных -->
<allow until="2024-03-07" comment="Для локальной работы">indexing-change</allow>
<!-- Изменение режима индексирования (services.xml) -->
<allow until="2024-03-07" comment="Для локальной работы">indexing-mode-change</allow>
<!-- Изменение типа данных в схеме данных -->
<allow until="2024-03-07" comment="Для локальной работы">field-type-change</allow>
<!-- Изменение типа тензора -->
<allow until="2024-03-07" comment="Для локальной работы">tensor-type-change</allow>
<!-- Значительное уменьшение (>50%) ресурсов узла -->
<allow until="2024-03-07" comment="Для локальной работы">resources-reduction</allow>
<!-- Удаление кластера данных или изменение его идентификатора (services.xml) -->
<allow until="2024-03-07" comment="Для локальной работы">content-cluster-removal</allow>
<!-- Изменение глобального атрибута в кластере данных -->
<allow until="2024-03-07" comment="Для локальной работы">global-document-change</allow>
<!-- Изменение глобальной точки входа -->
<allow until="2024-03-07" comment="Для локальной работы">global-endpoint-change</allow>
<!-- Увеличение избыточности данных -->
<allow until="2024-03-07" comment="Для локальной работы">redundancy-increase</allow>
<!-- Избыточность, равная одному, недопустима -->
<allow until="2024-03-07" comment="Для локальной работы">redundancy-one</allow>
<!-- Удаление сертификата -->
<allow until="2024-03-07" comment="Для локальной работы">certificate-removal</allow>
</validation-overrides>
Атрибут allow.until обозначает последний день, когда действует данное правило. Максимальный срок действия правила составляет 30 дней.
Заключение
На этом настройка и сборка самого простого сервера с Vespa завершены. С текущими настройками мы можем использовать Vespa для хранения и чтения документов.
В следующей статье мы обсудим:
Чем отличаются и какие задачи выполняют DocumentProcessor и QueryProcessor.
Как работает токенизатор текста.
Как сделать ранжирование, группировку и поиск по заданным условиям.
Сравним скорость поиска по полям-атрибутам с быстрым поиском и без.
И многое другое.
Комментарии (2)
Andrey_Solomatin
20.07.2024 18:44Потоковый режим позволяет найти информацию быстро и без необходимости создания индексов.
Интересно как это реализовано.
Andrey_Solomatin
Существенный недостаток эластик другой. Лицензия. Многие переходят на его форк open search