Вступление

Этот пост основан на предыдущем приложении для поиска по блогам и расширяет базовую поисковую систему, включая модели машинного обучения, которые помогают нам рекомендовать сообщения в блогах пользователям, которые попадают в наше приложение. Предположим, что как только пользователь прибывает, мы получаем его идентификационный номер пользователя, обозначенный здесь user_id, и что мы отправим эту информацию в Vespa и ожидаем получить список рекомендаций в блогах, содержащий 100 сообщений в блогах, адаптированных для этого конкретного пользователя.

Предпосылки:

Совместная фильтрация

Мы начнем нашу систему рекомендаций с реализации алгоритма совместной фильтрации для неявной обратной связи, описанного в (Hu et al. 2008). Данные считаются неявными, потому что пользователи явно не оценивали каждое прочитанное сообщение в блоге. Вместо этого они имели «понравившиеся» сообщения в блоге, которые им, вероятно, понравились (положительный отзыв), но не имели возможности «не понравиться» сообщениям в блоге, которые им не нравились (отсутствие отрицательных отзывов). Из-за этого неявная обратная связь считается по своей сути шумной, и тот факт, что пользователю «не понравилось» сообщение в блоге, может иметь множество различных причин, не связанных с его негативными чувствами по поводу этого сообщения в блоге.

С точки зрения моделирования, большая разница между наборами данных явной и неявной обратной связи заключается в том, что рейтинги явной обратной связи обычно неизвестны для большинства пар пользователь-элемент и рассматриваются как пропущенные значения и игнорируются алгоритмом обучения. Для неявного набора данных мы предполагаем, что рейтинг равен нулю, если пользователю не понравилось сообщение в блоге. Чтобы закодировать тот факт, что нулевое значение могло быть получено по разным причинам, мы будем использовать концепцию уверенности, введенную (Hu et. Al. 2008), которая заставляет положительную обратную связь иметь больший вес, чем отрицательная обратная связь.

После обучения модели совместной фильтрации у нас будет один вектор, представляющий латентный фактор для каждого пользователя и элемента, содержащегося в обучающем наборе. Эти векторы позже будут использоваться в структуре ранжирования Vespa, чтобы давать рекомендации пользователю на основе скалярного произведения между пользователем и скрытыми факторами документации. Очевидная проблема этого подхода заключается в том, что для новых пользователей и новых документов не будут доступны эти скрытые факторы. Это то, что называется проблемой холодного старта, и будет решаться с помощью методов, основанных на содержании, описанных в следующих публикациях.

Метрики оценки

Метрикой оценки, использованной Kaggle для этой задачи, была средняя точность на уровне 100 (MAP @ 100). Однако, поскольку у нас нет информации о том, какие сообщения в блогах не понравились пользователям (то есть у нас есть только положительные отзывы), и наша неспособность определить поведение пользователей в соответствии с нашими рекомендациями (это автономная оценка, отличная от обычной A / B-тестирование, проводимое компаниями, использующими системы рекомендаций), мы предлагаем замечание, аналогичное тому, которое включено в (Hu et. Al. 2008), и предпочитаем меры, ориентированные на отзыв. Следуя (Hu et al. 2008), мы будем использовать ожидаемый процентильный рейтинг.

Система оценки

Создание наборов для обучения и тестирования

Чтобы оценить выигрыш, полученный системой рекомендаций, когда мы начнем улучшать ее с помощью более точных алгоритмов, мы разделим набор данных, который у нас есть, на наборы для обучения и тестирования. Обучающий набор будет содержать пары документов (сообщение в блоге) и действий пользователя (лайков), а также любую доступную информацию о документах, содержащихся в обучающем наборе. Дополнительной информации о пользователях, кроме понравившихся им сообщений в блогах, нет. Набор тестов будет состоять из ряда документов, которые можно рекомендовать, и набора пользователей, которым мы должны дать рекомендации. Этот список документов набора тестов составляет пул контента Vespa, который представляет собой набор документов, хранящихся в Vespa и доступных для обслуживания пользователей. Действия пользователя будут скрыты из набора тестов и использованы позже для оценки рекомендаций Vespa.

Чтобы создать приложение, которое больше напоминает проблемы, с которыми компании сталкиваются при построении своих систем рекомендаций, мы решили сконструировать обучающие и тестовые наборы таким образом, чтобы:

  • Будут появляться сообщения в блоге, которые понравились в учебном наборе некоторым пользователям и которые также понравились в тестовом наборе другим набором пользователей, даже если эта информация будет скрыта в тестовом наборе. Эти случаи интересно оценить, насколько хорошо работает компонент системы эксплуатации (в отличие от исследования). То есть, если мы сможем идентифицировать высококачественные сообщения в блогах на основе доступной информации во время обучения и использовать эти знания, рекомендуя эти высококачественные сообщения в блогах другой группе пользователей, которым они также могут понравиться.
  • В тестовом наборе будут сообщения блога, которых никогда не было в обучающем наборе. Эти случаи интересны для того, чтобы оценить, как система справляется с проблемой холодного запуска. Системы, которые слишком склонны к эксплуатации, не смогут рекомендовать новые и неизученные сообщения в блогах, что приведет к возникновению цикла обратной связи, который заставит систему сосредоточиться на небольшой части доступного контента.

Ключевая проблема, с которой сталкиваются разработчики рекомендательных систем, заключается в том, как сбалансировать компоненты эксплуатации / исследования в своей системе, и наш разделенный набор обучающих / тестовых наборов, описанный выше, попытается воспроизвести эту задачу в нашем приложении. Обратите внимание, что это разделение отличается от подхода, принятого в соревновании Kaggle, где сообщения блога, доступные в тестовом наборе, никогда не были замечены в обучающем наборе, что исключает использование компонента уравнения.

Задание Spark использует trainPosts.json и создает папки blog-job/training_set_ids и blog-job/test_set_ids, содержащие файлы с post_id и user_idpairs:

$ cd blog-recommendation; export SPARK_LOCAL_IP="127.0.0.1"
$ spark-submit --class "com.yahoo.example.blog.BlogRecommendationApp" \
  --master local[4] ../blog-tutorial-shared/target/scala-*/blog-support*.jar \
  --task split_set --input_file ../trainPosts.json \
  --test_perc_stage1 0.05 --test_perc_stage2 0.20 --seed 123 \
  --output_path blog-job/training_and_test_indices
  • test_perc_stage1: процент сообщений блога, которые будут размещены только в тестовом наборе (компонент исследования).
  • test_perc_stage2: процент оставшихся пар (post_id, user_id), которые должны быть перемещены в набор тестов (компонент эксплуатации).
  • seed: начальное значение, используемое для воспроизведения результатов при необходимости.

Вычислить скрытые факторы пользователя и элемента

Используйте полный обучающий набор для вычисления скрытых факторов пользователя и элемента. Мы оставим обсуждение настройки и повышения производительности используемой модели в разделе о настройке модели и автономной оценке. Отправьте Spark job для вычисления скрытых факторов пользователя и элемента:

$ spark-submit --class "com.yahoo.example.blog.BlogRecommendationApp" \
  --master local[4] ../blog-tutorial-shared/target/scala-*/blog-support*.jar \
  --task collaborative_filtering \
  --input_file blog-job/training_and_test_indices/training_set_ids \
  --rank 10 --numIterations 10 --lambda 0.01 \
  --output_path blog-job/user_item_cf

Проверьте векторы для скрытых факторов для пользователей и сообщений:

$ head -1 blog-job/user_item_cf/user_features/part-00000 | python -m json.tool
{
    "user_id": 270,
    "user_item_cf": {
        "user_item_cf:0": -1.750116e-05,
        "user_item_cf:1": 9.730623e-05,
        "user_item_cf:2": 8.515047e-05,
        "user_item_cf:3": 6.9297894e-05,
        "user_item_cf:4": 7.343942e-05,
        "user_item_cf:5": -0.00017635927,
        "user_item_cf:6": 5.7642872e-05,
        "user_item_cf:7": -6.6685796e-05,
        "user_item_cf:8": 8.5506894e-05,
        "user_item_cf:9": -1.7209566e-05
    }
}
$ head -1 blog-job/user_item_cf/product_features/part-00000 | python -m json.tool
{
    "post_id": 20,
    "user_item_cf": {
        "user_item_cf:0": 0.0019320602,
        "user_item_cf:1": -0.004728486,
        "user_item_cf:2": 0.0032499845,
        "user_item_cf:3": -0.006453364,
        "user_item_cf:4": 0.0015929453,
        "user_item_cf:5": -0.00420313,
        "user_item_cf:6": 0.009350027,
        "user_item_cf:7": -0.0015649397,
        "user_item_cf:8": 0.009262732,
        "user_item_cf:9": -0.0030964287
    }
}

На этом этапе векторы со скрытыми факторами могут быть добавлены к сообщениям и пользователям.

Добавление векторов в определения поиска с помощью тензоров

Современные приложения машинного обучения часто используют большие многомерные пространства функций и выполняют сложные операции с этими функциями, например, в моделях большой логистической регрессии и глубокого обучения. Следовательно, необходимо иметь выразительную основу для определения и оценки выражений ранжирования такой сложности в масштабе.

Vespa поставляется с фреймворком Tensor, который объединяет и обобщает скалярные, векторные и матричные операции, обрабатывает разреженность, присущую большинству приложений машинного обучения (в большинстве случаев, оцениваемых моделью, не хватает значений для большинства функций) и позволяет моделям быть непрерывно обновлено. Дополнительную информацию о фреймворке Tensor можно найти в руководстве пользователя тензор.

Мы хотим, чтобы эти скрытые факторы были доступны в тензорном представлении, чтобы их можно было использовать во время ранжирования с помощью тензорного фреймворка. В blog_post.sd добавлено тензорное поле user_item_cf для хранения скрытого фактора сообщения в блоге:

field user_item_cf type tensor(user_item_cf[10]) {
	indexing: summary | attribute
	attribute: tensor(user_item_cf[10])
}
field has_user_item_cf type byte {
	indexing: summary | attribute
	attribute: fast-search
}

Новое определение поиска user.sd определяет тип документа с именем user для хранения информации для пользователей:

search user {
    document user {
        field user_id type string {
            indexing: summary | attribute
            attribute: fast-search
        }
        field has_read_items type array<string> {
            indexing: summary | attribute
        }
        field user_item_cf type tensor(user_item_cf[10]) {
            indexing: summary | attribute
            attribute: tensor(user_item_cf[10])
        }
        field has_user_item_cf type byte {
            indexing: summary | attribute
            attribute: fast-search
        }
    }
}

Где:

  • user_id: уникальный идентификатор пользователя
  • user_item_cf: тензор, который будет содержать скрытый фактор пользователя
  • has_user_item_cf: флаг, указывающий на то, что у пользователя есть скрытый фактор

Присоединяйтесь и передавайте данные

Соберите и разверните приложение:

$ mvn install

Разверните приложение (в контейнере Docker):

$ vespa-deploy prepare /vespa-sample-apps/blog-recommendation/target/application && \
  vespa-deploy activate

Подождите, пока приложение активируется (200 OK):

$ curl -s --head http://localhost:8080/ApplicationStatus

Код для объединения скрытых факторов в blog-job/user_item_cf в blog_post и пользовательские документы реализован в tutorial_feed_content_and_tensor_vespa.pig. После присоединения к новым полям создается поток Vespa, который подается на Vespa прямо из Pig:

$ pig -Dvespa.feed.defaultport=8080 -Dvespa.feed.random.startup.sleep.ms=0 \
  -x local \
  -f ../blog-tutorial-shared/src/main/pig/tutorial_feed_content_and_tensor_vespa.pig \
  -param VESPA_HADOOP_JAR=../vespa-hadoop*.jar \
  -param DATA_PATH=../trainPosts.json \
  -param TEST_INDICES=blog-job/training_and_test_indices/testing_set_ids \
  -param BLOG_POST_FACTORS=blog-job/user_item_cf/product_features \
  -param USER_FACTORS=blog-job/user_item_cf/user_features \
  -param ENDPOINT=localhost

При успешном соединении данных и фиде будут выведены:

Input(s):
Successfully read 1196111 records from: "file:///Users/kraune/github/vespa-engine/sample-apps/trainPosts.json"
Successfully read 341416 records from: "file:///Users/kraune/github/vespa-engine/sample-apps/blog-recommendation/blog-job/training_and_test_indices/testing_set_ids"
Successfully read 323727 records from: "file:///Users/kraune/github/vespa-engine/sample-apps/blog-recommendation/blog-job/user_item_cf/product_features"
Successfully read 6290 records from: "file:///Users/kraune/github/vespa-engine/sample-apps/blog-recommendation/blog-job/user_item_cf/user_features"
Output(s):
Successfully stored 286237 records in: "localhost"

Пример сообщения в блоге и пользователя:

Рейтинг

Настройте функцию ранжирования, чтобы возвращать наиболее подходящие сообщения в блоге с учетом некоторого скрытого фактора пользователя. Ранжируйте документы с помощью скалярного произведения между скрытыми факторами пользователя и поста в блоге, т. Е. Тензором запроса и скалярным произведением тензора поста блога (сумма произведения двух тензоров) - от blog_post.sd:

rank-profile tensor {
    first-phase {
        expression {
            sum(query(user_item_cf) * attribute(user_item_cf))
        }
    }
}

Настройте структуру ранжирования на ожидание, что query(user_item_cf) является тензором и что он совместим с атрибутом в типе профиля запроса - см. search/query-profiles/types/root.xml и search/query-profiles/default.xml:

<query-profile-type id="root" inherits="native">
    <field name="ranking.features.query(user_item_cf)" type="tensor(user_item_cf[10])" />
</query-profile-type>
<query-profile id="default" type="root" />

Это настраивает функцию ранжирования с именем query(user_item_cf) с типом tensor(user_item_cf[10]), который определяет его как индексированный тензор с 10 элементами. Это то же самое, что и атрибут, поэтому можно вычислить скалярное произведение.

Запросить Vespa с тензором

Рекомендации по тестированию, отправив тензор со скрытыми факторами: localhost: 8080 / search /? Yql = select% 20 *% 20from% 20sources% 20blog_post% 20where% 20has_user_item_cf% 20 =% 201; & rank = tensor & rank.features.query (user_item_cf) =% 7B% 7Buser_item_cf% 3A0% 7D% 3A0.1% 2C% 7Buser_item_cf% 3A1% 7D% 3A0.1% 2C% 7Buser_item_cf% 3A2% 7D% 3A0.1% 2C% 7Buser_item_cfD% 3A 2C% 7Buser_item_cf% 3A4% 7D% 3A0.1% 2C% 7Buser_item_cf% 3A5% 7D% 3A0.1% 2C% 7Buser_item_cf% 3A6% 7D% 3A0.1% 2C% 7Buser_item_cf% 3A7% 3A7% 7Buser_item_cf% 3A8% 7D% 3A0.1% 2C% 7Buser_item_cf% 3A9% 7D% 3A0.1% 7D

Разложенная строка запроса:

  • yql = select * from sources blog_post, где has_user_item_cf = 1 - это выбирает все документы типа blog_post, который имеет тензор скрытого фактора
  • restrict = blog_post - искать только в blog_post документах
  • rank = tensor - используйте профиль ранга tensor в blog_post.sd.
  • rank.features.query (user_item_cf) - отправить тензор как user_item_cf. Поскольку этот тензор определен в типе профиля-запроса, структура ранжирования знает его тип (т. Е. Размеры) и может выполнять скалярное произведение с атрибутом того же типа. Тензор перед URL-кодированием:
{
  {user_item_cf:0}:0.1,
  {user_item_cf:1}:0.1,
  {user_item_cf:2}:0.1,
  {user_item_cf:3}:0.1,
  {user_item_cf:4}:0.1,
  {user_item_cf:5}:0.1,
  {user_item_cf:6}:0.1,
  {user_item_cf:7}:0.1,
  {user_item_cf:8}:0.1,
  {user_item_cf:9}:0.1
}

Запросить Vespa с идентификатором пользователя

Следующий шаг - запросить Vespa по идентификатору пользователя, найти профиль пользователя для пользователя, получить от него тензор и рекомендовать документы, основанные на этом тензоре (как и запрос в предыдущем разделе). Профили пользователей передаются в Vespa в поле user_item_cf типа документа user.

Короче говоря, настройте поисковик для получения профиля пользователя по идентификатору пользователя - затем запустите запрос. Когда Контейнер Vespa получает запрос, он создает Query, представляющий его, и выполняет настроенный список таких компонентов поисковика, называемый цепочкой поиска. Объект query содержит всю информацию, необходимую для создания результата запроса, в то время как Result инкапсулирует все данные, сгенерированные из Query. Объект Execution отслеживает состояние вызова для выполнения поисковиков цепочки поиска:

package com.yahoo.example;
import com.yahoo.data.access.Inspectable;
import com.yahoo.data.access.Inspector;
import com.yahoo.prelude.query.IntItem;
import com.yahoo.prelude.query.NotItem;
import com.yahoo.prelude.query.WordItem;
import com.yahoo.processing.request.CompoundName;
import com.yahoo.search.Query;
import com.yahoo.search.Result;
import com.yahoo.search.Searcher;
import com.yahoo.search.querytransform.QueryTreeUtil;
import com.yahoo.search.result.Hit;
import com.yahoo.search.searchchain.Execution;
import com.yahoo.search.searchchain.SearchChain;
import com.yahoo.tensor.Tensor;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
public class UserProfileSearcher extends Searcher {
    public Result search(Query query, Execution execution) {
        // Get tensor and read items from user profile
        Object userIdProperty = query.properties().get("user_id");
        if (userIdProperty != null) {
            Hit userProfile =
                retrieveUserProfile(userIdProperty.toString(),
                execution);
            if (userProfile != null) {
                addUserProfileTensorToQuery(query, userProfile);
                NotItem notItem = new NotItem();
                notItem.addItem(new IntItem(1, "has_user_item_cf"));
                for (String item :
                     getReadItems(userProfile
                                  .getField("has_read_items"))) {
                    notItem.addItem(new WordItem(item, "post_id"));
                }
                QueryTreeUtil.andQueryItemWithRoot(query, notItem);
            }
        }
        // Restric to search in blog_posts
        query.getModel().setRestrict("blog_post");
        // Rank blog posts using tensor rank profile
        if(query.properties().get("ranking") == null) {
            query.properties().set(new CompoundName("ranking"),
                                   "tensor");
        } 
        return execution.search(query);
    } 
    private Hit retrieveUserProfile(String userId, 
                                    Execution execution) {
        Query query = new Query();
        query.getModel().setRestrict("user");
        query.getModel().getQueryTree()
            .setRoot(new WordItem(userId, "user_id"));
        query.setHits(1); 
        SearchChain vespaChain = 
            execution.searchChainRegistry().getComponent("vespa");
        Result result = new Execution(vespaChain,
                                      execution.context())
                        .search(query);
        execution.fill(result); // Get the summary data
        Iterator<Hit> hiterator = result.hits().deepIterator();
        return hiterator.hasNext() ? hiterator.next() : null;
    }
    private void addUserProfileTensorToQuery(Query query,
                                             Hit userProfile) {
        Object userItemCf = userProfile.getField("user_item_cf");
        if (userItemCf != null) {
            if (userItemCf instanceof Tensor) {
                query.getRanking().getFeatures()
                    .put("query(user_item_cf)", 
                         (Tensor)userItemCf);
            }
            else {
                query.getRanking().getFeatures()
                    .put("query(user_item_cf)",
                         Tensor.from(userItemCf.toString()));
            }
        }
    }
    private List<String> getReadItems(Object readItems) {
        List<String> items = new ArrayList<>();
        if (readItems instanceof Inspectable) {
            for (Inspector entry :
                 ((Inspectable)readItems).inspect().entries()) {
                items.add(entry.asString());
            }
        }
        return items;
    }
}

Поисковик настроен в services.xml:

<chain id='user' inherits='vespa'>
    <searcher bundle='blog-recommendation'
              id='com.yahoo.example.UserProfileSearcher' />
</chain> 

Разверните, затем запросите пользователя, чтобы получить рекомендации блога: localhost: 8080 / search /? User_id = 34030991 & searchChain = user.

Чтобы уточнить рекомендации, добавьте условия запроса: localhost: 8080 / search /? User_id = 34030991 & searchChain = user & yql = select% 20 *% 20from% 20sources% 20blog_post% 20where% 20content% 20contains% 20% 22pegasus% 22;

Настройка модели и автономная оценка

Теперь мы оптимизируем скрытые факторы, используя обучающий набор, вместо того, чтобы вручную выбирать значения гиперпараметров, как это было сделано в разделе «Вычислить скрытые факторы пользователя и элемента»:

$ spark-submit --class "com.yahoo.example.blog.BlogRecommendationApp" \
  --master local[4] ../blog-tutorial-shared/target/scala-*/blog-support*.jar \
  --task collaborative_filtering_cv \
  --input_file blog-job/training_and_test_indices/training_set_ids \
  --numIterations 10 --output_path blog-job/user_item_cf_cv

Как и прежде, скормите Vespa вновь вычисленные скрытые факторы. Обратите внимание, что нам нужно обновить спецификацию тензора в определении поиска в случае изменения размера скрытых векторов. Мы использовали размер 10 (ранг = 10) в разделе «Вычислить скрытые факторы пользователя и элемента», но наш алгоритм перекрестной проверки, приведенный выше, пробует разные значения для ранга (10, 50, 100).

$ pig -Dvespa.feed.defaultport=8080 -Dvespa.feed.random.startup.sleep.ms=0 \
  -x local \
  -f ../blog-tutorial-shared/src/main/pig/tutorial_feed_content_and_tensor_vespa.pig \
  -param VESPA_HADOOP_JAR=../vespa-hadoop*.jar \
  -param DATA_PATH=../trainPosts.json \
  -param TEST_INDICES=blog-job/training_and_test_indices/testing_set_ids \
  -param BLOG_POST_FACTORS=blog-job/user_item_cf_cv/product_features \
  -param USER_FACTORS=blog-job/user_item_cf_cv/user_features \
  -param ENDPOINT=localhost

Запустите следующий скрипт, который будет использовать Java UDF VespaQuery из vespa-hadoop, чтобы запросить у Vespa определенное количество рекомендаций в блоге для каждого user_id в нашем тестовом наборе. Имея список рекомендаций для каждого пользователя, мы можем затем вычислить ожидаемый процентильный рейтинг, как описано в разделе «Метрики оценки»:

$ pig \
  -x local \
  -f ../blog-tutorial-shared/src/main/pig/tutorial_compute_metric.pig \
  -param VESPA_HADOOP_JAR=../vespa-hadoop*.jar \
  -param TEST_INDICES=blog-job/training_and_test_indices/testing_set_ids \
  -param BLOG_POST_FACTORS=blog-job/user_item_cf_cv/product_features \
  -param USER_FACTORS=blog-job/user_item_cf_cv/user_features \
  -param NUMBER_RECOMMENDATIONS=100 \
  -param RANKING_NAME=tensor \
  -param OUTPUT=blog-job/metric \
  -param ENDPOINT=localhost:8080

По завершении обратите внимание на:

Input(s):
Successfully read 341416 records from: "file:/sample-apps/blog-recommendation/blog-job/training_and_test_indices/testing_set_ids"
Output(s):
Successfully stored 5174 records in: "file:/sample-apps/blog-recommendation/blog-job/metric"

В следующем посте мы повысим точность с помощью простой нейронной сети.

Vespa и Hadoop

Vespa была разработана для сохранения производительности с низкой задержкой даже в веб-масштабе, подобном Yahoo. Это означает поддержку большого количества одновременных запросов, а также очень большого количества документов. В предыдущем руководстве мы использовали набор данных размером примерно 5 ГБ. Для наборов данных такого размера не требуется распределенная файловая система для обработки данных. Однако мы предполагаем, что большинство пользователей Vespa захотят в какой-то момент расширить свои приложения. Поэтому в этом руководстве используются такие инструменты, как Apache Hadoop, Apache Pig и Apache Spark. Их можно запускать локально на ноутбуке, как в этом руководстве. Если вы хотите использовать HDFS (распределенную файловую систему Hadoop) для хранения данных, достаточно просто загрузить их в HDFS с помощью следующей команды:

$ hadoop fs -put trainPosts.json blog-app/trainPosts.json

Если вы выберете этот подход, вам необходимо заменить пути к локальным файлам на эквивалентные пути к файлам HDFS в этом руководстве.

У Vespa есть набор инструментов для облегчения взаимодействия между Vespa и экосистемой Hadoop. Их также можно использовать локально. Пример сценария Pig для кормления Vespa очень прост:

REGISTER vespa-hadoop.jar
DEFINE VespaStorage com.yahoo.vespa.hadoop.pig.VespaStorage();
A = LOAD '<path>' [USING <storage>] [AS <schema>];
-- apply any transformations
STORE A INTO '$ENDPOINT' USING VespaStorage();

Используйте Pig, чтобы передать файл в Vespa:

$ pig -x local -f feed.pig -p ENDPOINT=endpoint-1,endpoint-2

Здесь добавлен параметр -x local, чтобы указать, что этот сценарий запускается локально и не будет пытаться получить сценарии и данные из HDFS. Для локального запуска на вашем компьютере должны быть установлены библиотеки Pig и Hadoop, но вам не нужно устанавливать и запускать работающий экземпляр Hadoop. Больше примеров кормления Vespa от Pig можно найти в примерах приложений.