Содержание

Эта статья открывает серию из трёх материалов, посвящённых работе с поисковой системой хранения данных 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 доступных узлов.

Пример кластера данных с redundancy = 1
Пример кластера данных с redundancy = 1

В атрибуте 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)


  1. Andrey_Solomatin
    20.07.2024 18:44

    Существенный недостаток эластик другой. Лицензия. Многие переходят на его форк open search


  1. Andrey_Solomatin
    20.07.2024 18:44

    Потоковый режим позволяет найти информацию быстро и без необходимости создания индексов.

    Интересно как это реализовано.