олюцернивание в избранное  msdn  новое всё   Оценить +1123x:) +-   подписка   модер. 
От: Igor Sukhov rsdn 
Дата: 14.11.06 21:34
Оценка:46 (4)
Здравствуйте, akasoft, Вы писали:

A>Здравствуйте, Igor Sukhov, Вы писали:


IS>>В общем, сделал я Lucent-based поиск для Януса — ищет очень быстро даже на ноутбуке в базе из 1.5 миллионов сообщений (450 мегабайт построенного индекса).

IS>>если интересно, могу запостить исходники.

A>Ты его интегрировал в Янус, или отдельной утилитой сделал? Индекс где хранишь? А сообщения — из ЛБД берёшь? SQL Express или Акцесс?


По порядку:

Поиск встроен в сам Янус, а точнее, старая реализация (powered by like % statement) поиска заменена на новую, гораздо более прогрессивную – понимающую русские словоформы, знающую про русские stop words и гораздо более скоростную. Смысла во внешнем поиске не вижу, так как после того как искомое сообщений найдены, часто требуется изучить всю тему, или ответить на вопрос. Очевидно, что из Януса это сделать намного проще.

Индексирование и сам поиск используют lucene.net версии 1.9.1. Исходники поискового движка взять на сайте lucene .net и обязательно убедится, что файлы в папке Analysys\RU\ проекта включены в компиляцию)

Индекс хранится в виде файлов, в специальной папочке. Если компьютер слабенький а сообщений много — папку с индексом удобно расположить рядом с файлами данных локальной базы сообщений, чтобы упростить процедуру backup-а, т.к. построение индекса с нуля может занять несколько часов. Другое дело, что если компьютер слабый а сообщений куча и вдруг у вас навернулась система и побились файлы БД – то нужно очищать карму. Вот так вот хранится индекс.

БД – понятно, что MS SQL, т.к. делал я все для себя. Реализация – новый метод в DatabaseManager и четыре совсем новых класса, один из которых – data accessor к таблице messages и,. Для Access надо сделать скопировать скопировать последний и выполнить следующее действие: replace”Sql” на “OleDb”.


Коротко, как эти 4 класса взаимодействуют с Янусом.

Класс MessageIndexer вычитывает порциями данные из БД и создает индекс из полей таблицы messages. Первичное индексирование производится по явному действию пользователя, новые же сообщения индексируются после каждой синхронизации.

Класс MessageSearcher ищет в ранее созданном индексе, вытягивает список идентификаторов сообщений, которые удовлетворяют условиям поиска. Список идентификаторов передается в DatabaseManager.GetSearchMessages3 – ну а далее все как было раньше.


Исходный код:

using System;
using System.Collections.Generic;
using System.Text;

using System.Data;
using System.Data.SqlClient;

namespace Rsdn.Janus.Search
{
    /// <summary>
    /// Класс содержит методы для получение данных об сообщениях из локальной базе сообщений..
    /// </summary>
    static class  MessageDataAccessor
    {
        static readonly string _connectionString = Config.Instance.ConnectionString;


        /// <summary>
        /// По указанному интервалу идентификаторов, считывает из БД данные из таблицы сообщений 
        /// и создает из этих данных список MessageSearchInfo объектов.
        /// </summary>
        /// <param name="firstId">Начальный Id.</param>
        /// <param name="lastId">Конечный Id.</param>
        /// <remarks>Список отсортирован по возрастанию идентификаторов сообщений.</remarks>
        /// <returns></returns>
        public static List<MessageSearchInfo> GetMessages(int firstId, int lastId)
        {
            List<MessageSearchInfo> result = new List<MessageSearchInfo>();

            using (SqlConnection connection = new SqlConnection(_connectionString))
            {
                string sqlText = "select * from [messages] where mid between @firstId and @lastId order by mid";


                using (SqlCommand command = new SqlCommand(sqlText, connection))
                {
                    command.CommandTimeout = 10000000;

                    command.Parameters.Add(new SqlParameter("@firstId", firstId));
                    command.Parameters.Add(new SqlParameter("@lastId", lastId));


                    connection.Open();

                    SqlDataReader sqlDataReader = command.ExecuteReader();


                    int iMid = sqlDataReader.GetOrdinal("mid");
                    int iAuthor = sqlDataReader.GetOrdinal("usernick");
                    int iAuthorId = sqlDataReader.GetOrdinal("uid");
                    int iSubject = sqlDataReader.GetOrdinal("subject");
                    int iText = sqlDataReader.GetOrdinal("message");
                    int iGid = sqlDataReader.GetOrdinal("gid");
                    int iDateTime = sqlDataReader.GetOrdinal("dte");
                    
                    while (sqlDataReader.Read())
                    {
                        MessageSearchInfo msi =
                        new MessageSearchInfo
                        (
                            sqlDataReader.GetInt32(iMid),

                            sqlDataReader.GetString(iAuthor),
                            sqlDataReader.GetInt32(iAuthorId),

                            sqlDataReader.GetString(iSubject),
                            sqlDataReader.IsDBNull(iText) ? "" : sqlDataReader.GetString(iText),

                            sqlDataReader.GetInt32(iGid),

                            sqlDataReader.GetDateTime(iDateTime)
                        );

                        result.Add(msi);
                    }

                }


            }


            return result;
        }

    


        /// <summary>
        /// Возвращает максимальный Id сообщения из тех что есть в локальной базе сообщений.
        /// </summary>
        /// <returns></returns>
        public static int GetMaxMessageId()
        {
            using (SqlConnection connection = new SqlConnection(_connectionString))
            {
                string sqlText = "select max(mid) from [messages]";


                using (SqlCommand command = new SqlCommand(sqlText, connection))
                {
                    connection.Open();

                    return (int)command.ExecuteScalar();
                }
            }
        }

        /// <summary>
        /// Возвращает количество сообщений в локальной базе сообщений.
        /// </summary>
        /// <returns></returns>
        public static int GetMessageCount()
        {
            using (SqlConnection connection = new SqlConnection(_connectionString))
            {
                string sqlText = "select count(mid) from [messages]";


                using (SqlCommand command = new SqlCommand(sqlText, connection))
                {
                    connection.Open();

                    return (int)command.ExecuteScalar();
                }
            }
        }

    }
}



using System;
using System.Collections.Generic;
using System.ComponentModel;

using System.Text;

using Lucene.Net.Index;
using Lucene.Net.Documents;
using Lucene.Net.Analysis;
using Lucene.Net.Analysis.Standard;
using Lucene.Net.Analysis.RU;
using Lucene.Net.Search;
using Lucene.Net.QueryParsers;

namespace Rsdn.Janus.Search
{
    /// <summary>
    /// Класс содержит набор методов для создания и обновления индекса сообщений.
    /// </summary>
    class MessageIndexer
    {
        private string _indexPath;


        /// <summary>
        /// Конструктор - инициализируем путем до папки где хранится, или будет хранится, индекс.
        /// </summary>
        /// <param name="indexPath"></param>
        public MessageIndexer(string indexPath)
        {
            _indexPath = indexPath;
        }

        /// <summary>
        /// На это событие подписываемся, чтобы отслеживать прогресс при индексации большого
        /// количества сообщений - например при первичной индексации базы.
        /// 
        /// При обработке этого события имеется возможность остановить процесс индексации.
        /// </summary>
        public event CancelEventHandler Progress;
        
        /// <summary>
        /// Обновляем индекс, добавляя в него новые, еще не проиндексированные, сообщения.
        /// </summary>
        /// <returns>Количество новых проиндексированных сообщений.</returns>
        public int UpdateIndex()
        {
            int maxMidInDb = MessageDataAccessor.GetMaxMessageId();
        
            const int step = 9999;

            int firstId         = GetMaxMidIndexer() + 1;
            int lastId          = firstId + step;
            int indexedCount    = 0;

            while (firstId <= maxMidInDb)
            {
                List<MessageSearchInfo> portion = MessageDataAccessor.GetMessages(firstId, lastId);

                // Do index
                //
                Index(portion);

                CancelEventArgs ea = new CancelEventArgs();

                if (null != Progress)
                {
                    Progress(this, ea);
                }               

                firstId         = lastId + 1;
                lastId          = firstId + step;
                indexedCount    += portion.Count;

                if (ea.Cancel)
                {
                    break;
                }
            }

            return indexedCount;
        }

        
        /// <summary>
        /// Индексируем данные списка MessageSearchInfo объектов.
        /// </summary>
        /// <param name="listMessages"></param>
        private void Index(List<MessageSearchInfo> listMessages)
        {
            IndexWriter writer;

            try
            {
                writer = new IndexWriter(_indexPath, new RussianAnalyzer(), false);
            }
            catch
            {
                writer = new IndexWriter(_indexPath, new RussianAnalyzer(), true);
            }

            foreach (MessageSearchInfo msi in listMessages)
            {
                writer.AddDocument( CreateDocument(msi) );
            }

            writer.Close();
        }

     
        /// <summary>
        /// Создаем Lucene-кий документ (объект хранящий поля индексируемой сущности).
        /// </summary>
        /// <param name="msi"></param>
        /// <returns>Созданный документ.</returns>
        private static Document CreateDocument(MessageSearchInfo msi)
        {
            Document doc = new Document();

            doc.Add(new Field("id", msi.Id.ToString(), true, true, false));

            doc.Add(Field.Text("authorNickname", msi.AuthorNickname));
            doc.Add(new Field("authorId", msi.AuthorId.ToString(), true, true, false));

            doc.Add(Field.Text("subject", msi.Subject));

            // DO NOT STORE message text in the index.
            //
            doc.Add(new Field("text", msi.Text, false, true, true));
            doc.Add(new Field("forumId", msi.ForumId.ToString(), true, true, false));

            doc.Add(new Field("creationDateTime", DateField.DateToString(msi.CreationDateTime), Field.Store.YES, Field.Index.UN_TOKENIZED));

            return doc;
        }


        /// <summary>
        /// Возвращаем количество документов в индексе
        /// (проиндексированных за все время сообщений).
        /// </summary>
        /// <returns></returns>
        public int GetDocumentCount()
        {
            try
            {
                IndexSearcher searcher = new IndexSearcher(_indexPath);

                int documentCount = searcher.MaxDoc();
              
                searcher.Close();

                return documentCount;
            }
            catch
            {
                return 0;
            }
        }


        /// <summary>
        /// Возвращает максимальный Id сообщения находящегося в индексе.
        /// </summary>
        /// <returns></returns>
        private int GetMaxMidIndexer()
        {
            try
            {
                IndexSearcher searcher = new IndexSearcher(_indexPath);

                int maxDoc = searcher.MaxDoc();
                int maxMid = 0;

                if (maxDoc > 0)
                {
                    Document maxDocument = searcher.Doc(maxDoc - 1);

                    maxMid = int.Parse(maxDocument.Get("id"));
                }

                searcher.Close();

                return maxMid;
            }
            catch
            {
                return 0;
            }
        }

       
    }
}



using System;
using System.Collections.Generic;
using System.Text;

using Lucene.Net.Index;
using Lucene.Net.Documents;
using Lucene.Net.Analysis;
using Lucene.Net.Analysis.Standard;
using Lucene.Net.Analysis.RU;
using Lucene.Net.Search;
using Lucene.Net.QueryParsers;

namespace Rsdn.Janus.Search
{
    /// <summary>
    /// Класс содержит набор методов для поиска использующего Lucent движок.
    /// </summary>
    class MessageSearcher
    {
        private readonly string _indexPath;

        /// <summary>
        /// Конструктор - инициализируем путем до папки где хранится индекс.
        /// </summary>
        /// <param name="indexPath"></param>
        public MessageSearcher(string indexPath)
        {
            _indexPath = indexPath;
        }


        /// <summary>
        /// Ищем сообщения по заданным параметрам и возвращаем список идентификаторов
        /// сообщений.
        /// </summary>
        /// <param name="query">Текст который ищем. См. формат запросов на сайте lucene.</param>
        /// <param name="searchText">Искать ли в тексте сообщений.</param>
        /// <param name="searchSubject">Искать ли в заголовках сообщений.</param>
        /// <param name="searchAuthorNickname">Искать ли в никах авторов сообщений.</param>
        /// <param name="forumId">Если -1 - искать во всех форумах, иначе искать сообщения только в указанном форуме.</param>
        /// <returns>Спиков идентификаторов сообщений (идентификаторы - строки, не числа!) удовлетворяющих условиям поиска.</returns>
        public List<string> Search(
                                    string query,

                                    bool    searchText,
                                    bool    searchSubject,
                                    bool    searchAuthorNickname,

                                    int     forumId
                                    )
        {
            List<string> result = new List<string>();

            BooleanQuery bq = new BooleanQuery();

            if (searchText)
            {
                bq.Add(QueryParser.Parse(query, "text", new RussianAnalyzer()), false, false);
            }

            if (searchSubject)
            {
                bq.Add(QueryParser.Parse(query, "subject", new RussianAnalyzer()), false, false);
            }

            if (searchAuthorNickname)
            {
                bq.Add(QueryParser.Parse(query, "authorNickname", new RussianAnalyzer()), false, false);
            }

            Filter filter = null;

       
            if (-1 != forumId)
            {
                TermQuery tq = new TermQuery(new Term("forumId", forumId.ToString()));

                filter = new QueryFilter(tq);
            }


            IndexSearcher searcher = new IndexSearcher(_indexPath);

            Hits hits = (filter != null) ? searcher.Search(bq, filter) : searcher.Search(bq);


            for (int i = 0; i < hits.Length(); i++)
            {
                Document doc = hits.Doc(i);

                result.Add(doc.Get("id"));
            }

            searcher.Close();

            return result;
        }

    }
}



using System;
using System.Collections.Generic;
using System.Text;

namespace Rsdn.Janus.Search
{
    /// <summary>
    /// DTO с полями сообщения (табл. messages в БД) из которых будем строить индекс.
    /// </summary>
    class MessageSearchInfo
    {
        /// <summary>
        /// Id сообщения (столбец uid в БД)
        /// </summary>
        public int Id;

        /// <summary>
        /// Ник автора сообщения (столбец userNick в БД)
        /// </summary>
        public string AuthorNickname;

        /// <summary>
        /// Id автора сообщения (столбец uid в БД)
        /// </summary>
        public int AuthorId;

        /// <summary>
        /// Заголовок сообщения (столбец subj в БД)
        /// </summary>
        public string Subject;

        /// <summary>
        /// Текст сообщения (столбец text в БД)
        /// </summary>
        public string Text;

        /// <summary>
        /// Id форума (столбец gid в БД)
        /// </summary>
        public int ForumId;

        /// <summary>
        /// Время создания сообщения (столбец dte в БД)
        /// </summary>
        public DateTime CreationDateTime;

        public MessageSearchInfo
            (
                int id,

                string authorNickname,
                int authorId,
            
                string subject,
                string text,

                int forumId,
                DateTime creationDateTime


            )
        {
            this.Id = id;

            this.AuthorNickname = authorNickname;
            this.AuthorId = authorId;


            this.Subject = subject;
            this.Text = text;

            this.ForumId = forumId;

            this.CreationDateTime = creationDateTime;
        }

    }
}


    public static List<LinearTreeMsg> GetSearchMessages3(
            List<string> idList,
            bool SearchInMarked,
            bool SearchInMyMessages,
            bool SearchAnyWords,
            bool SearchInQuestions,
            DateTime from,
            DateTime to,
            SortType sortType)
        {
            ArrayList sqlParams = new ArrayList();

            string strWhere = String.Empty;

            #region Обработка "моих сообщений"

            if (SearchInMyMessages)
            {
                if (strWhere.Trim().Length > 0)
                    strWhere += " AND ";

                strWhere += "([uid] = @uid)";
                sqlParams.Add("@uid"); sqlParams.Add(Config.Instance.SelfId);
            }

            #endregion

            #region Обработка вопросов

            if (SearchInQuestions)
            {
                if (strWhere.Trim().Length > 0)
                    strWhere += " AND ";

                strWhere += "([tid] = 0)";
            }

            #endregion

            #region Обработка пометок пользователя

            if (SearchInMarked)
            {
                if (strWhere.Trim().Length > 0)
                    strWhere += " AND ";

                strWhere += "([ismarked] = " + SqlBuilder.True() + ")";
            }

            #endregion

            #region Обработка дат - from, to

            if (from.Ticks != 0)
            {
                // Сбросить время, оставить только дату
                from = new DateTime(from.Year, from.Month, from.Day);

                if (strWhere.Trim().Length > 0)
                    strWhere += " AND ";

                strWhere += "([dte] >= @fromdate)";
                sqlParams.Add("@fromdate"); sqlParams.Add(from);
            }

            if (to.Ticks != 0)
            {
                // Сбросить время, оставить только дату
                to = new DateTime(to.Year, to.Month, to.Day);

                if (strWhere.Trim().Length > 0)
                    strWhere += " AND ";

                strWhere += "([dte] <= @todate)";
                sqlParams.Add("@todate"); sqlParams.Add(to);
            }

            #endregion

   
            StringBuilder sb = new StringBuilder(10 * idList.Count);

            foreach (string messageId in idList)
            {
                sb.Append(messageId).Append(",");
            }

            sb.Remove(sb.Length - 1, 1);

             if (strWhere.Trim().Length > 0)
             {
                    strWhere += " AND ";
             }


             strWhere += String.Format("mid in({0})", sb.ToString());


            string commandText = string.Format(@"
                SELECT
                    [mid],
                    [gid],
                    [tid],
                    [pid],
                    [dte],
                    [uid],
                    [usernick],
                    [uclass],
                    [subject],
                    [isread],
                    [ismarked],
                    (SELECT Sum([rate]*[rby]) FROM [rating] WHERE [mid]=m.[mid] AND [rate] > 0) AS [rate],
                    (SELECT COUNT(*) FROM [rating] WHERE [mid]=m.[mid] AND [rate] = {0})    AS [smiles],
                    (SELECT COUNT(*) FROM [rating] WHERE [mid]=m.[mid] AND [rate] = {1})    AS [agree],
                    (SELECT COUNT(*) FROM [rating] WHERE [mid]=m.[mid] AND [rate] = {2})    AS [p1rate],
                    (SELECT COUNT(*) FROM [rating] WHERE [mid]=m.[mid] AND [rate] = {3})    AS [disagree],
                    (SELECT Sum([rate]*[rby]) FROM [rating] WHERE [tid]=m.[mid] AND [rate] > 0) AS [themerate],
                    (SELECT {4} FROM [subscribed_forums] WHERE [id]=m.[gid])              AS [Forum]
                FROM [messages] m
                {5}
                {6}",
                (int)MessageRates.Smile,
                (int)MessageRates.Agree,
                (int)MessageRates.Plus1,
                (int)MessageRates.DisAgree,
                Config.Instance.ForumDisplayConfig.ShowFullForumNames ? "[descript]" : "[name]",
                strWhere.Length != 0 ? "WHERE " + strWhere : String.Empty,
                DataSorter.SortCriteria(sortType, SortType.ByIdDesc));

            return JanusDB.ExecuteList<LinearTreeMsg>(commandText, sqlParams.ToArray());
        }



Ну, вот почти и все. Хотя нет – вот что надо еще сделать, не забыл конечно добавить Lucene.Net.Dll в references проекта Janus:

Изменения в SearchDummyForm.cs (метод btnSearch_Click):

            using (ProgressFormManager pfm = new ProgressFormManager(
                    SR.Search.Searching))
            {
                List<LinearTreeMsg> res = new List<LinearTreeMsg>();

                if (searchText.Length == 0)
                {
                    res = DatabaseManager.GetSearchMessages2(
                                         (Config.Instance.AdvancedSearch ? Config.Instance.SearchForumId : -1),
                                         Config.Instance.SearchText,
                                         Config.Instance.SearchInText,
                                         Config.Instance.SearchInSubject,
                                         Config.Instance.SearchAuthor,
                                         Config.Instance.SearchInMarked,
                                         Config.Instance.SearchInMyMessages,
                                         Config.Instance.SearchAnyWord,
                                         Config.Instance.SearchInQuestions,
                                         searchFromDate.Checked ? searchFromDate.Value : new DateTime(0),
                                         searchToDate.Checked ? searchToDate.Value : new DateTime(0),
                                         Config.Instance.SearchSortCriteria);
                }
                else
                {
                    MessageSearcher searcher = new MessageSearcher(_indexPath);



                    List<string> result = searcher.Search(Config.Instance.SearchText.Trim(),
                                                            Config.Instance.SearchInText,
                                                            Config.Instance.SearchInSubject,
                                                            Config.Instance.SearchAuthor,
                                                            (Config.Instance.AdvancedSearch ? Config.Instance.SearchForumId : -1));

                    if (result.Count > 0)
                    {
                        res = DatabaseManager.GetSearchMessages3(
                                                                result,
                                                                Config.Instance.SearchInMarked,
                                                                Config.Instance.SearchInMyMessages,
                                                                Config.Instance.SearchAnyWord,
                                                                Config.Instance.SearchInQuestions,
                                                                searchFromDate.Checked ? searchFromDate.Value : new DateTime(0),
                                                                searchToDate.Checked ? searchToDate.Value : new DateTime(0),
                                                                Config.Instance.SearchSortCriteria);
                    }
                }



                if (searchInOverquoting.Checked)
                    FilterOverquoting(res);



Изменения в Synchronizer.cs (метод Sync)

                if (ApplicationManager.Instance.OutboxManager.DownloadTopics.Count > 0)
                {
                    ApplicationManager.Instance.Logger.LogInfo(SR.Sync.DownloadTopics);
                    JanusTopicRequest topicRequest = PrepareTopicRequest();
                    JanusTopicResponse topicResponse = GetTopicResponse(svc, topicRequest);
                    ProcessTopicResponse(topicResponse);
                }

                // Update index
                //
                MessageIndexer messageIndexer = new MessageIndexer(this._indexPath);

                string indexedMsg = String.Format("Проиндексировано {0} сообщений.", messageIndexer.UpdateIndex());

                ApplicationManager.Instance.Logger.Log(indexedMsg);


                OnSyncFinished();



Не смотрите на _indexPath — это обман зрения, на самом деле это должно называться Config.Instance.SearchIndexPath

Ну и напоследок — known features:

1. Начальное создание индекса – это долгий процесс, многие эстеты, в том числе считают что для этого должно иметь отдельный пункт меню, нарошный диалог с индиктором прогресса и кнопку – чтобы превать потенциально длительную операцию. Я, сумел подавить свою тягу к прекрасному. Если вы не сумеете – обратите внимание на MessageDataAccessor.GetMessageCount, MessageIndexer.Progress и Progress.GetDocumentCount

2. Если сообщение удаляют – то ссылка на него отстается в индексе. Это проблема тех кто пишет такие сообщения – я не виноват.

3. Если сообщение исправляют или переносят в другой форум (т.е. меняется или текст, заголовок, ник автора, или id форума) – то индекс в данном случае, также не обновляется. Простое решение – это искать сразу во всех форумах и не искать те слова, которые обычно удаляют модераторы. Более сложное решение – переделать логику индексирования – т.е. передавать в нее id только что полученных сообщений и в случае если в индексе уже есть сообщения с таким id – обновлять поля (см. Document.RemoveFields & Document.AddField). Экзотическое решение — это приехать обратно в Сидней, переписать БД и папку с индексом на RAID массив, удалить файлы индекса и вызвать синхронизацию чтобы пересоздать индекс с нуля. Но это конечно на любителя



Писалось в один проход, без рефакторинга — в коде возможно наличие рудиментов.
... << RSDN@Home 1.2.0 alpha rev. 0>>
* thriving in a production environment *