Рекомендованные индексы для Azure SQL

Одно из последних изменений в AzureSQL - рекомендованные индексы. Система анализирует запросы, которые выполняет база данных и предлагает индексы для их оптимизации. Как итог, все выглядит следующим образом - Микрософт придумал проблему в виде DTU, которого всегда не хватает и сам же эту проблему решает в автоматическом режиме. Система сама решает свои же проблемы. На скриншоте ниже видно, что автоматически добавленный индекс экономит мне 0,38 DTU пожирая дополнительное место. Но так как места для всех баз выделено под 250Гб, то места не жалко, можно добавлять сколько угодно индексов, если система будет считать, что она работает быстрее.


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


Visual Studio 2013 Update 3 RTM

Стала доступна MSVS2013.3 RTM, список нововведений и ссылка на скачивание тут. Пост более технический, чтобы сохранить ссылку. Про обновленный Azure SDK 2.4 читать тут.


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


Обязательные расширения для Microsoft Visual Studio 2013

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

1.ReSharper 8
Считается платным расширением, но грамотный разработчик должен уметь достать себе копию. Давно гуляет и кряки и стрельнуть у кого-то можно, либо притвориться студентом. Доставание - можно считать испытанием на проф-пригодность.

2.Nuget
В целом, про него можно забыть, так как идет по умолчанию в vs2013.

3.Web Essentials
Все что нужно для ВЕБ-разработки, ставим отсюда.

4.Regex Tester
Если надо протестировать регулярное выражение.

5.CodeMaid
Позволяет удобно "шаманить" с расположением кода и комментариями и т.п. .

6.Productivity Power Tools
По видео презентации больше похоже на Resharper для индусов. Не ставил.

7.Code Digger
Основано на Pex, позволяет определять покрытость кода тестами. Тоже качаем.

8.ozCode
Более удобная отладка. Тоже качаем.

 Остались еще красивые платные расширения, типа nDepend, но ссылки на них смотрите в английском первоисточнике. Еще может оказаться полезным проверка правописания - Spell Check  

UPD: И еще 3 полезных - Trailing Whitespace Visualizer, Error Watcher, Add empty file


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


Отключение логирования в Windows Azure

Есть такая проблема, как слишком много логов. Они по умолчанию включены в Azure и пишутся на диагностический диск. Если у вас роль маленькая, то диск этот размером в 15ГБ (+5ГБ сама система). К слову, есть такая фишка, что если заводить Extra Small Virtual Machine, а не Extra Small Web Role, то можно получить за теже 600 р в месяц диск на 120ГБ. Микрософт такое несоответствие объясняет тем, что на виртуальные машины обычно доставляют большие программы, вот и выделили места как у Small Instance.

Так вот, эти 15ГБ быстро исчерпываются, если у вас много обращений и автоматически не чистятся. Такое приводит к тому, что у роли вызывается процесс Reimage, что дает заметный перебой в работе. Чтобы в автоматическом деплое отключить логирование (конечно же для VM можно соединится по RDP и ручками настроить IIS как надо), достаточно подсоединить к проекту библиотеку Microsoft.Web.Administration, которая находится по пути C:\Windows\System32\inetsrv\Microsoft.Web.Administration.dll. А в фенкции старта самой роли прописать следующее:

Copy Source | Copy HTML
  1. var iisManager = new ServerManager();
  2. Configuration config = iisManager.GetApplicationHostConfiguration();
  3. ConfigurationSection httpLoggingSection = config.GetSection("system.webServer/httpLogging");
  4. httpLoggingSection["selectiveLogging"] = @"LogAll";
  5. httpLoggingSection["dontLog"] = true;
  6. iisManager.CommitChanges();

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


VS2013: New web developer features

Видео объясняющее все нововведения для Visual Studio 2013 для веб-разработчика. Из блога Мэда.

[video mp4="http://media.ch9.ms/ch9/3f10/11f2237c-377d-406b-9ac8-d5e5935d3f10/3-503_mid.mp4"]



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


HTML5 Video for BlogEngine

Добавил возможность загрузки видео в блоги на базе BlogEngine. Видео проигрывается при помощи новейшего HTML5 и скриптов техаского прогера MediaElementJS. Жаль что пока не работает для мобильных устройств, а то получилось бы очень удобная замена Youtube. Ниже видео танцующего медведя.

[video src="Medved.mp4" width="420" height="360"]


Метки:   Категории:BlogEngine | Csharp | Code


Azure SDK 1.7 -> 1.8 или как потерять 1000р за один апдейт

Еще в октябре прошлого года одновременно с MSVS2012 появилась версия Azure SDK 1.8 и казалось, что это чисто формальное обновление связанное с переходом на новую Visual Studio. Конечно есть и улучшения, такие как поиск по имени ошибки во многочисленных логах IntelliTrace (как можно было не догадаться сделать это с самомго начала?). Возможно рано или поздно у нас также появится средства для нормальной работы с blobstorage из самой студии. Однако, подозрения, что с 1.8 что-то не так начали появляться сразу, о чем был поднять вопрос в сообществе по рекламе Azure в России.

По опыту выяснилось,что
1) В результате автоматического обновления ссылки на библиотеки 1.8 ставятся CopyLocal = False, даже если до этого у вас стояло копирования этой библиотеки.
2) Уничтожен класс CloudConfigurationManager, отвечавший за получение настроек роли. Вместо него можно использовать RoleEnvironment.GetConfigurationSettingValue("Diagnostics.ConnectionString"); С той лишь разницей, что последняя может падать, если RoleEnvironment.IsAvailiable = false.Например, если обратиться до завершения WebRole.OnStart
3) И самое страшное проиллюстрирую следующим кодом. Заключается в различной обработке Таблицами параметров содержащих символов попадающих под UrlEncode.
Copy Source | Copy HTML
  1. string pageuri = pageElement.Attribute("uri").Value.ToLower().Replace("/default.aspx", "/");
  2. string pageuri_encoded = HttpUtility.UrlEncode(pageuri);
  3. SapePageAzure result = (from e in CreateQuery()
  4.                         where e.RowKey == pageuri_encoded &&
  5.                         e.PartitionKey == partitionid select e).FirstOrDefault();
  6. if (result == null)
  7. {
  8.     result = new SapePageAzure
  9.     {
  10.         RowKey = pageuri,
  11.         PartitionKey = partitionid,
  12.         LastAccess = DateTime.Now
  13.     };
  14.     ContainerTable.AddObject(TableName, result);
  15.     //ContainerTable.SaveChangesWithRetries();
  16. }

Если в Azure SDK1.7 нужно было использовать в обоих местах pageuri_encoded, и в выворе и во вставке. То в Azure SDK1.8 добавилась скрытая перекодировка, и получилось, что при выборе параметров из таблицы нужно использовать pageuri_encoded, а при вставке pageuri, так как теперь кодируется автоматически.
Как результат - все блоги из blogscloud.ru, которые синхронизированы с Sape и хранят набор ссылок в виде URL в таблицах, перестали корректно работать. Sape обнаруживает, что все ссылки на сайтах не отображаются и блокирует все сайты отправляя их на повторную можерацию. End of story.


PS: По случаю отзыв про tfspreview.com - возможность бесплатного использования TFS2012 для одиноких разработчиков. В общем ни в коем случае не расчитывайте на нее. Очень сильно тормозит и не дает чекинить большое количество документов. Появилось сие творение видимо потому, что все нормальные платные TFS провайдеры, такие как discountasp.net не смогли или не захотели вовремя предоставить TFS2012 для коммерческих пользователей.

Метки:   Категории:Analytics | Csharp | microsoft | Code


Контекстный поиск с русской и английской морфологией на основе проектов с открытым исходным кодом и облачных сервисов 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


Visual Studio User Voice

Существует страница, где собираются все отзывы пользователей MSVS2010 и предложения по улучшению. Там много интересного, например, можно узнать почему не существует x64 версия MSVS. Также очень мало упоминаний про Azure. Не удивительно, так как мало кто успел плотно поработать, да и сами разработчики постоянно дорабатывают интеграцию msvs с облачной платформой. Я же проголосовал за Debug Lambda expressions, хоть и редко их использую, но их работа не всегда понятна.

Метки:   Категории:Csharp | microsoft


WCF Proxy tunnel

Ниже расскажу о реализации проброса WCF запросов от одного сервера через несколько промежуточных, так называемый wcf tunneling, вопрос о реализации которого уже поднимался на gotdotnet.ru. Там автор предлагал использовать Castle.DynamicProxy для динамической генерации прокси-классов для создания канала. Однако комментаторы отметили, что полученный функционал дублирует лишь стандартные возможности при прямом взаимодействии точка-точка.
Актуальность приобретается, когда добавляется набор промежуточных точек. При этом на клиенте идет перехват вызова произвольной удаленной функции FuncName на вызов абстрактной функции object _DataTransfer(HeaderAuthen auth, object ServiceLink, object FuncName, object[] param); Далее обращение транслируется через серию промежуточных машин и в итоге обращение передается на ServiceLink, где обрабатывается. Также возвращается результат. Грубо говоря примитивный функционал Biztalk своими руками по передачи soap сообщений в многосвязной сети. Кроме того мы без ведома пользователя накладываем на канал защиту через HeaderAuthen auth.
Отдельно выкладываю реализацию класс DynamicServiceProxy и ServiceChannelManagerInterceptor, подменяющие вызов, а также сам вызов в ServiceChannelManager. Соответственно все закладывается в библиотеку, которая должна быть на всех точках, а на туннельных-промежуточных точках еще и реализуется Контранкт, в котором важно описать передаточную функцию _DataTransfer, все функции на конечных точках, а также все классы и типы через KnownTypeContainer. Также важно включить сериализатор XmlSerializerFormat, так как по умолчанию в WCF используется DataContractSerializer, которых не позволяет передавать object. То есть вызовы проходить будут, а данные - нет. Кроме того у XmlSerializerFormat тоже есть особенность, кто нельзя передать некоторые стандартные типы, например, DataSet как параметр, так как в них используется нетипизированный ArrayList. Вот пожалуй и все, что нужно знать.

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


Программисты...

...негры нашего времени. За прошлый 2010 год имел неприятность переработать с различными типами программистов .Net, так как сам программирую на Chsarp. Ниже своеобразный обзор о печальной картине подрастающего поколения, выводы и может в следующем посте приведу перевод одной интересной книги о том, как искать хороших программистов.
Итак, задачу я ставил достаточно сложно и общно, описывая что должно работать глазами пользователя и разрешая задавать мне вопросы по хожу и ограничивая в зарплате рамками разумного. Что получилось или входные данные:
  • Первый тип. Женат. Москва. В соц-сетях время не тратит. Ведущий разработчик мелкого коммерческого предприятия. Имел хорошие практические навыки и требовательно относился к коду по части реализации, для работы с данными переваривал только Linq. Не справился, нехватка времени.
  • Второй. Женат. Москва. Сидит вконтакте. Ведущий разработчик в около-государственной структуре. Имел плохие практические навыки, по образованию - программист. Не справился, по качеству реализации.
  • Третий. Холост. Приехал в Москву на учебу-заработки. Сидит в Твиттере, пишет статьи на хабр. Имел средние практические навыки, по образованию - программист. Не справился, нехватка времени.
  • Четвертый. Женат. Москва. Сидит в Живом Журнале. Имел обширные практические навыки, по образованию - программист. Реализовывал все по теории - от абстрактной модели. Не справился, по качеству реализации.
  • Пятый. Холост. Не Москва. Не сидит в соц.сетях. Имел плохие практические навыки. В целом справляется, делает не совсем то, как надо, но на замечания, что это нужно переделать потому-то, реагирует нормально, потому что если не справится, то ему в Замкадье будет нечего кушать.

И вот если у меня с программистами такие проблемы возникают, то какие же проблемы возникают у различных гуманитариев, которые далеки от программирования. Поэтому ниже небольшой список хороших черт для наемного программиста, который будет на вас вкалывать как негр и сделает то, что надо:
  • Холост. Говорит о том, что у него есть время работать.
  • Не сидит в соц.сетях и не пишет статьи о том как надо программировать. Ибо одно дело учить, а другое дело реализовывать.
  • По образованию технарь, но не специализировался на программировании в вузе. Ему должно быть наплевать на теоретическую базу, а волновать только то, что написано в документации и как это сделать побыстрее, да чтобы работало.
  • Не живет в Москве.
  • Уже делал несколько других проектов в том числе и для себя. То есть само-обучаем!

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

Метки:   Категории:Csharp | life


json for .net

Всегда не любил выдачу JSON за то, что она подразумевает обработку в JS на стороне клиента, а значит много гемора для программиста. Но оказывается существует уже и полноценное решение делающее JSON не сложнее обычной XML сериализации. Речь про newtonjson.dll, написанную на .Net и неплохо документированную. Отдельно порадовала возможность конвертации конечного листа дерева в произвольный тип путем простого вызова типа cursor["leafname"].Value<long>();. Вот бы все значения ячеек для датагридов тоже имели такой шаблонизатор, чтобы не прописывать каждый раз приведение типа.

Пример JSON сериализатора в .Net для результатов выдаваемых гуглом при поисковых запросах через AJAX API - можно найти в последних изменениях blogsapi

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

Метки:   Категории:BlogsAPI | Csharp | Code


How to RSS your data

Что такое RSS большинство должно знать - это форма экспорта текстовых данных с сайта. Лично меня в RSS не устраивает то, что нельзя задать идентификатор сообщения уникальный для моего поста в рамках сегодняшнего дня, а не в рамках ресурса, за что отвечает поле guid. Это нужно для того, чтобы RSS агрегатор, типа reader.google.com, мог понять, что эти 3 новости перекопированные на разные сайты - есть одно и тоже. Например новости Яндекса копируются на хабр и на яру, а у меня из-за этого в 3 раза больше сообщений. Введение такого хэш-идентификатора "на день" вполне оправдано. И согласуется с тем, как работают сервисы сокращения ссылок, замещая старые короткие ссылки, новыми значениями, что позволяет короткой ссылке оставаться короткой.

Другая интересная несогласованность проявляется при попытке создания RSS выдачи при помощи сериализации объекта класса (рекомендую библиотеку RssToolKit 2.0 для этого). Как мы знаем все строки в CSharp храняться в виде UTF-16, поэтому при сериализации мы получаем XML в этой кодировке, жестко указанной в заголовке. Но, как оказалось, некоторые браузеры - FireFox, IE8 (в Opera все впорядке) - наотрез отказываются воспринимать такой XML, требуя UTF-8 и жесткого задания версии RSS, поэтому пришлось дописать RssToolKit, добавив преобразование кодировок следующим образом:
Copy Source | Copy HTML
  1. /// <summary>
  2. /// Returns XML of the Generic Type.
  3. /// </summary>
  4. /// <param name="rssDocument">The RSS document.</param>
  5. /// <typeparam name="T">RssDocumentBase</typeparam>
  6. /// <returns>string</returns>
  7. public static string ToRssXml<T>(T rssDocument) where T : RssDocumentBase
  8. {
  9.     if (rssDocument == null)
  10.     {
  11.         throw new ArgumentNullException("rssDocument");
  12.     }
  13.  
  14.     MemoryStream memoryStream = new MemoryStream();
  15.     String XmlizedString = null;
  16.     using (XmlTextWriter output = new XmlTextWriter(memoryStream, Encoding.UTF8))
  17.     {
  18.  
  19.         XmlSerializer serializer = new XmlSerializer(typeof(T));
  20.         serializer.Serialize(output, rssDocument);
  21.         memoryStream = (MemoryStream)output.BaseStream;
  22.         XmlizedString = UTF8ByteArrayToString(memoryStream.ToArray());
  23.         return XmlizedString;
  24.     }
  25. }
  26.  
  27. /// <summary>
  28. /// To convert a Byte Array of Unicode values (UTF-8 encoded) to a complete String.
  29. /// </summary>
  30. /// <param name="characters">Unicode Byte Array to be converted to String</param>
  31. /// <returns>String converted from Unicode Byte Array</returns>
  32. private static String UTF8ByteArrayToString(Byte[] characters)
  33. {
  34.     UTF8Encoding encoding = new UTF8Encoding();
  35.     String constructedString = encoding.GetString(characters);
  36.     return (constructedString);
  37. }
После этого создание RSS asp-сайта занимает несколько строчек:
Copy Source | Copy HTML
  1. //говорим, что ответом есть XML/RSS
  2. Response.ContentType = "application/rss+xml";
  3. //вытаскиваем из ьазы данные
  4. using (MySQL db = new MySQL())
  5. {
  6.     DataSet ds = db.GetData("get_rss");
  7.     if (ds != null && ds.Tables[0].Rows.Count > 0)
  8.     {
  9.         RssDocument rss = new RssDocument()
  10.         {
  11.             Version = "2.0",
  12.             Channel = new RssChannel()
  13.             {
  14.                 LastBuildDate = DateTime.Now.ToString(),
  15.                 Language = "ru-RU",
  16.                 WebMaster = "topbot@ya.ru",
  17.             }
  18.         };
  19.         rss.Channel.Title = "blabla"
  20.         rss.Channel.Link = ds.Tables[0].Rows[0]["linkto"].ToString();
  21.         rss.Channel.Items = new List<RssItem>(0);
  22.         foreach (DataRow dr in ds.Tables[0].Rows)
  23.         {
  24.             RssItem ritem = new RssItem()
  25.             {
  26.                 PubDate = ((DateTime)dr["when"]).ToString("s"),
  27.                 Description = dr["text"].ToString()
  28.             };
  29.             rss.Channel.Items.Add(ritem);
  30.         }
  31.         Response.Write(rss.ToXml(DocumentType.Rss));
  32.     }
  33.     else
  34.     {
  35.         Response.ContentType = "application/rss+xml";
  36.     }
  37.  
  38. }

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


Скриншоты сайтов на .Net

Как оказалось делать скриншоты сайтов не такая уж и простая задача. Но выход из нее можно найти создавая отдельным потоком скрытую форму с объектом WebBrowser и используя некоторые компоненты MS IE7+.
Ниже идет малокомментированный код, который подскажет Вам, как это правильнее всего сделать. Если есть вопросы, то гуглите и разбирайтесь в документации:
Copy Source | Copy HTML
  1. #region private event handlers
  2.  
  3.  
  4.     /// <summary>
  5.     /// События результата открытия страницы в браузере _webBrowser.Navigate(url.OriginalString);
  6.     /// </summary>
  7.     private unsafe void OnDocumentCompleted(object sender, WebBrowserDocumentCompletedEventArgs e)
  8.     {
  9.         WebBrowser browser = (sender as WebBrowser);
  10.         if (browser != null)
  11.         {
  12.             if(!browser.DocumentText.Contains("<!--Error Body-->"))//если открылось
  13.             {
  14.                 int width = browser.Document.Body.ScrollRectangle.Size.Width;
  15.                 int height = browser.Document.Body.ScrollRectangle.Size.Height;
  16.                 this.Size = browser.Document.Body.ScrollRectangle.Size;
  17.                 this.ClientSize = browser.Document.Body.ScrollRectangle.Size;
  18.  
  19.                 this.Height = 10;
  20.                 this.Width = 20;
  21.                 browser.Width = width;
  22.                 this.Top = -200;
  23.                 browser.Top = -100;
  24.                 browser.Height = 5000;//height;
  25.                 panel.AutoScrollPosition = new Point(300, 300);
  26.                 this.Refresh();
  27.                 //повторный рендеринг страницы тут нужен
  28.                 back.RunWorkerAsync();
  29.             }
  30.         }
  31.     }
  32.  
  33.     void back_RunWorkerCompleted(object sender, RunWorkerCompletedEventArgs e)
  34.     {
  35.         WebBrowser browser = this._webBrowser;
  36.         if (browser != null)
  37.         {
  38.             if (!browser.DocumentText.Contains("<!--Error Body-->"))
  39.             {
  40.                 int width = browser.Document.Body.ScrollRectangle.Size.Width;
  41.                 int height = browser.Document.Body.ScrollRectangle.Size.Height;
  42.                 //объект из IE7+
  43.                 mshtml.IHTMLDocument2 document = (browser.Document.DomDocument as mshtml.IHTMLDocument2);
  44.  
  45.                 if (document != null)
  46.                 {
  47.                     mshtml.IHTMLElement element = (document.body as mshtml.IHTMLElement);
  48.                     if (element != null)
  49.                     {
  50.                         IHTMLElementRender render = (element as IHTMLElementRender);
  51.                         if (render != null)
  52.                         {
  53.                             Image img = new Bitmap(width, height);
  54.                             using (Graphics graphics = Graphics.FromImage(img))
  55.                             {
  56.                                 IntPtr hdcDestination = graphics.GetHdc();
  57.                                 render.DrawToDC(hdcDestination);
  58.                                 graphics.ReleaseHdc(hdcDestination);
  59.                               //  browser.DrawToBitmap((Bitmap)img, new Rectangle(0, 0, width, height));
  60.                                 string FileName = wheretosave;
  61.                                 _webBrowser.DrawToBitmap((Bitmap)img, new Rectangle(0, 0, width, height));
  62.                                 img.Save(FileName);
  63.                             }
  64.                         }
  65.                     }
  66.                 }
  67.             }
  68.         }
  69.         this.Close();
  70.     }
  71.  
  72.     void back_DoWork(object sender, DoWorkEventArgs e)
  73.     {
  74.         Thread.Sleep(5000);
  75.     }
  76.  
  77.     #endregion

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


1C:Предприятие.

По просьбам пользователей добавил в проекте http://s-c.me подсветку кода для "1С". Заодно изучил. С одной стороны очень похоже на переведенный CSharp, а с другой стороны одного взгляда на этот язык достаточно, чтобы почувствовать особую энергетику (Как энергетика продуктов развалившегося Автоваза). Понимаешь, что с помощью этого языка можно делать такие вещи, которые бы тебе в голову не пришло делать. Например, проверку ИНН номера на корректность, но с другой стороны это просто жизненно необходимая вещь для некоторых (Как возможность открутить руль у шестерки и унести его с собою, чтобы не угнали).

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

Пример подсветки языка 1с:
Copy Source | Copy HTML
Запрос = Новый Запрос(ТекстЗапроса);
Результат = Запрос.Выполнить(); ТаблицаРезультатов = Результат.Выгрузить();
 
Текст = Новый ЗаписьТекста(ПутьФайла, КодировкаТекста.ANSI);
Для i = 0 ПО ТаблицаРезультатов.Количество() - 1 Цикл
  Стр = "";
  Для j = 0 По ТаблицаРезультатов.Колонки.Количество() - 1 Цикл
    Стр = Стр + ТаблицаРезультатов[i][j] + ";"
    Текст.ЗаписатьСтроку(Лев(Стр, СтрДлина(Стр) - 1));
  КонецЦикла;
КонецЦикла;
Текст.Закрыть();

Метки:   Категории:S-c.me | Csharp


PDF / DOC to TXT

При индексировании архива офисных файлов мне понадобилось извлекать текстовую информацию из различных файлов таких как DOC или PDF. Решение несложное, достаточно воспользоваться несколькими библиотеками. Для PDF - это PDFBox, портированный с Java на CSharp. Для DOC - это Microsoft.Office.Interop.Word.dll из набора OfficeAPI. Для удобства, необходимые библиотеки можно скачать одним архивов по ссылке

Под катом я приведу код, показывающего как с ними работать.
Начнем с PDF, подключим имена:
Copy Source | Copy HTML
using org.pdfbox.pdmodel;//PDF library
using org.pdfbox.util;
После этого документ можно преобразовать в текст 3мя строчками:
Copy Source | Copy HTML
PDDocument pdfFile = PDDocument.load(onefile);
PDFTextStripper stripper = new PDFTextStripper();
String innertext = stripper.getText(pdfFile);

Для DOC больше кода, поэтому я вынес все в отдельный класс, производя конвертирование как innertext = ConvertDoc.GetDocText(onefile);
Copy Source | Copy HTML
using Microsoft.Office.Interop.Word;
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Reflection;
using System.Text;
 
/// <summary>
/// объект для операция с Вордовыми документами
/// </summary>    
public class ConvertDoc
{
    // Объект отвечающий за результат конвертации.
    // Ставим .txt - текстовый в досовской кодировке
    static ConvConfig tmpItem = new ConvConfig(".txt", WdSaveFormat.wdFormatDOSText);
 
    /// <summary>
    /// Получение текста из доковского файла
    /// </summary>
    /// <param name="file">Ссылка на документ</param>
    /// <returns>Текст документа</returns>
    public static String GetDocText(String inFileName)
    {
        String retText = String.Empty;
        //создаем объекты для дальнейшей передачи в вордовые функции
        object fileName = inFileName;
        object fileSaveName = Path.GetTempFileName() + tmpItem.itemExtension; //случайное уникальное имя + .txt;
        object vk_read_only = true;
        object vk_visible = false;
        object vk_true = true;
        object vk_false = false;
        object vk_dynamic = 2;
        object vk_saveformat = tmpItem.itemWord;
 
        object missing = Missing.Value;//все параметры, которые мы пропустим
        /////
        object vk_range = missing;
        object vk_to = missing;
        object vk_from = missing;
        /////
        // создаем класс для работы с вордом
        ApplicationClass vk_word_app = new ApplicationClass();
 
        Document aDoc = null;
        try
        {
            // вызываем скрытое открытие документа 
            aDoc = vk_word_app.Documents.Open(
                     ref fileName, ref missing,
                     ref vk_read_only, ref vk_false,
                     ref missing, ref missing,
                     ref missing, ref missing,
                     ref missing, ref missing,
                     ref missing, ref vk_visible,
                     ref missing, ref missing,
                     ref missing);
 
            //Сохраняем в файл в заданном формате
            aDoc.SaveAs(ref fileSaveName, ref vk_saveformat,
                 ref missing, ref missing, ref missing, ref missing,
                 ref missing, ref missing, ref missing, ref missing,
                 ref missing, ref missing, ref missing, ref missing,
                 ref missing, ref missing);
            //корректно закрывает все открытое
            if (aDoc != null)
            {
                aDoc.Close(ref vk_false, ref missing, ref missing);
            }
            //читаем из временного файла в досовской кодировке и удаляем его
            retText = File.ReadAllText(fileSaveName.ToString(), Encoding.GetEncoding(866));
            File.Delete(fileSaveName.ToString());
        }
        catch (Exception err)
        {
            Debug.WriteLine("Ошибка чтения файла: " + inFileName + " - " + err.Message);
        }
        finally
        {
            vk_word_app.Quit(ref vk_false, ref missing, ref missing);
        }
        return retText;
    }
}
 
/// <summary>
/// Настройки для конвертации
/// </summary>
public class ConvConfig
{
    //расширение
    public String itemExtension;
    //тип объекта на выдаче
    public Object itemWord;
 
    //The constructors
    public ConvConfig() { }
 
    public ConvConfig(String inExtension, Object inWordType)
    {
        // This allows us to set the properties 
        itemExtension = inExtension;
        itemWord = inWordType;
    }
}
 

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


.Net - Контексный поиск с учетом русской и английской морфологии

Подведу итог моим экспериментам с библиотекой Lucene.Net, под катом немало кода.

Для чего это: Поиск документов по словам с учетом морфологических модификаций в большой базе документов
Программный язык: CSharp.
Потрачено времени: Двое суток
Использованные продукты: MSVS, Lucene.Net, Lemmanizer с дополнительным английским словарем.
Ограничения и лицензии: Apache2.0 для Lucene и 250y.e. за Lemmanizer или бесплатная демо до конца года.
Дополнительно: Немало полезного есть на CodeProject, а также пример простого поиска описывали в RSDN

Начну с результатов
1) Индекс на 1530 текстовых документов занял 40минут, и 1,4мегабайта на диске. Если отключить морфологию, то те же действия выполняются за 1 минуту.
2) Иллюстрация поиска, который выполняется почти моментально

Видно, что по слову authority и authorities найдено одинаковое число документов, при отключении морфологии их число разнится
Одинаковое число документов для Бандитко и Чичваркина вызвано тем, что в xml настройке для SynonymFilter я указал эти слова в качестве синонимов.


Итак, наша задача при написании контекстного поиска с учетом морфологии с использованием Lucene.Net сводиться к описанию класса по разбору текста и вставлению в него всех слов, которые как-то коррелируют с текущим. Я написал следующий класс
Copy Source | Copy HTML
  1. namespace Lucene.Net.Analysis.Morphology
  2. {
  3.     using System;
  4.     using System.Data;
  5.     using System.Configuration;
  6.     using System.Web;
  7.     using System.IO;
  8.     using System.Web.Security;
  9.     using System.Web.UI;
  10.     using System.Web.UI.WebControls;
  11.     using System.Web.UI.WebControls.WebParts;
  12.     using System.Web.UI.HtmlControls;
  13.     using Lucene.Net.Analysis.Standard;
  14.     using Lucene.Net.SynonymEngine;
  15.  
  16.     /// <summary>
  17.     /// Анализатор текста для Lucene.Net с использованием морфологического анализа и словаря синонимов
  18.     /// </summary>
  19.     public class MorphologyAnalyzer : Analyzer
  20.     {
  21.         public MorphologyAnalyzer(ISynonymEngine engine)
  22.         {
  23.             //запомнили откуда брать синонимы
  24.             SynonymEngine = engine;
  25.             isSearchQuery = false;
  26.             //грузим словари
  27.             Morph = new MorphENRU();
  28.         }
  29.  
  30.         private MorphENRU Morph;
  31.         private ISynonymEngine _SynonymEngine;
  32.         private Boolean isSearchQuery;
  33.  
  34.         /// <summary>
  35.         /// Объект для перечисления синонимов слова
  36.         /// </summary>
  37.         public ISynonymEngine SynonymEngine {
  38.             get {
  39.                 return _SynonymEngine;
  40.             }
  41.             private set
  42.             {
  43.                 _SynonymEngine = value;
  44.             }
  45.         }
  46.  
  47.         /// <summary>
  48.         /// Выключаем проверку синонимов
  49.         /// </summary>
  50.         /// <returns></returns>
  51.         public MorphologyAnalyzer SetQuerySynonym()
  52.         {
  53.             this.isSearchQuery = true;
  54.             return this;
  55.         }
  56.  
  57.         /// <summary>
  58.         /// Анализуем текст и возвращаем в виде нобора слов для помещения в индекс и подсчета частот
  59.         /// </summary>
  60.         /// <param name="fieldName">Имя индексируемого файла</param>
  61.         /// <param name="reader">Поток для чтнения файла</param>
  62.         /// <returns>TokenStream</returns>
  63.         public override TokenStream TokenStream(string fieldName, TextReader reader)
  64.         {
  65.             //создаем обход слов
  66.             TokenStream result = new StandardTokenizer(reader);
  67.  
  68.             //преобразуем исходную строчку
  69.             result = new StandardFilter(result); // выделение слов при помощи StandardTokenizer
  70.             result = new LowerCaseFilter(result);// Приведение к нижнему регистру
  71.  
  72.             // простой фильтр английских местоимений,
  73.             // русских, к сожалению нету
  74.             result = new StopFilter(result, StopAnalyzer.ENGLISH_STOP_WORDS);
  75.             result = new MorphFilter(result, Morph); // вставляем морфологическую модификацию
  76.             result = new SynonymFilter(result, SynonymEngine, this.isSearchQuery); // вставляем синонимы
  77.  
  78.             //возвращаем набор токенов для помещения в индекс
  79.             return result;
  80.         }
  81.     }
  82.  
  83. }

Видно, что кроме стандартных фильтров, я добавил MorphFilter - вставка морфологических модификаций и SynonymFilter - для вставки синонимов. Класс MorphENRU используется для работы со словарями морфологии на базе библиотеки Lemmanizer и идет ниже
Copy Source | Copy HTML
  1. namespace Lucene.Net.Analysis.Morphology
  2. {
  3.     using LEMMATIZERLib;//морфология
  4.     using System;
  5.     using System.Collections.Generic;
  6.     using System.Configuration;
  7.     using System.Data;
  8.     using System.Diagnostics;
  9.     using System.IO;
  10.     using System.Text;
  11.     using System.Text.RegularExpressions;
  12.     using System.Web;
  13.     using System.Web.Security;
  14.     using System.Web.UI;
  15.     using System.Web.UI.WebControls;
  16.     using System.Web.UI.WebControls.WebParts;
  17.     using System.Web.UI.HtmlControls;
  18.  
  19.     /// <summary>
  20.     /// Работа с морфологией для анг. и русского языка
  21.     /// </summary>
  22.     public class MorphENRU
  23.     {
  24.         /// <summary>
  25.         /// Регулярное выражение для определения английских слов
  26.         /// </summary>
  27.         private Regex rWordEn = new Regex(@"[a-z0-9]+", RegexOptions.Singleline | RegexOptions.Compiled
  28.             | RegexOptions.ExplicitCapture);
  29.  
  30.         /// <summary>Анализатор английских слов</summary>
  31.         private ILemmatizer Lemmatizer_en;
  32.  
  33.         /// <summary>
  34.         /// Анализатор русских слов
  35.         /// </summary>
  36.         private ILemmatizer Lemmatizer_ru;
  37.  
  38.  
  39.         public MorphENRU()
  40.         {
  41.             //в конструкторе нужно загрузить морфологию
  42.             try
  43.             {
  44.                 Lemmatizer_ru = new LemmatizerRussianClass();
  45.                 Lemmatizer_ru.LoadDictionariesRegistry();
  46.                 Lemmatizer_en = new LemmatizerEnglishClass();
  47.                 Lemmatizer_en.LoadDictionariesRegistry();
  48.                 Debug.WriteLine("Загрузка морфологи успешно завершена.");
  49.             }
  50.             catch (Exception e)
  51.             {
  52.                 Debug.WriteLine("Ошибка при открытиии морфологического словаря: " + e.Message);
  53.                 //ошибка может быть по многим причинам - например,
  54.                 //кто-то удалил файлы словарей или истекла лицензия,
  55.                 //тогда игнорируем морфологию
  56.                 Lemmatizer_ru = null;
  57.                 Lemmatizer_en = null;
  58.             }
  59.         }
  60.  
  61.         /// <summary>
  62.         /// Делаем слово морфологически инвариантным
  63.         /// </summary>
  64.         /// <param name="word"></param>
  65.         /// <returns></returns>
  66.         public List<string> NormalizeWord(string word)
  67.         {
  68.             List<string> WordList = null;
  69.             if (Lemmatizer_ru != null &&
  70.     Lemmatizer_en != null)
  71.             {
  72.                 //если ошибка в словарях, то пропускаем обработку
  73.                 int weight = -1;
  74.                 bool isEng = this.rWordEn.Match(word).Success;//английское или русское слово
  75.                 // ищем варианты в словаре
  76.                 IParadigmCollection ParadigmCollection =
  77.                     isEng ?
  78.                     Lemmatizer_en.CreateParadigmCollectionFromForm(word, 1, 1) :
  79.                     Lemmatizer_ru.CreateParadigmCollectionFromForm(word, 1, 1);
  80.                 // выбираем наиболее тяжелое по весу
  81.                 for (int j = 0; j < ParadigmCollection.Count; j++)
  82.                 {
  83.                     if (ParadigmCollection[j].WordWeight > weight)
  84.                     {
  85.                         if (ParadigmCollection[j].Norm == "ДЛИТЬ") continue;
  86.                         //будем брать всего одно самое весовое слово для морфологии
  87.                         if (WordList == null)
  88.                         {
  89.                             WordList = new List<string>();
  90.                             WordList.Add(ParadigmCollection[j].Norm.ToLower());
  91.                         }
  92.                         else
  93.                         {
  94.                             WordList[0] = ParadigmCollection[j].Norm.ToLower();
  95.                         }
  96.                         weight = ParadigmCollection[j].WordWeight;
  97.                         //графемы нам не нужны
  98.                         //gramma = this.ParadigmCollection[j].SrcAncode;
  99.                     }
  100.                 }
  101.             }
  102.             return WordList;
  103.         }
  104.     }
  105. }


В функции NormalizeWord видно, что для каждого слова мы берем только ту его морфологическую модификацию, которая имеет наибольший вес. Можно было бы брать больше слов, но тогда размер индекса сильно бы увеличился. Кроме того, на этапе фильтра MorphFilter мы игнорируем все слова с длинною меньше 4х символов, чтобы ускорить работу. Сам класс MorphFilter приведен ниже
Copy Source | Copy HTML
  1. namespace Lucene.Net.Analysis.Morphology
  2. {
  3.     using System;
  4.     using System.Data;
  5.     using System.Configuration;
  6.     using System.Collections.Generic;
  7.     using System.Web;
  8.     using System.Web.Security;
  9.     using System.Web.UI;
  10.     using System.Web.UI.WebControls;
  11.     using System.Web.UI.WebControls.WebParts;
  12.     using System.Web.UI.HtmlControls;
  13.     using Lucene.Net.Analysis;
  14.  
  15.     /// <summary>
  16.     /// Обход набора слов и вставка морфологических модификаций
  17.     /// </summary>
  18.     public class MorphFilter : TokenFilter
  19.     {
  20.         private Queue<Token> morphTokenQueue
  21.             = new Queue<Token>();
  22.  
  23.         private MorphENRU _MorphEngine;
  24.         public MorphENRU MorphEngine { get { return _MorphEngine; } private set { _MorphEngine = value; } }
  25.  
  26.         public MorphFilter(TokenStream input, MorphENRU morphEngine)
  27.             : base(input)
  28.         {
  29.             if (morphEngine == null)
  30.                 throw new ArgumentNullException("morphEngine");
  31.  
  32.             MorphEngine = morphEngine;
  33.         }
  34.  
  35.         public override Token Next()
  36.         {
  37.             // Если есть слова в очереди, то надо их поместить в поток прежде чем одти дальше
  38.             if (morphTokenQueue.Count > 0)
  39.             {
  40.                 return morphTokenQueue.Dequeue();
  41.             }
  42.  
  43.             //Берем след. слово из текста
  44.             Token t = input.Next();
  45.  
  46.             //если пусто, то конец потока
  47.             if (t == null)
  48.                 return null;
  49.  
  50.             //разбор морфологии только для слов длинною более 4х символов
  51.             if (t.TermText().Length > 4)
  52.             {
  53.  
  54.                 //получение актуальных морфологий
  55.                 IEnumerable<string> mWords = MorphEngine.NormalizeWord(t.TermText());
  56.  
  57.                 //если нет слов то вернем слово просто
  58.                 if (mWords != null)
  59.                 {
  60.                     //Переберем все морфологические формы которые более употребляемы
  61.                     foreach (string word in mWords)
  62.                     {
  63.                         //убедимся, что не дублируем слово
  64.                         if (!t.TermText().Equals(word))
  65.                         {
  66.                             //делаем морфологический токен
  67.                             Token mToken = new Token(word, t.StartOffset(), t.EndOffset(), "<MORPH>");
  68.  
  69.                             // устанавливаем относительное смещение в 0,
  70.                             // это нужно, чтобы отразить то, что добавляемое слово соответствует
  71.                             // старому месту в изначальном тексте
  72.                             mToken.SetPositionIncrement(0);
  73.  
  74.                             //помещаем в очередь на помещение в поток
  75.                             morphTokenQueue.Enqueue(mToken);
  76.                         }
  77.                     }
  78.                 }
  79.             }
  80.             //
  81.             return t;
  82.         }
  83.     }
  84.  
  85. }
Теперь можно использовать объект MorphologyAnalyzer в качестве аргумента для стандартных функций индексации и поиска Lucene.Net библиотеки. Пример, стандартного использования Lucene.Net находится по ссылке на RSDN в начале поста.


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


Кто я?

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

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

Topbot at FeedsBurner

Мои Твиты

Twitter сентября 24, 19:17
@SielenaSemper по какой причине эта трансляция более недоступна?

Twitter сентября 23, 14:26
Разбор сериала "Конец детства" http://dlvr.it/Ppxjly https://twitter.com/f1ashr/status/911597559604420608/photo/1

Twitter сентября 22, 17:41
Спустя 18 лет в ЖЖ запустили поиск по постам http://dlvr.it/PpmtD1

Twitter сентября 22, 16:01
Интернет ищет инопланетян (экзопланеты) http://dlvr.it/PpltJF

Twitter сентября 22, 16:01
dlvrit вводит лимиты http://dlvr.it/PpltJf

Twitter сентября 22, 14:24
Iron Sky 3 - новый трейлер на 9 мая http://dlvr.it/PpkrvS https://twitter.com/f1ashr/status/911234743747805184/photo/1

Twitter сентября 22, 14:24
Про терракты в Лондоне http://dlvr.it/Ppkrv7

Twitter сентября 22, 14:24
Коломенский кремль: Альтернативная история http://dlvr.it/Ppkrrx https://twitter.com/f1ashr/status/911234723476676608/photo/1

Twitter сентября 22, 14:24
Паштет из мяса медведя с брусникой http://dlvr.it/PpkrcB https://twitter.com/f1ashr/status/911234712026234882/photo/1

Twitter сентября 22, 14:24
Рейтинг каналов и ботов Телеграмм http://dlvr.it/PpkrQw https://twitter.com/f1ashr/status/911234670553063424/photo/1

Twitter сентября 22, 14:24
Facebook опять забанил http://dlvr.it/PpkrPG

Twitter сентября 22, 14:24
Звездные войны Изгой Один http://dlvr.it/PpkrDL https://twitter.com/f1ashr/status/911234655310905344/photo/1

Twitter сентября 21, 15:26
Робот играющий в гольф http://dlvr.it/PpWS1G

Twitter сентября 21, 14:53
Мобильное приложение t30p.ru в AppStore http://dlvr.it/PpW5Qk

Twitter сентября 21, 14:53
Что будет 22 сентября? http://dlvr.it/PpW5Lb https://twitter.com/f1ashr/status/910879685042618368/photo/1

Twitter сентября 21, 14:20
Битва за Мосул в 360 с вертолета http://dlvr.it/PpVlXD https://twitter.com/f1ashr/status/910871392970608640/photo/1

Twitter сентября 21, 14:20
Очередной летающий электро-автомобиль http://dlvr.it/PpVlWl https://twitter.com/f1ashr/status/910871386507186177/photo/1

Twitter сентября 21, 14:20
Яндекс атаковал Израиль http://dlvr.it/PpVlX9

Twitter сентября 21, 14:20
Мелькает число 35 в международных новостях http://dlvr.it/PpVlWZ

Twitter сентября 21, 14:20
Информационные аккаунты в Твиттере http://dlvr.it/PpVlWF

Мой твиттер

Копирайт

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

© Copyright 2008