Содержание
Эта статья открывает серию из трёх материалов, посвящённых работе с поисковой системой хранения данных 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.
-
Как работает токенизатор текста.
-
Как сделать ранжирование, группировку и поиск по заданным условиям.
-
Сравним скорость поиска по полям-атрибутам с быстрым поиском и без.
-
И многое другое.
ссылка на оригинал статьи https://habr.com/ru/articles/827460/
Добавить комментарий