?

Log in

No account? Create an account
Dotnet_am

dotnet_am


C# и платформа .NET в вопросах и ответах

.NET технологии в Армении


Previous Entry Share Next Entry
4.1.1 Как организовать поиск по сайту
Dotnet_am
dotnet_am

Можно использовать возможности предоставляемые поисковыми системами, например Google search.
Можно использовать готовые решения , такие как EasySearchASP.NET.
Можно даже включить Microsoft Indexing Service на сервере и программно получать от нее данные.

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

Ниже код страницы, которая эту задачу решает.  Для поиска нужно перейти по ссылке на странице, передав, в параметре text, то что надо искать.  

Например:
Если страница поиска называется  SearchResults.aspx , а ищется слово test, то  Response.Redirect("SearchResults.aspx?text=test" ) покажет страницу с результатами поиска.

using System;
using System.Collections.Generic;
using System.Web;
using System.Web.UI;
using System.Web.UI.WebControls;
using System.Collections;
using System.IO;
using System.Text;
using System.Text.RegularExpressions;


public partial class SearchResults : System.Web.UI.Page
{
    protected void Page_Load(object sender, EventArgs e)
    {
        string strText = this.Request.QueryString["text"].ToString().Trim();
        string[] strExtensions = { "*.htm", "*.html", "*.aspx" };
        litSearchResults.Text = this.SearchSite(strText, this.Server.MapPath(""), strExtensions);
    }


    public string SearchSite(string strText, string path, string[] extensions)
    {   
        // constants
        const int MAX_PAGES_TO_SEARCH = 10000;
        const int NIMBER_OF_OCCURENCIES_TO_DISPLAY_PER_PAGE = 7;
        //this is variable for full HTML result of the search
        StringBuilder sbRes = new StringBuilder();
        StringBuilder sbTempRes = new StringBuilder();
        string strTitle;
        // Generic SortedList with ReverseComparer to sort results by relevance (max occurences first)
        SortedList<int, string> listResults = new SortedList<int, string>(new ReverseComparer<int>());
        //regular expression to remove HTML tags
        Regex rex = new Regex("<[^>]*>");
        //regular expression to get page title
        Regex rex2 = new Regex("<title>(.*)</title>", RegexOptions.IgnoreCase);
        int nFound = 0; //count of pages found
        DirectoryInfo di = new DirectoryInfo(path);
        foreach (string ext in extensions) // for each file extension
        {
            foreach (FileInfo f in di.GetFiles(ext, SearchOption.AllDirectories))
            {
                // ressolve full URL to the page
                string subDir = f.DirectoryName.Replace(path, string.Empty).Replace("\\", "/") + "/";
                if (subDir.Length >= 1)
                    if (subDir.Substring(0, 1) == "/")
                        subDir = subDir.Substring(1);
                subDir = this.ResolveUrl("~") + subDir;
                // read file
                StreamReader sr = new StreamReader(f.FullName, Encoding.Default);
                string strF = sr.ReadToEnd();
                sr.Close();
                //suppress HTML
                string strContent = rex.Replace(strF, "");
                //Search for the text
                int nStart = strContent.ToLower().IndexOf(strText.ToLower());
                if (nStart > 0)//found!
                {
                    nFound++;//increase count of pages found
                    //title-link
                    strTitle = rex.Replace(rex2.Match(strF).Value, "");
                    if (string.IsNullOrEmpty(strTitle))
                        strTitle = f.Name; // If title is empty take file name instead
                    sbTempRes.Append("<p><a target=_blank href=\"");
                    sbTempRes.Append(subDir);
                    sbTempRes.Append(f.Name);
                    sbTempRes.Append("\">");
                    sbTempRes.Append(strTitle);
                    sbTempRes.Append("</a><br>");
                    sbTempRes.Append("<hr>");

                    int nApear = 0;
                    nStart = 0;
                    while ((nStart = strContent.ToLower().IndexOf(strText.ToLower(), nStart + strText.Length)) > 0)
                    {
                        nApear++;
                        if (nApear < NIMBER_OF_OCCURENCIES_TO_DISPLAY_PER_PAGE)
                        {
                            int nBefore = Math.Min(50, nStart);
                            int nAfter = Math.Min(50, strContent.Length - (strText.Length + nStart)) - 1;
                            sbTempRes.Append("...");
                            sbTempRes.Append(strContent.Substring(nStart - nBefore, nBefore));
                            sbTempRes.Append("<span style=\"background-color: yellow; font-weight: bold\">");
                            sbTempRes.Append(strContent.Substring(nStart, strText.Length));
                            sbTempRes.Append("</span>");
                            sbTempRes.Append(strContent.Substring(nStart + strText.Length, nAfter));
                            sbTempRes.Append("...<br>");
                        }
                    }
                    sbTempRes.Append("<br>");
                    sbTempRes.Append("Number of occurences: " + nApear.ToString());
                    sbTempRes.Append("</p>");

                    listResults.Add(nApear * MAX_PAGES_TO_SEARCH + nFound, sbTempRes.ToString());
                    sbTempRes.Remove(0, sbTempRes.Length);
                    if (nFound >= MAX_PAGES_TO_SEARCH - 1)
                        break; // Maximum number of pages to search is reached
                }
            }
        }

        foreach (KeyValuePair<int, string> i in listResults) // Retrieve search results by relevance
        {
            sbRes.Append(i.Value);
        }
        if (nFound == 0)
            sbRes.Insert(0, "<b>Your search returned no results </b>");
        else
            sbRes.Insert(0, "<b> Number of pages found: " + nFound.ToString()+"</b>");
        sbRes.Insert(0, "<hr>");
        sbRes.Insert(0, "<h2>Search results</h2>");
        return sbRes.ToString();
    }

    public class ReverseComparer<T> : IComparer<T> where T : IComparable<T>
    {
        public int Compare(T obj1, T obj2)
        {
            return -(obj1.CompareTo(obj2));
        }
    }

}

Коротко опишу, что она делает. Весь механизм поиска реализован в функции SearchSite, которая возвращает HTML код с результатами поиска. Результат, для показа на странице записывается в literal control litSearchResults.
Функция SearchSite получает три параметра: 
string strText - что искать (текст)
string path - где искать (локальная директория сайта в данном случае получает при помощи this.Server.MapPath("") )
string[] extensions - массив расширений файлов в которых надо искать
Дальше просматривает все файлы с указанными расширениями и ищет в них искомую строку. Перед началом поиска при помощи регулярных выражений из содержания страницы удаляются HTML  тэги. Из результатов форматирует HTML код и загоняет его в Generic SortedList (listResults) с обратной сортировкой, по одному элементу на найденную страницу. Это делается для того, чтобы результаты выводились по релевантности (первыми были страницы где искомое слово попадается чаще). В конце все содержимое listResults собирается в нужной последовательности в StringBuilder sbRes.

А вот как это выглядит.



Надо бы прицепить CSS для лучшего вида.

P.S. Будет работать на версиях .NET framework 2.0 и выше

Tags:

  • 1
Ммм, не сказал бы что мне это понравилось.
Во-первых, искать строчку в 10000 страницах обычным string.IndexOf (квадратичный алгоритм) некошерно :).
Во-вторых - почему ты ищешь именно вхождение комбинации ? а что если слова разбросаны по тексту ?
И последнее - по-моему надо искать во всех страницах, и только потом взять MAX_PAGES_TO_SEARCH самых релевантных, потому что можно упустить на самом деле нужные странички.

Да, не все здесь идеально - это первый черновой, но работает очень быстро - я попробовал пару тысяч страниц - задержки нет.
Разбросанные по тексту не ищет - это да, но можно слегка поменять - будет искать. Просто такого требования не было. Надо было искать только точные сообщения, но на регистр символов внимания не обращать.
MAX_PAGES_TO_SEARCH - это не количество страниц в которых ищется, а максимальное количество в которых найдено. То есть на сайте может быть миллион страниц, а искомое выражение встречаться в 7000-ах. Найдет все. И потом - MAX_PAGES_TO_SEARCH можно безболезненно увеличить.
Тут есть более серьезные недостатки например regular expression хорошо очищает HTML-тэги, но с XHTML - не так хорошо справляется - надо подправить.

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

Кстати, можешь измерить время, интересно за сколько времени он ищет на паре тысяч страниц.

2464 страниц (сайт клиента скопированный 14 раз), средний размер 34000 символов.
Найдено: 78
Время: 2.51 сек
Компьютер: Core2Duo E8400 3.0 GHz, RAM 2Gb

Рубен не позорься. Линейных поиск отменили еще в прошлом веке. Надо все зафигачить в mysql базу, сделать фулл текст индекс и искать одной строчкой командой MATCH AGAINST.

http://dev.mysql.com/doc/refman/5.0/en/fulltext-search.html

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

(Deleted comment)
Эпик фейл брателло :) Я могу предложить штук 5 алгоритмов, и все они будут лучше. Хотя надо признать что я не с кондачка, полгода писал (точнее добавлял новые фичи) в поисковик написанный на C.

Знаю я про индексацию и full text search index.
Да вот дело в то, что в сайте статические страницы и базы у них нет.
Хотя и в этом случае можно сделать оптимальнее. Так как меняться он будет не часто, можно проиндексировать - данные загнать в XML и, кешировать этим пользоваться при поиске. Будет быстро.

Если он меняется не часто, и количество страниц несколько тысяч - можно его просто банально проиндексировать по всем ключевым словам вообще, тогда искать можно будет лучше, да и быстрее (логарифмический поиск вероятнее всего).

Не обращай внимания на них Руб джан, они так офигели, что им подавай только алгоритмы со сложностью O(1).
Во всем виноват CQG. Он их развратил. :)))))))

В СQG все "чузох"-ы ? :)

Как это не обращать внимания?
Я все это дело не только для того задумал, чтобы рассказать всем, что я знаю, а чтобы еще выведать, что знают другие...

  • 1