在Firebase中处理大量数据以用于推荐系统

时间:2017-03-12 22:10:14

标签: firebase database-design firebase-realtime-database recommendation-engine nosql

我正在构建推荐系统,我使用Firebase存储和检索有关电影用户首选项的数据。

每部电影都可以有多个属性,数据如下所示:

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

为了提出建议,我的算法需要输入所有数据(电影)并与用户资料相匹配。

但是,在制作模式中,我需要检索超过10,000张电影。虽然算法可以相对快速地处理这个问题,但是从Firebase加载这些数据需要花费很多时间。

我按如下方式检索数据:

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

我想知道你是否对如何加快速度有任何想法?是否有任何已知的插件或技术可以解决这个问题?

我知道非规范化可以帮助分割数据,但问题是我需要所有电影和所有相应的属性。

3 个答案:

答案 0 :(得分:12)

我的建议是使用云功能来处理这个问题。

解决方案1(理想情况下)

如果您可以每小时/每天/每周计算建议

您可以使用Cloud Functions Cron每天/每周启动,并计算每个用户每周/每天的推荐值。通过这种方式,您可以获得与Spotify与其每周播放列表/推荐相似的结果。

这样做的主要优点是您的用户不必等待下载所有10,000部电影,因为这将在云功能中发生,每周日晚上,编制25个推荐列表,并保存进入用户的数据节点,您可以在用户访问其个人资料时下载该节点。

您的云功能代码如下所示:

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's profiles kind of like 
  userRef.update({"userRecommendations" : {"reco1" : "Her", "reco2", "Black Mirror"}});
  // etc. 
}

原谅伪代码。我希望这会给出一个想法。

然后在您的前端,您必须为每个用户只获得userRecommendations。这样你就可以改变带宽和带宽。从用户设备到云功能的计算。就效率而言,在不知道如何计算推荐的情况下,我无法提出任何建议。

解决方案2

如果您无法每小时/每天/每周计算建议,则每次用户访问其推荐面板时都必须执行此操作

然后,每次用户访问其推荐页面时,您都可以触发云功能。我使用的一个快速作弊解决方案是将值写入用户的配置文件,例如:{getRecommendations:true},一次在页面加载,然后在云函数中监听getRecommendations中的更改。只要你有这样的结构:

userID> getRecommendations:true

如果您有适当的安全规则,以便每个用户只能写入其路径,则此方法将为您提供正确的userID以发出请求。因此,您将知道要为哪些用户计算建议。云功能最有可能更快地提取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's preferences here.
        // then 
        return userRecommendationsRef.update({"getRecommendations" : {"reco1" : "Her", "reco2", "Black Mirror"}});
    });
  });
});

由于您的前端会在userRecommendationsRef监听更改,因此只要您的云功能完成,您的用户就会看到结果。这可能需要几秒钟,因此请考虑使用加载指示器。

P.S 1:我最终使用了比原先预期更多的伪代码,并删除了错误处理等等。希望这通常能够解决问题。如果有任何不清楚的地方,请发表评论,我很乐意澄清。

P.S。 2:我使用非常类似的流程为我的一个客户制作的迷你内部服务,现在它已经开心运行了一个多月。

答案 1 :(得分:2)

虽然您声明您的算法需要所有电影和所有属性,但这并不意味着它会立即处理它们。任何计算单元都有其限制,并且在您的算法中,您可能将数据块化为计算单元可以处理的较小部分。

话虽如此,如果你想加快速度,你可以修改你的算法来并行获取和处理数据/电影:

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

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

使用这种方法,如果处理真的比提取更快,你可以节省几乎整个处理时间(但是最后一个块)(但是你没有说过你的算法运行“相对快”,与获取所有电影相比)

你的问题的这种“高级”方法可能是你获取电影真的很慢的机会,虽然它需要更多的工作而不是简单地激活图书馆的假设“加速”按钮。虽然在处理大量数据时这是一种合理的方法。

答案 2 :(得分:2)

Firebase NoSQL JSON结构最佳做法是“避免嵌套数据”,但是你说,你不想改变你的数据。因此,根据您的情况,您可以对firebase的任何特定节点(每部电影的节点)进行REST调用。

解决方案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技术。意味着,只要Firebase NoSQL JSON中的某个特定节点发生更改,相应的客户端(具有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's 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) {
            }
}