Подтвердить что ты не робот

Работа с большим количеством данных в Firebase для системы рекомендаций

Я создаю систему рекомендаций, в которой я использую Firebase для хранения и извлечения данных о фильмах и предпочтениях пользователя.

Каждый фильм может иметь несколько атрибутов, и данные выглядят следующим образом:

{ 
    "titanic": 
    {"1997": 1, "english": 1, "dicaprio": 1,    "romance": 1, "drama": 1 }, 
    "inception": 
    { "2010": 1, "english": 1, "dicaprio": 1, "adventure": 1, "scifi": 1}
...
}

Чтобы сделать рекомендации, мой алгоритм требует ввода всех данных (фильмов) и сопоставления с профилем пользователя.

Однако в режиме производства мне нужно получить более 10000 фильмов. Хотя алгоритм может справиться с этим относительно быстро, для загрузки этих данных из Firebase требуется много времени.

Я извлекаю данные следующим образом:

firebase.database().ref(moviesRef).on('value', function(snapshot) {
    // snapshot.val();
}, function(error){
    console.log(error)
});

Мне интересно, есть ли у вас мысли о том, как ускорить процесс? Существуют ли какие-либо плагины или методы, известные для решения этой проблемы?

Я знаю, что денормализация может помочь разбить данные вверх, но проблема в том, что мне нужны ВСЕ фильмы и ВСЕ соответствующие атрибуты.

4b9b3361

Ответ 1

Мое предложение состояло в том, чтобы использовать Cloud-функции для обработки этого.

Решение 1 (в идеале)

Если вы можете рассчитывать предложения каждый час/день/неделю

Вы можете использовать Cloud Functions Cron для ежедневного/еженедельного запуска и расчета рекомендаций для каждого пользователя каждую неделю/день. Таким образом, вы можете достичь результата, более или менее сходного с тем, что Spotify делает с их еженедельными плейлистами/рекомендациями.

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

Код ваших облачных функций будет выглядеть так:

var movies, allUsers; 

exports.weekly_job = functions.pubsub.topic('weekly-tick').onPublish((event) => {
  getMoviesAndUsers();
});  

function getMoviesAndUsers () {
  firebase.database().ref(moviesRef).on('value', function(snapshot) {
    movies = snapshot.val();
    firebase.database().ref(allUsersRef).on('value', function(snapshot) {
        allUsers = snapshot.val();
        createRecommendations();
    });
});
}

function createRecommendations () {
  // do something magical with movies and allUsers here.

  // then write the recommendations to each user profiles kind of like 
  userRef.update({"userRecommendations" : {"reco1" : "Her", "reco2", "Black Mirror"}});
  // etc. 
}

Простите псевдокод. Надеюсь, это дает представление.

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

Решение 2

Если вы не можете рассчитывать предложения каждый час/день/неделю, и вы должны делать это каждый раз, когда пользователь обращается к панели рекомендаций

Затем вы можете запускать функцию облака каждый раз, когда пользователь посещает их страницу рекомендаций. Быстрое чит-решение, которое я использую для этого, - это записать значение в профиль пользователя, например: {getRecommendations:true}, один раз на pageload, а затем в облачных функциях слушать изменения в getRecommendations. Пока у вас есть такая структура:

userID > getRecommendations: true

И если у вас есть правильные правила безопасности, так что каждый пользователь может писать только на свой путь, этот метод даст вам правильный идентификатор пользователя, который также сделает запрос. Таким образом, вы будете знать, какой пользователь должен вычислять рекомендации. Облачная функция могла бы, скорее всего, вывести 10 000 записей быстрее и сохранить полосу пропускания пользователя и, наконец, написать рекомендации только для профиля пользователей. (аналогично решению 1 выше). Ваша настройка понравится:

[Фронтальный код]

//on pageload
userProfileRef.update({"getRecommendations" : true});
userRecommendationsRef.on('value', function(snapshot) {  gotUserRecos(snapshot.val());  });

[Облачные функции (базовый код)]

exports.userRequestedRecommendations = functions.database.ref('/users/{uid}/getRecommendations').onWrite(event => {
  const uid = event.params.uid;
  firebase.database().ref(moviesRef).on('value', function(snapshot) {
    movies = snapshot.val();
    firebase.database().ref(userRefFromUID).on('value', function(snapshot) {
        usersMovieTasteInformation = snapshot.val();
        // do something magical with movies and user preferences here.
        // then 
        return userRecommendationsRef.update({"getRecommendations" : {"reco1" : "Her", "reco2", "Black Mirror"}});
    });
  });
});

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

P.S 1: Я закончил использование большего количества псевдокода, чем первоначально предполагалось, и удалил обработку ошибок и т.д., надеясь, что это, как правило, имеет смысл. Если есть что-то непонятное, комментарий, и я буду рад прояснить.

P.S. 2: Я использую очень похожий поток для мини-внутреннего сервиса, который я построил для одного из моих клиентов, и он был счастлив работать более месяца.

Ответ 2

Лучшая практика Firebase NoSQL JSON - это "Избегать вложенных данных", но вы сказали, что не хотите менять свои данные. Таким образом, для вашего состояния вы можете иметь вызов REST для любого конкретного node (node вашего каждого фильма) из firebase.

Решение 1). Вы можете создать некоторое фиксированное количество потоков через ThreadPoolExecutors. Из каждого рабочего потока вы можете выполнить HTTP (запрос вызова REST), как показано ниже. На основе производительности вашего устройства и мощности памяти вы можете решить, сколько рабочих потоков вы хотите манипулировать через ThreadPoolExecutors. У вас может быть фрагмент кода, как показано ниже:

/* creates threads on demand */
    ThreadFactory threadFactory = Executors.defaultThreadFactory(); 

/* Creates a thread pool that creates new threads as needed, but will reuse previously constructed threads when they are available */

    ExecutorService threadPoolExecutor = Executors.newFixedThreadPool(10); /* you have 10 different worker threads */  

for(int i = 0; i<100; i++) { /* you can load first 100 movies */
/* you can use your 10 different threads to read first 10 movies */
threadPoolExecutor.execute(() -> {



        /* OkHttp Reqeust */
        /* urlStr can be something like "https://earthquakesenotifications.firebaseio.com/movies?print=pretty" */
                Request request = new Request.Builder().url(urlStr+"/i").build(); 

    /* Note: Firebase, by default, store index for every array. 
Since you are storing all your movies in movies JSON array, 
it would be easier, you read first (0) from the first worker thread, 
second (1) from the second worker thread and so on. */

                try {
                    Response response = new OkHttpClient().newCall(request).execute(); 
    /* OkHttpClient is HTTP client to request */
                    String str = response.body().string();
                } catch (IOException e) {
                    e.printStackTrace();
                }
                return myStr;
            });
            }
                threadPoolExecutor.shutdown();

Решение 2) Решение 1 не основано на шаблоне Listener-Observer. На самом деле, Firebase имеет технологию PUSH. Значит, всякий раз, когда что-то особенное node изменяется в Firebase NoSQL JSON, соответствующий клиент, у которого есть прослушиватель подключений для конкретного node JSON, получит новые данные через onDataChange(DataSnapshot dataSnapshot) { }. Для этого вы можете создать массив DatabaseReferences, как показано ниже:

      Iterable<DataSnapshot> databaseReferenceList = FirebaseDatabase.getInstance().getReference().getRoot().child("movies").getChildren();

for(DataSnapshot o : databaseReferenceList) { 
 @Override
            public void onDataChange(DataSnapshot o) {



      /* show your ith movie in ListView. But even you use RecyclerView, showing each Movie in your RecyclerView item is still show. */
/* so you can store movie in Movies ArrayList. When everything completes, then you can update RecyclerView */

                }

            @Override
            public void onCancelled(DatabaseError databaseError) {
            }
}

Ответ 3

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

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

| fetch  | -> |process | -> | fetch  | ...
|chunk(1)|    |chunk(1)|    |chunk(3)|

(in parallel) | fetch  | -> |process | ...
              |chunk(2)|    |chunk(2)|

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

Этот "высокий уровень" подхода вашей проблемы, вероятно, ваш лучший шанс, если выборка фильмов очень медленная, хотя для этого требуется больше работы, чем просто активировать гипотетическую кнопку "ускорить" библиотеки. Хотя это хороший подход при работе с большим объемом данных.