Статьи

Поиск пути к безумию: временная шкала Facebook

Facebook определенно изменил то, как мы делаем вещи. Иногда это не всегда к лучшему, как видно по тому, как много времени человечество тратит на просмотр кошачьих видео.

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

Это также довольно сложная модель для проектирования и сборки. Недавно мне позвонил друг, которому было поручено создать временную шкалу в стиле Facebook. Как и большинство таких задач, у нас есть понятие социальной сети, где люди следуют за другими людьми. Я предполагаю, что это на самом деле не YASNP (еще один проект социальной сети), но я не проверял слишком глубоко.

Вопрос был в том, как на самом деле построить график. Первое, что попробует большинство людей, будет примерно так:

    var user = session.Load<User>(userId);

    var timelineItems = 

       session.Query<Items>()

          .Where(x=>x.Source.In(user.Following))

          .OrderByDescending(x=>x.PublishedAt)

          .Take(30)

          .ToList();

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

Это не может работать. И мне все равно, какую технологию вы используете для хранения. Вы не можете создать хорошее решение, используя запросы для чего-то вроде временной шкалы.

Угол Нитпикера:

  • Если у вас есть пользователи, которые следят за МНОЖЕСТВОМ людей (и у вас они будут), у вас могут возникнуть проблемы с запросом.
  • Чем больше элементов, тем медленнее становится этот запрос. Поскольку вам нужно отсортировать их все, прежде чем вы сможете вернуть результаты. И вы, вероятно, будете иметь их много.
  • Вы не можете по-настоящему ослепить этот запрос хорошо или легко.
  • Вы не можете применить дополнительную фильтрацию каким-либо значимым способом.

Давайте рассмотрим следующий сценарий. Допустим, я забочусь об этом человеке Рохита. Но я действительно не забочусь о Фармвилле. 

спрятать ленту фармвилля

А потом:

скрыть фармвилл

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

Вы не можете сделать это с помощью запросов. Ни в коем случае.

Вместо этого вы должны перевернуть его. Вы бы сделали что-то вроде этого:

        var user = session.Load<User>(userId);

        var timelineItmes = session.Query<TimeLineItems>() 

              .Where(x=>x.ForUser == userId)

              .OrderBy(x=>x.Date)

              .ToList();

Обратите внимание, как мы структурируем это. Существует набор объектов TimeLineItems, в которых хранится немного информации о наборе элементов. Обычно у нас есть один на пользователя в день. Что-то типа:

  • пользователи / 123 / график / 2013-03-12
  • пользователи / 123 / график / 2013-03-13
  • пользователи / 123 / график / 2013-03-14

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

Of course, that means that you have to have something that builds those timeline documents. That is usually an async process that run whenever you have a user that update something. It goes something like this:

    public void UpdateFollowers(string itemId)

    {

        var item = session.Include<Item>(x=>x.UserId)

            .Load(itemId);

     

        var user = session.Load<User>(item.UserId);

     

        // user.Followers list of documents with batches of followers

        // we assume that we might have a LOT, so we use this techinque

       // to avoid loading them all into memory at once

       // http://ayende.com/blog/96257/document-based-modeling-auctions-bids

       foreach(var followersDocId in user.Followers)

       {

           NotifyFollowers(followersDocId, item);

       }

   }

    

   public void NotifyFollowers(string followersDocId, Item item)

   {

       var followers = session.Include<FollowersCollection>(x=>x.Followers)

           .Load(followersDocId);

    

       foreach(var follower in followers.Followers)

       {

           var user = session.Load<User>(follower);

           if(user.IsMatch(item) == false)

               continue;

           AddToTimeLine(follower, item);

       }

   }

As you can see, we are batching the operation, loading the followers and batched on their settings, decide whatever to let that item to be added to their timeline or not.

Note that this has a lot of implications. Different people will see this show up in their timeline in different times (but usually very close to one another). Your memory usage is kept low, because you are only processing some of it at any given point in time. For users with a LOT of followers, and there will be some like those, you might want to build special code paths, but this should be fine even at its current stage.

What about post factum operations? Let us say that I want to start following a new user? This require special treatment, you would have to read the latest timeline items from the new user to follow and start merging that with the existing timeline. Likewise when you need to delete someone. Or adding a new filter.

It is a lot more work than just changing the query, sure. But you can get things done this way. And you cannot get anywhere with the query only approach.