КНДР - что там происходит

Одна желтая газетенка написала сегодня о подготовке ядерных ударов где-то в районе Северной Кореи. Можно было бы пропустить это, списав на привычную манеру составления заголовков. Однако, если развернуть все новости по Северной Корее в нормальном виде. То ситуация там действительно развивается стремительно.
  • В начале марта, как упоминалось ранее, там прошли совместные учения США и Южной Кореи. План учений назывался как "Отработка действий по оккупации Северной Кореи." И 21 марта эти учения завершились.
  • США заблокировали денежные операции с Северной Кореей.
  • Северная Корея разорвала перемирие по результатам войны 50-53гг. и произвела испытания ракет малой дальности.
  • В рамках учений США провели имитацию ядерной бомбардировки.
  • Операции с недвижимостью в Южной Корее упали до минимума с 2006 года, когда были проведены первые ядерные испытания.
  • После имитации бомбардировок - Ким Чен Ын распорядился готовиться к войне (в эти выходные), к нанесению ракетных ударов на все вражеские объекты захватчика США на материке, на Гавайях и на Гуаме.

Метки:   Категории:Blogs | news


Контекстный поиск с русской и английской морфологией на основе проектов с открытым исходным кодом и облачных сервисов Microsoft Azure

keywords: контекстный поиск, русская морфология, поточное индексирование, облачные сервисы
abstract: В статье дается общее представление об архитектуре поискового сервиса, построенного на проектах с открытым исходным кодом и рассматриваются базовые проблемы реализации безотказной его работы в условиях поточных данных и минимальных системных ресурсах. Реализация опирается на следующие проекты:

На скриншотах присутствует программа для работы с BlobStorage - AzureStorageExplorer, которая также доступна с открытым кодом - http://azurestorageexplorer.codeplex.com/. Сам пост написан и хостится в облаке Azure с использованием BlogEngine - http://blogengine.codeplex.com/

 

Общее

Разложим задачу создания поискового сервиса на пять составляющих: 1. Сбор данных. 2. Фильтрация, токенизация, стемминг, синонимизация данных. 3. Помещение в поисковый индекс. 4. Обработка поисковых запросов. 5. Распознавание что хотел пользователь и предоставление нативного языка запросов.

В сегодняшней статье не будут разобраны пункты 2 и 5. Так как токенизация и стемминг осуществляется при помощи библиотеки Lemmatizer.dll, разработанной сотрудниками Яндекса, и рассматривалась мною 4 года назад. За прошедшее время статью открывали более 20 тыс. раз. Сама же библиотека Lemmatizer.dll перестала быть платной и с 2011 года доступна всем желающим в исходниках. Последний пункт - создание человеческого языка, запросов требует большую наработку данных уже от пользователей производящих запросы, и может дорабатываться в процессе работы поискового сервиса. На сегодняшний момент языком запросов будет являться стандартный синтаксис Lucene, который достаточно гибкий. Так например, орфографические ошибки в запросах можно исправлять с помощью нечеткого поиска "word~",

Сбор данных

В поточном режиме при помощи Twitterizer.NET подключимся к Twitter.Streaming API. Вытаскиваем все русскоязычные твиты, фотографии instagram, видео с youtube (+Vimeo), посты на стене Вконтакте, фотографии Facebook, посты GooglePlus. Для разбора данных воспользуемся библиотеками BlogsAPI, GoogleGData, FacebookSDK соответственно. Вытаскиваем из соц.сетей все данные, которые не требуют access_token - то есть присутствия пользователя. Примеры извлечения этих данных есть в примерах самих библиотек. Также важно понимать, что большинство ссылок на информацию пропущено через сервисы сокращения ссылок. Для разворачивания таких ссылок есть простая функция в BlogsAPI для большинства известных сокращателей:

Copy Source | Copy HTML
  1. Shortener shrt = Common.GetShortenerByLink(url.ExpandedUrl) as Shortener;
  2. if (shrt != null)
  3. {
  4.         fulllink = shrt.ConvertDataTo(url.ExpandedUrl, ItemType.ShortUrl, ItemType.FullUrl);
  5. }

После извлечения - данные группируются в пакеты (Batch) и отправляются на WCF-сервис индексирования. Для сложных данных, требующих дополнительных запросов используется Azure BlobQueue(примеры работы) для отложенной индексации или, как в случае Facebook и Vkontakte, группового извлечения данных, когда в одном запросе к социальной сети запрашивается сразу несколько постов.

Так как много данных не бывает, то в тотже индекс мы добавим все посты (если быть точным - большую часть) livejournal.com, liveinternet.ru, qip.ru,juick.com, blogi.mail.ru и т.д. Отдельно отмечу, что такой интернет гигант как Яндекс не выдает информации по своим блогам wow.ya.ru и видеоблогам на video.yandex.ru через API.

Индексирование

Благодаря облачным технологиям Микрософта у нас появляется замечательный объект BlobStorage, который предоставляет до 100 ТБ под хранение индекса. Чтобы разместить в нем наш поисковый индекс формируемый Lucene, достаточно воспользоваться проектом AzureDirectory. Основной индекс будет храниться в BlobStorage, а на локальном инстансе WebRole создается специальная промежуточная директория - LocalStorage, где производятся все операции с индексом по поиску и изменению его. Важно понимать, что инстансов WebRole у нас должно быть 2 и более, так как к конечный момент времени один из них заблокировал индекс на запись (добавление объектов), а с остальных идет непрерывное чтение (поисковые команды). При этом чтение на инстансе, где идет запись - невозможно. Модель "один писатель, много читателей". При завершении записи данные с локального LocalStorage копируются в центральный BlobStorage откуда синхронизируются на остальные машины. Ниже приводится класс, который реализует связь Lucene и AzureDirectory, функции работы с Lucene не приводятся, примеры этого есть в интернете.

  1. public abstract class IndexerBase
  2. {
  3.     protected IndexerBase()
  4.     {
  5.     }
  6.     /// <summary>
  7.     /// Constructor
  8.     /// </summary>
  9.     protected IndexerBase(string sDir):base()
  10.     {
  11.         IndexDir = sDir;
  12.     }
  13.     /// <summary>
  14.     /// Объект блокировки, переопределяемый дочерними классами
  15.     /// </summary>
  16.     protected static object oWriting = new object();
  17.     /// <summary>
  18.     /// Название папки с индексом
  19.     /// </summary>
  20.     protected string IndexDir = "IndexBase";
  21.     public string cachepath
  22.     {
  23.         get
  24.         {
  25. #if DEBUG
  26.             return Path.Combine(Environment.ExpandEnvironmentVariables("%temp%"), IndexDir);
  27. #else
  28.             return Path.Combine(RoleEnvironment.GetLocalResource("MyStorage").RootPath,IndexDir);
  29. #endif
  30.         }
  31.     }
  32.     private AzureDirectory _azureDirectory;
  33.     /// <summary>
  34.     /// Директория
  35.     /// </summary>
  36.     protected AzureDirectory azureDirectory
  37.     {
  38.         get
  39.         {
  40.             if (_azureDirectory != null) return _azureDirectory;
  41.             _azureDirectory = new AzureDirectory(CloudStorageAccount.Parse(
  42.                         RoleEnvironment.GetConfigurationSettingValue("SearchConnectionString")), IndexDir,
  43.                         new SimpleFSDirectory(new DirectoryInfo(cachepath)));
  44.             return _azureDirectory;
  45.         }
  46.     }
  47.     /// <summary>
  48.     /// Указывает на первое время запуска и открытия поискового индекса
  49.     /// </summary>
  50.     protected static Boolean firsttime = true;
  51.     /// <summary>
  52.     /// Статический поток занимающийся перезаписью сегментов
  53.     /// </summary>
  54.     protected static ConcurrentMergeScheduler Merger = new ConcurrentMergeScheduler();
  55.     /// <summary>
  56.     /// То, как писать
  57.     /// </summary>
  58.     private IndexWriter _MyWriter = null;
  59.     protected IndexWriter MyWriter
  60.     {
  61.         get
  62.         {
  63.             if (_MyWriter != null) return _MyWriter;
  64.             lock (oWriting)
  65.             {
  66.                 if (_MyWriter != null) return _MyWriter;
  67.                 CreateMyWriter();
  68.             }
  69.             return _MyWriter;
  70.         }
  71.         set { _MyWriter = value; }
  72.     }
  73.     private void CreateMyWriter()
  74.     {
  75.         try
  76.         {
  77.             _MyWriter = new IndexWriter(azureDirectory, MyAnalyser, false);
  78.             //важны атомарные операции, так как несмотря на lock(oWriting) в случае нескольких дочерних классов индексирования, возникнуть может многопоточность
  79.             Interlocked.Exchange(ref LockFailsCounter,  0); //успешно открылось на запись, сбросили счетчик неудач
  80.         }
  81.         catch (LockObtainFailedException e0)
  82.         {
  83.             //если уже много ошибок подряд, около часа
  84.             if (LockFailsCounter > 30)
  85.             {
  86.                 //принудительно удаляем блокировку =(
  87.                 azureDirectory.ClearLock("write.lock");
  88.                 //Trace.Write(e0);
  89.                 Trace.Write("Принудительное снятие старой блокировки на запись.");
  90.                 Thread.Sleep(60 * 1000); //1 min for unlock
  91.                 _MyWriter = new IndexWriter(azureDirectory, MyAnalyser, false);
  92.                 Interlocked.Exchange(ref LockFailsCounter,  0);//успешно открылось на запись, сбросили счетчик неудач
  93.             }
  94.             else
  95.             {
  96.                 Interlocked.Increment(ref LockFailsCounter);
  97.                 throw;
  98.                 //вызовем ошибку, чтобы запрос на индексирование был повторен через некоторое время, когда файл разблокируется
  99.             }
  100.         }
  101.         catch (Exception err)
  102.         {
  103.             Trace.WriteLine("Не удалось открыть старый индекс: " + err.Message);
  104.             if (System.IO.Directory.Exists(cachepath))
  105.             {
  106.                 System.IO.Directory.Delete(cachepath, true);
  107.                 //удаляем возможный старый хлам помешавший открытию
  108.                 System.IO.Directory.CreateDirectory(cachepath);
  109.             }
  110.             //создадим новый еще раз!
  111.             _MyWriter = new IndexWriter(azureDirectory, MyAnalyser, true);
  112.         }
  113.         _MyWriter.SetMergeScheduler(Merger);
  114.         _MyWriter.SetMergeFactor(10);
  115.         //_MyWriter.SetUseCompoundFile(false);
  116.     }
  117.     /// <summary>
  118.     /// Время открытия поискового индекса
  119.     /// </summary>
  120.     protected DateTime? SyncTime
  121.     {
  122.         get { return HttpContext.Current.Application[GetType() + "synctime"] as DateTime?; }
  123.         set { HttpContext.Current.Application[GetType() + "synctime"] = value; }
  124.     }
  125.     /// <summary>
  126.     /// Количество неудачных попыток открыть индекс на запись
  127.     /// </summary>
  128.     /// <remarks>Важно, что это один объект на все дочерние классы индексирования</remarks>
  129.     private static int LockFailsCounter =  0;
  130.     /// <summary>
  131.     /// То, как писать
  132.     /// </summary>
  133.     protected IndexSearcher _MySearcher
  134.     {
  135.         get { return HttpContext.Current.Application[GetType() + "searcher"] as IndexSearcher; }
  136.         set { HttpContext.Current.Application[GetType() + "searcher"] = value; }
  137.     }
  138.     public IndexSearcher MySearcher
  139.     {
  140.         get
  141.         {
  142.             if (SyncTime.HasValue && SyncTime.Value.AddHours(1)<DateTime.Now)//более ~  с момента последней синхронизации(!)
  143.             {
  144.                 //делаем запись только в том, случае, если индекс уже был успешно открыт ранее на запись, и сделана синхронизация с основным
  145.                 //иначе может произойти потеря
  146.                 IndexCommit();
  147.             }
  148.             if (_MySearcher != null) return _MySearcher;
  149.             lock (oWriting)
  150.             {
  151.                 if (_MySearcher != null) return _MySearcher;
  152.                 var myPerf = new PerformanceTimer();
  153.                 myPerf.StartTimer();
  154.                 try
  155.                 {
  156.                     _MySearcher = new IndexSearcher(azureDirectory, true);
  157.                 }
  158.                 catch (CorruptIndexException)
  159.                 {
  160.                     //требуется починка индекса
  161.                     FixIndex(RoleEnvironment.DeploymentId);
  162.                 }
  163.                 catch (Exception e1) //если у нас нету еще директории индекса или сбой соединения
  164.                 {
  165.                     Trace.Write(e1);
  166.                     if (_MyWriter != null)
  167.                     {
  168.                         Trace.Write("Warning: have to close MyWriter to open MySearcher!");
  169.                         _MyWriter.Commit();
  170.                         _MyWriter.Close();
  171.                         _MyWriter = null;
  172.                     }
  173.                     if (System.IO.Directory.Exists(cachepath))
  174.                     {
  175.                         System.IO.Directory.Delete(cachepath, true); //удаляем возможный старый хлам помешавший открытию
  176.                         System.IO.Directory.CreateDirectory(cachepath);
  177.                     }
  178.                     try
  179.                    &nbssearcherp;{
  180.   nbsp;запись                      //важно для всех случаев когда индекс открывается в отсутствующую папку.
  181.                         var openindex = MyWriter;
  182.                     }catch(LockObtainFailedException)
  183.                     {
  184.                         //Папка с индексом успешно открыта и создана, однако другая роль уже пишет индекс
  185.                     }
  186.                     try
  187.                     {
  188.                         _MySearcher = new IndexSearcher(azureDirectory, true);
  189.                     }catch(FileNotFoundException foi)
  190.                     {
  191.                         //индекс скопированный с мастера не содержит нужного сегмента
  192.                         if(foi.InnerException!=null && foi.InnerException.GetType() == typeof(StorageClientException))
  193.                         {
  194.                             //вызываем утилиту исправления ошибок
  195.                             FixIndex(RoleEnvironment.DeploymentId);
  196.                         }
  197.                     }
  198.                     Trace.Write(String.Format("BadMode: _MySearcher {0} loaded in {1}", GetType().Name, myPerf.StopTimer()));
  199.                 }
  200.                 if(firsttime)
  201.                 {
  202.                     firsttime = false;
  203.                     Trace.Write(String.Format("GoodMode: _MySearcher {0} loaded in {1}", GetType().Name, myPerf.StopTimer()));
  204.                 }
  205.             }
  206.             return _MySearcher;
  207.         }
  208.     }
  209.     /// <summary>
  210.     /// Морфологический анализатор
  211.     /// </summary>
  212.     internal static MorphologyAnalyzer _MyAnalyser;
  213.     protected MorphologyAnalyzer MyAnalyser
  214.     {
  215.         get
  216.         {
  217.             if (_MyAnalyser != null && _MyAnalyser.Morph != null && _MyAnalyser.Morph.isLoaded) return _MyAnalyser;
  218.             Trace.Write("Заново загружаем MorphologyAnalyzer");
  219.             _MyAnalyser = new MorphologyAnalyzer(null, false);
  220.             return _MyAnalyser;
  221.         }
  222.     }
  223.     static IndexerBase()
  224.     {
  225.         RoleEnvironment.Stopping += delegate
  226.         {
  227.             try
  228.             {
  229.                 //дожидаемся завершения записи
  230.                 lock (oWriting)
  231.                 {
  232.                     //останавливаем поток слияния
  233.                     Merger.Close();
  234.                     Merger = null;
  235.                 }
  236.             }catch(Exception e)
  237.             {
  238.                 Trace.Write(e);
  239.             }
  240.         };
  241.     }
  242.     /// <summary>
  243.     /// Запись индекса на диск и переоткрытие поискового механизма
  244.     /// </summary>
  245.     protected void IndexCommit()
  246.     {
  247.         SyncTime = DateTime.Now;
  248.         lock (oWriting)
  249.         {
  250.             if (_MyWriter != null)
  251.             {
  252.                 //закрыли текущий коммит
  253.                 try
  254.                 {
  255.                     _MyWriter.Commit();
  256.                 }catch(CorruptIndexException)
  257.                 {
  258.                     FixIndex(RoleEnvironment.DeploymentId);
  259.                 }
  260.                 _MyWriter.Close();
  261.                 _MyWriter = null;
  262.             }
  263.             if (_MySearcher != null)
  264.             {
  265.                 //закрыли тукущий поисковик
  266.                 _MySearcher.Close();
  267.                 _MySearcher = null;
  268.             }
  269.         }
  270.         //открыли новый поисковик
  271.         var opensearcher = MySearcher;
  272.     }
  273.     /// <summary>
  274.     /// Серьезный процесс по востановлению индекса 
  275.     /// </summary>
  276.     /// <param name="DeploymentId"></param>
  277.     /// <returns></returns>
  278.     public string FixIndex(string DeploymentId)
  279.     {
  280.         var myPerf = new PerformanceTimer();
  281.         myPerf.StartTimer();
  282.         Trace.WriteLine(String.Format("Вызов проверки статуса и исправления индекса со значением  {0} / {1}", DeploymentId, GetType().Name));
  283.         string lockfile = GetType().Name + "_" + DeploymentId + ".lock";
  284.         Lock indexLock = azureDirectory.MakeLock(lockfile);
  285.         if (indexLock.Obtain())//блокировка основного индекса
  286.         {
  287.             try
  288.             {
  289.                 //устроим проверку индекса!
  290.                 CheckIndex fixer = new CheckIndex(azureDirectory);
  291.                 CheckIndex.Status oStatus = fixer.CheckIndex_Renamed_Method();
  292.                 Trace.WriteLine(GetType().Name+":status=" + oStatus.numBadSegments + ":" + oStatus.totLoseDocCount);
  293.                 if (oStatus.numBadSegments != oStatus.numSegments && oStatus.numBadSegments >  0)
  294.                 {
  295.                     fixer.FixIndex(oStatus); //опасная функция перезаписи
  296.                 }
  297.                 Trace.WriteLine(GetType().Name + ":FixIndex finished");
  298.                 //убедимся, что разблокировали индекс после правки
  299.                 azureDirectory.ClearLock("write.lock");
  300.             }
  301.             catch (Exception e1)
  302.             {
  303.                 Trace.Write(e1);
  304.             }
  305.             finally
  306.             {
  307.                 indexLock.Release();
  308.                 azureDirectory.DeleteFile(lockfile);
  309.             }
  310.             //////////******************////////////
  311.         }
  312.         return myPerf.StopTimer().ToString();
  313.     }
  314. }


Поясню основные моменты:
1. Класс абстрактный, так как от него наследуется WCF сервис, который может содержать свой индекс. То есть один инстанс может работать с несколькими индексами Lucene которые хранятся в одном BlobStorage в разных папках.
2. В Azure любой инстанс может быть выключен в любую секунду, поэтому важно обрабатывать событие остановки и корректно завершать работу отдельного потока ConcurrentMergeScheduler занимающегося оптимизацией индекса.
3. Объект чтения MySearcher у нас открыт всегда, чтобы минимизировать время поиска. Объект записи MyWriter создается только при получении данных на запись и соответственно закрывает и блокирует чтение для текущего индекса на инстансе.
4. Очень важна отказоустойчивость, поэтому такая детальная обработка ошибок LockObtainFailedException - когда пришли данные на запись, а основной индекс в BlogStorage заблокирован. Например, другим инстансом, который после этого падает по OutOfMemoryException и не снимает блокировки. Соответственно через несколько таких ошибок блокировка снимается автоматически и буфера поставщиков данных не успевают переполниться и потери данных не происходит.
5. Другая стандартная ошибка - CorruptIndexException - вполне может быть следствием сбоем сети при передачи больших файлов. При этом последняя операция записи отменяется, однако это занимает значительное время и блокирует другие операции записи. Ниже, как это выглядит в логах.




Еще раз перечислю основные проблемы, к которым нужно быть готовыми: одновременный приход запросов на индексирование, произвольное выключение инстанса, нечитаемость индекса, нехватка памяти, невозможность эксклюзивно заблокировать индекс, очищение LocalStorage. Последние подразумевает то, что согласно документации, подключаемый к инстансу диск несмотря на настройку cleanOnRecycle="false" всеравно не является величиной постоянной и может быть очищен в любой момент. Последнее означает повторную синхронизацию с центральным BlobStorage и открытие объектов чтения и записи займет дольше обычного (2-3 сек) на несколько минут. В итоге все работает стабильно и загрузка одного индексирующего инстанса показана ниже. К слову, мы используем всего-лишь два инстанса с минимальными мощностями, так как сильно ограничены в ресурсах. Пикам по 100% соответствует обработка пакета на индексацию. С уверенностью можно сказать, что за простой ресурсов мы не платим.


Вторая картинка приведена для сравнения, и формируется на системном портале Azure как среднее по WebRoles в Endpoint. Очевидно, что она не дает представления о том, что твориться с инстансами.

Выполнение поисковых запросов

В условиях, когда мы не знаем в каком состоянии находится инстанс, пишет он или нет, единственный правильный вариант - отправлять поисковый запрос на все инстансы, и брать первый ненулевой ответ. При наличии ресурсов можно было бы выделить отдельную Endpoint и WebRole, которая работала бы только на чтение. Но мы не ищем простых путей, поэтому на каждой webrole с индексом имеется следующая поисковая функция, выполняющаяся только если инстанс не пишет в индекс

Copy Source | Copy HTML
  1. /// <summary>
  2. /// Контекстный поиск с проверкой на поток записи, если открыта запись, то возвращаем NULL
  3. /// </summary>
  4. /// <param name="opts">Слова и параметры поиска</param>
  5. /// <param name="ignorelock">Не проверять идет ли запись</param>
  6. /// <returns>найденные результаты, CAN BE NULL</returns>
  7. public IndexedDocument[] SearchForAdvanced(SearchForOptions opts, bool ignorelock)
  8. {
  9.     //без проверки на блокировку
  10.     if (ignorelock) return SearchFor(opts);
  11.     bool isNotLocked = false;
  12.     try
  13.     {
  14.         isNotLocked = Monitor.TryEnter(oWriting);
  15.     }
  16.     finally
  17.     {
  18.         if (isNotLocked)
  19.         {
  20.             //важно освободить блокировку на поиск как можно скорее
  21.             Monitor.Exit(oWriting);
  22.         }
  23.     }
  24.     if (isNotLocked)
  25.     {
  26.         return SearchFor(opts);
  27.     }
  28.     //заблокировано
  29.     return null;
  30. }

И соответственно на поисковом клиенте реализуется многопоточная отправка с ожидаем быстрейшего ненулевого результата через ThreadPool.QueueUserWorkItem(state => SearchForAdvancedAsync(oData));

Copy Source | Copy HTML
  1. /// <summary>
  2. /// Объет параллельного запроса
  3. /// </summary>
  4. public class SearchAsyncObject
  5. {
  6.     /// <summary>
  7.     /// Число параллельных запросов
  8.     /// </summary>
  9.     public readonly int Count = 3;
  10.     /// <summary>
  11.     /// Число обработанных вызовов
  12.     /// </summary>
  13.     public int Finished =  0;
  14.     /// <summary>
  15.     /// Поисковые параметры
  16.     /// </summary>
  17.     public SearchForOptions opts = null;
  18.     /// <summary>
  19.     /// Результат поиска
  20.     /// </summary>
  21.     public IndexedDocument[] Founds = null;
  22.     /// <summary>
  23.     /// Mutex
  24.     /// </summary>
  25.     public ManualResetEvent mre = new ManualResetEvent(false);
  26. }
  27. /// <summary>
  28. /// Функция завершения поиска
  29. /// </summary>
  30. private static void SearchForAdvancedAsync(SearchAsyncObject data)
  31. {
  32.     try
  33.     {
  34.         IndexClient service = Indexer.GetIndexClient();
  35.         IndexedDocument[] founds = service.SearchForAdvanced(data.opts, false);
  36.         if (founds != null && data.Founds == null) //найден результат(!)
  37.         {
  38.             data.Founds = founds;
  39.             data.mre.Set();
  40.         }else if (Interlocked.Increment(ref data.Finished) >= data.Count){
  41.             data.mre.Set();//все потоки завершили работу
  42.         }
  43.     }
  44.     catch (Exception e1)
  45.     {
  46.         Trace.Write(e1);
  47.     }
  48. }

Функции добавления документов в индекс и выполнение операций поиска уже на конкретной машине выполняются как описано в документации Lucene.

 

Результат

Построенный поисковый сервис используется для выявления наиболее интересной информации в русскоязычных социальных сетях и ранжирования этой информации для формирования топа событий на сайте http://t30p.ru. Полученное таким образом самостоятельное независимое СМИ сильно снижает стоимость и увеличивает качество интернет журналистики. В поисковый индекс можно добавлять новые индексные поля и обновлять код без нарушения работы сервиса или потери данных (проверено). Сейчас в индексе более 200 млн. объектов от 2,5 млн. социальных аккаунтов; примерно 2,5 млн. новых объектов в сутки; размер индекса в пределах 100 GB; время выполнения запросов - 0,5 сек; к индексу выполняется примерно 1000 запросов в час, в том числе внутре-технические. Ожидаемое время запаздывания между нахождением данных и появлением их в поисковой выдаче - 5-15 мин. (в частности, задержку дает "кэширующий" CDN на поисковой странице).

Проблемы и решения

Более int.MaxValue объектов
Поисковый индекс Lucene ограничен примерно в 2 млрд. объектов. Поэтому для работы с большим числом объектов на помощь приходит MultiSeacher, который еще не портирован в .Net версию. Он позволяет производить поиск по конечному набору поисковых индексов при этом теряется скорость.
Нехватка места или превышение 100ТБ
При использовании MultiSeacher для поиска по нескольким поисковым индексам каждый индекс может быть привязан к своему диску или BlobStorage, что решает проблему с ограничением места.
Дублирование информации
Несмотря на обработку поточных данных от самих социальных сервисов, в них часто происходят сбои и разрывы соединений, что приводит к разного рода ошибкам. Так возможно повторное индексирование постов. Поэтому на поисковом клиенте производится проверка уникальности ссылки на пост в результате. В теории эту задачу можно переложить на фильтр DuplicateFilter
Lucene 4.0 имеет классы встроенной русскоязычной морфологии
Какой бы не была встроенная морфология, я бы рекомендовал использовать более тяжеловесный Lemmatizer, библиотеки которого в архиве zip занимают 100МБ, что говорит о серьезности подхода к составлению словаря.
И куда потом все эти собранные данные?
Например, можно создать сервис в Azure Marketplace и продавать доступ к ним по фиксированной цене. Там же есть интересный платный сервис перевода текстов на многие языки, что позволит в автоматическом режиме перевести всю собранную информацию на любой язык и сделать международный топ русскоязычных социальных новостей.


Метки: , , , , , ,   Категории:Blogs | Csharp | Yandex | microsoft | Code


Про Твиттер и BlogEngine.NET

Индус опубликовал переделанный виджет Твиттера для BlogEngine, Это обновление вызвано было тем, что 10 октября, как некоторые могут заметить, Твиттер отключил выдачу данных по RSS, в связи с постепенным переходом на API v1.1. Отключение RSS выдачи вызывает проблему в том, что согласно Twitterizer.NET ленту пользователя можно получать только через функцию UserTimeline, которая подразумевает передачу данных oauth, то есть с аутентификацией. Однако, как умно поступил индус, оказывается всю ленту пользователя Твиттера можно сгружать напрямую, только уже не в виде RSS, а в виде XML или JSON:
Старая ссылка на RSS - https://twitter.com/statuses/user_timeline/t30p.rss (Not Valid) Новая ссылка на XML - http://api.twitter.com/1/statuses/user_timeline/t30p.xml Новая ссылка на JSON - http://api.twitter.com/1/statuses/user_timeline/t30p.json
То есть данные благоразумно вынесены на отдельный домен и добавлена версионность в виде единички.

Завершить пост хотелось бы опусом разработчиков BlogEngine, которые на днях заявили о выходе следующей версии 2.7, в которой по сути ничего нового, кроме обновления внутренних скриптов написания постов. Да и предыдущая версия 2.6 - содержала единственно обновление в виде "Мультиблогов", фишка которая полностью содрана с wordpress'a и которая, на мой взгляд, внесла неразбериху и в код и саму работу блогодвижка. Когда общая лента одного блога может быть сформирована как совокупность блогов внутри базы. Очевидно, что это по логике дублирует мультипользовательность, когда к одному домену-блогу имеют доступ несколько пользователей и они дружно выкладывают материалы. То есть разработка BlogEngine полностью стагнировала, как и у wordpress, после того, как основные разработчики системы ушли работать в Микрософт, а на смену пришли индусы.

Чтобы не быть голословным озвучу основные проблемы для блогдвижков, которые также поставили себе разработчики ЖЖ и которые позволили бы двигаться дальше. Их всего 3.
  • Azure + Multi Roles - чтобы один блог обслуживало несколько инстансов, то есть чтобы была масштабируемость блога. По факту это было сделано для blogscloud.ru, а то что предлагается как NuGet Package - все таже ненадежная одноинстансовая установка и неможет называться полноценным переносом в облако.
  • Mobile - у BlogEngine есть нормальная мобильная версия, но никто так и не занялся нормальным масштабированием картинок на лету. Это сложная задача, и хотелось бы чтобы опытные индусы ее решили.
  • Комментарии - до сих пор нет нормального решения для большого количества комментариев к посту. Всякие централизованные системы типа DISQUS тоже не устраивают, и медленно грузятся, и хранят контент не у нас. Да и децентролизованные блогодвижки должны быть максимально независимыми.
Этот список можно продолжить, но даже эту малость никто не делает. И для поисковиков ссылка на инструкцию, как перейти с блога wordpress на блог BlogEngine.NET.


Метки:   Категории:Blogs | Twitter | BlogEngine


livejournal latest records

В одном из последних обновлений Livejournal в целях популяризации новичков было сообщено о странице с самыми свежими записями. Этот вынужденный шаг супротив тренда по переводу своих блогов на стандалоны и на закрытие своих блогов, делая их платными. Так вот, в лучших традициях ЖЖ оказалось, что RSS выдача последних записей не работает. Точнее она не обновляется, ввиду того, что сильно закэширована под GoatProxy, да и реализована в каком-то жутком RDF с кастомными схемами. Поэтому, если приглядеться к самой странице, то можно увидеть 2 интересные вещи. Во первых, она делает вызова JSON на http://livejournal.com/__api/ - видимо API точка, документация по которой, как указано, находится по пути http://www.livejournal.com/support/faqbrowse.bml?faqid=377, где пишется, что Reserved for new feature. То есть этот API будет описан в лучшем случае в будущем, а пока можно попробовать использовать как есть. Все же JSON менее избыточен нежели RDF.

И напоследок, меня заинтересовала страница со статистикой http://www.livejournal.com/stats/. Видимо это старая, системная, которой давно никто не занимается. Так вот из нее следует, что активных авторов в ЖЖ порядка 100тыс. в сутки, и если добавить данные, что яндекс индексирует 150 тыс. записей в сутки, то всреднем 1,5 записи на активного блогера. Также есть данные по демографии и они очень странные, но их комментировать бесполезно, так как там явно замешаны американские боты.

Без ответа также остается вопрос получим ли мы возможность доступа на чтение и индексирование всех свежих комментариев в ЖЖ, или это останется преимуществом яндекса.

Метки:   Категории:Blogs | bugs | Livejournal


Вакансия в СУПе

Заметил вакансию открытую в СУПе - http://www.t30p.ru/jobs.aspx?l=http%3a%2f%2fhh.ru%2fvacancy%2f6382222 - позволяющую запросто любому блогеру стать "платным блогером" с белой зарплатой.

Метки:   Категории:Blogs


Про последние обновление ЖЖ

Обновился ЖЖ и мы получили 3 строчки промо и списки бана для пользователей. Да, эту фишку просили многие блогеры последние 3 года, но обновление несет следующие минусы
1) Размывается аудитория, мощность ЖЖ уменьшается, как новостного ресурса.
2) Утверждается, что списки забаненных будут влиять на рейтинг - то есть нарушение главного принципа, что экспертная система влияет на свою же оценку.

Метки:   Категории:Blogs


про блоги и жж

Очередное видео про блоги, комментарии
1. Правильно, что скоро соцкап скоро будет типизирован. В целом, социальный авторитет тоже это ждет.
2. Да, AdSense не приносит денег вообще.
3. Основные деньги ЖЖ приносятся через спец проекты и брендовые блоги. Так что blogscloud.ru - тоже правильная концепция. Можно попробовать концепцию "создай свое СМИ" и тем самым открыть возможности создания блогов для всех.
4. Архитектура ЖЖ - убога, например проблема разделения представления и отображения по темам оформления успешно решена в BlogEngine, особенно радует мобильная версия в отдельной теме.
5. Государство не делает "интренет все в одном" в отличии от фейсбука и гугла только потому, что это дорого и правильно давать другим возможность ошибаться и тратиться на социальный сети, которые тем не менее будут контролироваться государством.

Метки:   Категории:Blogs


Azure Tables / Multi Role Counter

Пост содержит много кода и призван рассказать о реализации счетчика посещений в мульти-инстансовой мультипоточной среде, коей является любое веб приложение под Windows Azure. Впервые простая реализация счетчика была продемонстрирована в шоу channel9 и в целом он работал неправильно и решал лишь проблему мультипоточности при помощи Interlocked объекта.
Во-первых сразу определимся, что данные у нас по счетчику будут складироваться в Azure Table - по сути это следующее этап развития SQL баз данных, которые не требуют знания SQL, а позволяют описывать структуру объекта прямо в коде. Для работы с таблицами используется Entity Framework, для которого удобно использовать обертку SyncTimes примерно следующего вида:
Copy Source | Copy HTML
  1. /// <summary>
  2. /// Многопоточная работа с таблицами Азуре
  3. /// </summary>
  4. /// <typeparam name="T"></typeparam>
  5. public class SyncTimes<T> where T: TableServiceEntity,new()
  6. {
  7.     #region Azure Table's Row
  8.     public T _tc;
  9.     public T tc
  10.     {
  11.         get
  12.         {
  13.             if (_tc != null) return _tc;
  14.             _tc = Reload();
  15.             return _tc;
  16.         }
  17.         set { _tc = value; }
  18.     }
  19.     #endregion
  20.     public string roleid
  21.     {
  22.         get
  23.         {
  24.             return RoleEnvironment.IsAvailable ? RoleEnvironment.CurrentRoleInstance.Id : "0";
  25.         }
  26.     }
  27.     #region Работа через Таблицы
  28.     [ThreadStatic]
  29.     public static TableServiceContext _containerTable;
  30.     public TableServiceContext ContainerTable
  31.     {
  32.         get
  33.         {
  34.             if (_containerTable != null) return _containerTable;
  35.             lock (CurrentInstance)
  36.             {
  37.                 if (_containerTable != null) return _containerTable;
  38.                 CloudStorageAccount storageAccount = CloudStorageAccount.Parse(
  39.                     "DefaultEndpointsProtocol=http;AccountName=imagecontainer;AccountKey=");
  40.                 CloudTableClient tableClient = storageAccount.CreateCloudTableClient();
  41.                 tableClient.CreateTableIfNotExist(typeof(T).Name);
  42.                 // Retrieve a reference to a container  
  43.                 _containerTable = tableClient.GetDataServiceContext();
  44.                 _containerTable.IgnoreResourceNotFoundException = true;
  45.                 _containerTable.MergeOption = MergeOption.AppendOnly;
  46.             }
  47.             return _containerTable;
  48.         }
  49.         set { _containerTable = value; }
  50.     }
  51.     #endregion
  52.     public static SyncTimes<T> CurrentInstance
  53.     {
  54.         get
  55.         {
  56.             var table = HttpContext.Current.Items[typeof(T).Name] as SyncTimes<T>;
  57.             if (table != null) { return table; }
  58.             table = new SyncTimes<T>();
  59.             HttpContext.Current.Items[typeof(T).Name] = table;
  60.             return table;
  61.         }
  62.     }
  63.     /// <summary>
  64.     /// Создание запросного объекта
  65.     /// </summary>
  66.     /// <returns></returns>
  67.     public IQueryable<T> CreateQuery()
  68.     {
  69.         return ContainerTable.CreateQuery<T>(typeof(T).Name);
  70.     }
  71.     /// <summary>
  72.     /// загрузка-обновление таблицы
  73.     /// </summary>
  74.     /// <returns></returns>
  75.     private T Reload()
  76.     {
  77.         try
  78.         {
  79.             string blogid = Blog.CurrentInstance.Id.ToString();
  80.                 ReadOnlyCollection<EntityDescriptor> oEntities = ContainerTable.Entities;
  81.                 if (oEntities.Count >  0)
  82.                 {
  83.                     EntityDescriptor ed = oEntities.FirstOrDefault(
  84.                         p => p.Entity.GetType() == typeof (T) &&
  85.                              ((T) p.Entity).RowKey == roleid &&
  86.                              ((T) p.Entity).PartitionKey == blogid);
  87.                     if (ed != null)
  88.                     {
  89.                         ContainerTable.Detach(ed.Entity); //удалим из трекинга(!)
  90.                     }
  91.                 }
  92.                 T te = (from e in CreateQuery() where e.RowKey == roleid && e.PartitionKey == blogid select e).FirstOrDefault();
  93.                 if (te == null)
  94.                 {
  95.                     te = new T{PartitionKey = blogid,RowKey = roleid};
  96.                     ContainerTable.AddObject(typeof(T).Name, te);
  97.                     //ContainerTable.SaveChangesWithRetries();
  98.                 }
  99.                 return te;
  100.         }catch(Exception e1)
  101.         {
  102.             Trace.WriteLine(e1);
  103.         }
  104.         return null;
  105.     }
  106.     /// <summary>
  107.     /// Делаем обновление в БД.
  108.     /// </summary>
  109.     /// <remarks>передаем нул в качестве объекта, если хотим просто вызвать обновление</remarks>
  110.     /// <returns>TRUE if no error</returns>
  111.     public Boolean Update(object te, bool applychanges = true)
  112.     {
  113.             try
  114.             {
  115.                 if (te != null)
  116.                 {
  117.                         try
  118.                         {
  119.                             ContainerTable.UpdateObject(te);
  120.                         }
  121.                         catch(ArgumentException e0)//not tracking
  122.                         {
  123.                             ContainerTable.Detach(te);
  124.                             ContainerTable.AttachTo(typeof(T).Name, te, "*");
  125.                             ContainerTable.UpdateObject(te);
  126.                         }
  127.                         catch (DataServiceRequestException e1)//tracking by different uri
  128.                         {
  129.                             ContainerTable.Detach(te);
  130.                             ContainerTable.AttachTo(typeof(T).Name, te, "*");
  131.                             ContainerTable.UpdateObject(te);
  132.                         }
  133.                 }
  134.                 if (applychanges &&
  135.                     ContainerTable.Entities.Count(p => p.State != EntityStates.Unchanged && p.State!=EntityStates.Detached) >  0)
  136.                 {
  137.                     ContainerTable.SaveChangesWithRetries();
  138.                 }
  139.             }
  140.             catch (DataServiceRequestException ex)
  141.             {
  142.                 //значит объект не соответствует тому, что хранится в БД и нужно его обновить
  143.                 if (typeof(T) != typeof(HitsCounter))
  144.                 {
  145.                     Utils.Log(ex);
  146.                 }
  147.                 return false;
  148.             }
  149.         return true;
  150.     }
  151.     /// <summary>
  152.     /// Делаем обновление в БД.
  153.     /// </summary>
  154.     /// <returns></returns>
  155.     public T[] GetAllInstances(string blogid)
  156.     {
  157.         lock (ContainerTable)
  158.         {
  159.             try
  160.             {
  161.                 return (from e in CreateQuery() where e.PartitionKey == blogid && e.RowKey != roleid select e).ToArray();
  162.             }
  163.             catch (Exception e1)
  164.             {
  165.                 Trace.WriteLine(e1);
  166.             }
  167.         }
  168.         return null;
  169.     }
  170. }

Важно понимать, что [ThreadStatic] объект необъодимо занулять в начале хендлера каждого запроса, чтобы гарантировать, что внутри каждого потока у нас уникальный DataContext постоянный на протяжении всей обработки запроса. Теперь перейдем к реализации класса учета обращений. В моем случае мультипоточность учитывается классом ConcurrentDictionary и сама запись в Таблицу вызывается только при достижении 10 обработанных запросов в инстансе. Это не идеально, и имеет некоторую долю ошибок, однако реализация точного учета обращений в мульти-истансовой веб-роле привело бы к блокировки потока на 100мс, чего мы избегаем. Отмечу, что для разных инстонсов роли пишутся разные объекты в таблице, а при итоговом выводе, когда нужно получить сумму - они суммируются.
Copy Source | Copy HTML
  1. /// <summary>
  2. /// Summary description for TopPosts
  3. /// </summary>
  4. /// <remarks></remarks>
  5. [Extension("Counts and displays the number of viewers for a post", "3.0", "")]
  6. public class TopPosts
  7. {
  8.     /// <summary>
  9.     /// Initializes a new instance of the <see cref="TopPosts"/> class.
  10.     /// </summary>
  11.     /// <remarks></remarks>
  12.     static TopPosts()
  13.     {
  14.         Post.Serving += new EventHandler<ServingEventArgs>(OnPostServing);
  15.     }
  16.     /// <summary>
  17.     /// Called when [post serving].
  18.     /// </summary>
  19.     /// <param name="sender">The sender.</param>
  20.     /// <param name="e">The <see cref="BlogEngine.Core.ServingEventArgs"/> instance containing the event data.</param>
  21.     /// <remarks></remarks>
  22.     private static void OnPostServing(object sender, ServingEventArgs e)
  23.     {
  24.         NameValueCollection headers = HttpContext.Current.Request.Headers;
  25.         if (headers["X-moz"] == "prefetch")
  26.         {
  27.             return;
  28.         }
  29.         IPublishable ipub = (IPublishable)sender;
  30.         try
  31.         {
  32.             // Check For Single Post View, When viewing Specific Post, basically through post.aspx)
  33.             if (e.Location == ServingLocation.SinglePost)
  34.             {
  35.                 int viewCount;
  36.                 // Fetch out total views of current viewing post.
  37.                 viewCount = IncrementPostViewCount(ipub.Id.ToString());
  38.                 // Override the body of the post (temporary) to display total views
  39.                 if (Security.IsAuthenticated)
  40.                 {
  41.                     e.Body = String.Format(Resources.labels.totalViews + ": {0}<br/>", viewCount) + e.Body;
  42.                 }
  43.             }
  44.             else if (e.Location == ServingLocation.PostList && Security.IsAuthenticated)
  45.             {
  46.                 int viewCount = GetCountForPost(ipub.Id.ToString());
  47.                 // Override the body of the post (temporary) to display total views
  48.                 e.Body = String.Format(Resources.labels.totalViews + ": {0}<br/>", viewCount) + e.Body;
  49.             }
  50.         }
  51.         catch (Exception)
  52.         {
  53.         }
  54.     }
  55.     /// <summary>
  56.     /// Gets the popular posts.
  57.     /// </summary>
  58.     /// <param name="numberOfTopPosts">The number of top posts.</param>
  59.     /// <returns></returns>
  60.     /// <remarks></remarks>
  61.     public static List<KeyValuePair<string, int>> GetPopularPosts(int numberOfTopPosts)
  62.     {
  63.         List<KeyValuePair<string,int>> list = new List<KeyValuePair<string, int>>( 0);
  64.         IQueryable<HitsCounter> q = SyncTimes<HitsCounter>.CurrentInstance.CreateQuery();
  65.         HitsCounter[] posts =
  66.             (from e in q where e.PartitionKey == Blog.CurrentInstance.Id.ToString() select e).OrderByDescending(
  67.                 z => z.hits).Take(numberOfTopPosts).ToArray();
  68.         list.AddRange(posts.Select(hitsCounter => new KeyValuePair<string, int>(hitsCounter.RowKey, hitsCounter.hits)));
  69.         return list;
  70.     }
  71.     /// <summary>
  72.     /// Gets total view count for a certain post
  73.     /// </summary>
  74.     /// <param name="postId">the post id</param>
  75.     /// <returns>total live views count</returns>
  76.     /// <remarks></remarks>
  77.     public static int GetCountForPost(string postId)
  78.     {
  79.         return (from e in SyncTimes<HitsCounter>.CurrentInstance.CreateQuery() where e.RowKey == postId select e).
  80.             Sum(hitsCounter => hitsCounter.hits);
  81.     }
  82.     /// <summary>
  83.     /// Количество просомтров для обновления
  84.     /// </summary>
  85.     public static ConcurrentDictionary<string, int> oViews = new ConcurrentDictionary<string, int>();
  86.     /// <summary>
  87.     /// Increment view count of a post
  88.     /// </summary>
  89.     /// <param name="postId">Id of the post</param>
  90.     /// <returns>the post's view count</returns>
  91.     /// <remarks></remarks>
  92.     public static int IncrementPostViewCount(string postId)
  93.     {
  94.         int viewCount =  0;
  95.         try
  96.         {
  97.             string PartitionKey = RoleEnvironment.IsAvailable ? RoleEnvironment.CurrentRoleInstance.Id : "0";
  98.             PartitionKey += "_"+Blog.CurrentInstance.Id.ToString();
  99.             //если есть несохраненные изменения, то это для нас!
  100.             HitsCounter h = null;
  101.             IQueryable<HitsCounter> q = SyncTimes<HitsCounter>.CurrentInstance.CreateQuery();
  102.             HitsCounter[] hArray = (from e in q where e.RowKey == postId select e).ToArray();
  103.             foreach (HitsCounter hitsCounter in hArray)
  104.             {
  105.                 viewCount += hitsCounter.hits;
  106.                 if (hitsCounter.PartitionKey == PartitionKey)
  107.                 {
  108.                     h = hitsCounter;
  109.                 }
  110.             }
  111.             viewCount++;
  112.             bool success = false;
  113.             oViews.AddOrUpdate(postId, 1, (key, oldValue) => oldValue+1);
  114.             if (oViews[postId] > 10)
  115.             {
  116.                 if (h == null)
  117.                 {
  118.                     h = new HitsCounter(PartitionKey, postId)
  119.                             {
  120.                                 hits = oViews[postId]
  121.                             };
  122.                     SyncTimes<HitsCounter>.CurrentInstance.ContainerTable.AddObject("HitsCounter", h);
  123.                     success = SyncTimes<HitsCounter>.CurrentInstance.Update(null);
  124.                 }
  125.                 else
  126.                 {
  127.                     h.hits += oViews[postId];
  128.                     success = SyncTimes<HitsCounter>.CurrentInstance.Update(h);
  129.                     //успешное обновление
  130.                 }
  131.                 if (!success)
  132.                 {
  133.                     Utils.Log("Postid=" + postId + "; views=" + oViews[postId]);
  134.                 }
  135.                 else
  136.                 {
  137.                     int viewCount2;
  138.                     oViews.TryRemove(postId, out viewCount2);
  139.                 }
  140.             }
  141.         }
  142.         catch (DataServiceRequestException e1)
  143.         {
  144.             Utils.Log(e1);
  145.         }
  146.         catch(Exception e2)
  147.         {
  148.             Utils.Log(e2);
  149.         }
  150.         return viewCount;
  151.     }
  152. }

В ближайшие дни расскажу, почему обновление BlogEngine 2.6 содержит большой идеологический минус и о том, какие есть сложности в постронии блогохостинговой платформы.

Метки: , ,   Категории:Blogs | Code


обо всем по немногу

  • Очень интересная флешка с информацией о размерах всех известных человечеству объектах
  • Оптимизация 100 из 100 по версии гугла для блога на BlogEngine достижима
  • В ЖЖ на след.неделе появится социальный капитал, который уже можно смотреть на страницах топа, сейчас там обновленная версия по вчерашнее число.
  • Для рейтинга по фейсбуку появилась возможность вывести колонку с кнопками лайков, что позволяет удобно залайкать почти все русскоязычные страницы.
  • В ходе тестирований ссылки с русскоязычными буквами признаны неполноценными в некоторых браузерах, особенно в Хроме, поэтому проставляем только англоязычные

Метки:   Категории:Blogs | t30p


Блог на мобильном

Победителем конкурса на лучшие тему оформления блога стала тема для просомтра блога на мобильном - очень красиво, надо поставить и в свои же блоги по умолчанию

Метки:   Категории:Blogs


Вблоге

Вконтакте заявил, что собирается стать полноценной блогплатформой . Ничего удивительного, ведь по статистике в США стандалоне блоги имеют вторую суммарную после фейсбука посещаемость. По случаю порекламируюсь, что мы умеем создавать уникальный стендалоне блоги в облаке.

Метки:   Категории:Blogs


bb AdSense

Выяснилась причина, по которой гугл втихаря крутил у меня в блоге рекламу 3 месяца, а показы незасчитывал. А вину этому стал пост http://t30p.livejournal.com/14701.html. В правилах гугла написано, что блокируются сайты, которые содержат либо 1) непристойные или провокационные изображения 2) материалы, содержащие грубую или ненормативную лексику, в том числе эротические рассказы 3) сексуальные советы или рекомендации 4) эротические игрушки и продукция и т.д. А именно этим наполнена блогосфера, особенно по пятницам. В общем 100 у.е. в год не стоят того, чтобы посетители видели объявления. Все, отключаю AdSense в блогах, а жаль, ведь планировал окупить платный аккаунт за год.

UPDATE: После публикации этой записи и отключения рекламы в AdSense, СУП тут же перевел мой блог t30p@lj в режим read-only за публикацию полгода назад поста про навального. Собственно текст разбирательства с конфликтной комиссией. И копия самого поста про навального.

Метки:   Категории:Blogs | Google


Про YAC2011

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

Единственное, что интересное, так это ЯндексБлоги. Удалось узнать, что последнее время они решают технические вопросы связанные с выросшей нагрузкой, поэтому и не выкладывают новых фишек. И видимо до нового года ничего не будет. Сейчас яндекс индексирует около 7 млн. постов в блогах в сутки, из них около 80% - это микроблоги на mail.ru, а еще 70% оставшегося - спам.

Метки:   Категории:Blogs | Yandex


Agava tango down

Только что упали все win VDS агавы. Пока новостей нет, но в месте с ними недоступны и t30p.ru . UPD: Были перебои электричества, где-то 3 часа потеряли.

Метки:   Категории:Blogs | bugs


ЖЖ update comment

На днях обновился ЖЖ и, что самое интересное, были озвучены планы на следующий update. В эти планы входит сделать новый топ записей, обещают убрать все "гавно"! ну надо же! И вместе с тем говорят про введение новой возможности для любого блогера выведение своей записи на главную lj.ru . Изначально, как мне казалось, для этого было введена возможность "рекомендовать", но как выяснилось, больше людей нажимают "лайки фейсбука", нежели скрытую внизу страницы кнопку рекомендовать. Таким образом, создавая платный топ - ЖЖ усердно готовится к политическим баталиям следующего года и уже планирует технологию собственного обогащения. Чтобы политические организации платили не блогерам посредникам, а напрямую в кассу ЖЖ. Комментарий тут простой - "ЖЖ всегда находило интересные способы, чтобы потерять аудиторию, но не полностью".

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

Метки:   Категории:Blogs | Livejournal


Опять про рейтинг яндекса.

Сегодня обновился рейтинг Яндекса. Некоторые, особо завязанные на нем, уже успели поднять в жж очередной плачь. Некоторые предлагают массово удалять кнопки яндекса. А прошло-то всего 3 месяца с прошлого обновления от 27.03.2011, что вполне нормальный срок для пересчета графа на 20 млн. вершин и всех ссылок в них за год, которых должно быть около 2 млрд. для всей блогосферки.

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

Метки:   Категории:Blogs | Yandex


Госзакупки в блогосфере

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

Во-первых, технические требования были изложены крайне коряво и тертые люди уверяли меня, что документ специально написан для местных, да еще и с нарушением ФЗ.
На мой же взгляд, проблемные моменты следующие:
  • В ТЗ написано, что "в случае невозможности получения сообщений по электронной почте, информация доставляется силами Исполнителя и на средства Исполнителя в указанное выше время на электронных носителях" - передача информации на носителях напрямую в администрацию самары для многих затруднительна и нет гарантии получения электронного сообщения заказчиком на протяжении всего срока работ.
  • В ТЗ запрашивается "Ежедневный отчет ... по электронной почте в 10.00 ч. и в 16.00 ч.". Такая регулярность малоосмыслена, так как картина обсуждения в соц.сетях и частота появляния новых упоминаний об администрации не настолько велика. Молодежные движения предпочитают еженедельный или два раза на неделе вид ответности. Более регулярный мониторинг обычно возлагается на пресс-службы администраций, в которых налажено круглосуточное дежурство.
  • По ТЗ необходимо проставлять "Количество просмотров публикации". Важно понимать, что в большинстве случаев эта величина имеет оценочный характер и далека от действительности. И даже если сегодня у негативного отзыва мало просмотров, то неизвестно, что будет завтра. Поэтому все упоминания должны иметь одинаковую очередность при анализе.
  • Пункт "Информирование заказчика о появление интересных, острых сообщений в течение дня....в течение 30 минут после появления информации в сети". Все современные мониторинги социальных сетей заточены на работе через открытые интерфейсы поисковых машин. Которые в свою очередь не гарантируют, что информация будет проиндексирована и норма гарантированного индексирования записи для различных платформ составляет 4 часа.
  • В порядке расчетов указано, что "заказчик производит оплату ежемесячно по факту оказания услуг в течение 30 календарных дней с момента подписания актов оказанных услуг.". 30 дней - слишком большой временной интервал для проведения оплаты, сравнимый со всем периодом оказания услуги.
  • В ТЗ указано "Совместная работа с Заказчиком по формированию оперативных ответов, комментариев и реплик - выстраивание эффективных коммуникационных связей". Такая формулировка выходит за рамки мониторинга блогосферы и затрагивает услугу по пиару в социальных сетях, что требует несколько большей оплаты.


Но не смотря ни на что торги проведены, подано 11 заявок, в том числе 2 от московских фирм и выиграла Тюмень, получив аж 139т.р. до вычета налогов. Что примерно 35т.р. в месяц на одного человека. В комиссии присутствовали заместитель председателя и секретарь. Таким образом, ручной бесцельный труд остается дешевле автоматизированного и уходит по разумным ценам.

Метки:   Категории:Blogs | Government


победа здравого смысла

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

Метки:   Категории:Blogs


Про ЖЖ и рынду

Во вторник прошло странное событие, которое называлось "Рында ЖЖ 2010" и имело своею целью простимулировать лучших блогеров за 2010 год и их журналистскую активность на 2011 год. Отмечу, что приз взяла Волочкова, которая скандал-то устроила уже в январе 2011 года. Все кто посетили мероприятие, мало того, что были пользователями Фейсбука, странный ход СУПа, но и отзываются о скучности мероприятия и тесноте, в которой все проходило. Венцом оного действа можно назвать очередной пост блогера с 8ми летним стажем, который говорит о смерти ЖЖ в текущем варианте и призывается сделать из ЖЖ Фейсбук (хаха). Единственная дельная мысль приведенная им - что администрации СУПа стоит сконцентрироваться на мероприятиях для платных пользователей, а не для всех. Это бы уменьшило число участников, но улучшило их качество.
Отдельно смешно читать про то, что в Фейсбуке меньше спама и что там реальные люди.

Метки:   Категории:Blogs | Livejournal


Платные блогеры

Из новостей блогосферы, оказывается с начала года 2 известных блогера объявили, что пишут рекламные посты в открытую за бабки: russos.livejournal.com - 15т.р. за пост, и zyalt.livejournal.com - 50т.р. Что примечательно, названные суммы вполне коррелируют с опубликованной мною таблицей c оценками стоимости постов в рунете на основе получаемого эффекта. Что примечательно, математически им были названы суммы в 20т.р. для russos и 40т.р. для zyalt.

Метки:   Категории:Blogs


Кто я?

Программист. Я слежу за блогосферой и знаю, как будет развиваться интернет. Когда у меня есть время я даже прилагаю для этого усилия. Подробнее

Последние комментарии

Не отображать

Topbot at FeedsBurner

Облако тэгов

Мои Твиты

Twitter декабря 7, 13:11
Механический медведь для Америки - Погружение во Тьму http://dlvr.it/Qt3hCf https://twitter.com/f1ashr/status/1071029327888736256/photo/1

Twitter декабря 6, 07:49
@coinhive_com Are you paying or not? Are you stopping your service?

Twitter декабря 1, 15:49
Международная система единиц СИ http://dlvr.it/QsZWxP https://twitter.com/f1ashr/status/1068894908939784192/photo/1

Twitter ноября 30, 15:47
Пользователи умудрились поломать гео-локацию в Инстаграм http://dlvr.it/QsVQn0

Twitter ноября 30, 15:47
Популярные блогеры и посты за 2017 год http://dlvr.it/QsVQmn https://twitter.com/f1ashr/status/1068532024984498176/photo/1

Twitter ноября 30, 15:47
Разбор сериала Медичи: Повелители Флоренции http://dlvr.it/QsVQmf https://twitter.com/f1ashr/status/1068532019930353664/photo/1

Twitter ноября 29, 15:45
Обновлен робот instagram для лайков http://dlvr.it/QsPrh2

Twitter ноября 29, 15:45
Skype and Baidu links http://dlvr.it/QsPrht

Twitter ноября 29, 15:45
О смене дизайна Вконтакте http://dlvr.it/QsPrgT

Twitter ноября 23, 13:18
Статистика просмотров Телеграма http://dlvr.it/Qrwybs

Twitter ноября 20, 11:54
По-тихому закрылся http://taaasty.com http://dlvr.it/QrgcJf

Twitter ноября 15, 21:21
Разное - сетевое http://dlvr.it/QrLfbH

Twitter ноября 12, 08:54
@lopp @coinhive_com Beware, that coinhive doesnt process payouts for last month and no reply from them.

Twitter ноября 6, 08:26
@MultiFollow_com @coinhive_com The same, cant get payment since Oct 29

Twitter октября 29, 18:13
Вконтакте закрыла гео-поиск http://dlvr.it/QptcDZ

Twitter октября 29, 17:48
RT @t30p: Моя Борьба (Mein Kampf) - Мыслить №112 https://youtu.be/eq3r3n3jAkY с помощью @YouTube

Twitter октября 23, 17:57
8-й час в России http://dlvr.it/QpG99N https://twitter.com/f1ashr/status/1054793845546790912/photo/1

Twitter октября 18, 06:44
Яндекс атаковал Израиль http://dlvr.it/QnhhCy

Twitter октября 17, 16:07
Как разверифицироваться в Твиттере? http://dlvr.it/Qnddsc https://twitter.com/f1ashr/status/1052591848303120390/photo/1

Twitter октября 17, 16:07
На батуте в космос http://dlvr.it/QnddrT

Мой твиттер

Копирайт

Все мысли, высказанные в блоге, являются моим мнением и за это мнение меня никто не забанит! Кроме того, никто не имеет право копировать материалы блога без использования ctrl+C/V!

© Copyright 2008