2008-11-11 66 views
10

在開發搜索我正在構建的站點時,我決定採用便宜且快捷的方式,並使用Microsoft Sql Server的全文搜索引擎,而不是像Lucene.Net這樣更強大的工具。C#爲搜索結果顯示找到相關文檔片段

我想要的功能之一就是google-esque相關的文檔片段。我很快發現確定「相關」片段比我意識到的更困難。

我想根據搜索詞密度在找到的文本中選擇摘錄。所以,基本上,我需要在文本中找到最密集的段落。如果一段文字是一些任意數量的字符(比如說200,但它確實無關緊要)。

我的第一個想法是在一個循環中使用.IndexOf(),並建立一個項距離數組(從先前找到的項中減去找到的項的索引),然後......什麼?將任意兩個,任意三個,任意四個,任意五個順序數組元素相加,並使用總和最小的那個(因此,搜索項之間的最小距離)。

這似乎凌亂。

有沒有一種既定的,更好的,或更明顯的方式來做到這一點比我想出的?

+0

感嘆......又浪費了150分...... – 2010-07-27 20:44:26

+0

?那是什麼意思? – 2010-08-17 18:57:30

+0

對於任何對這個問題感興趣的人,都有一個更新的語言不可知的問題,它的回答比任何這個問題都要高:** [給出一個文檔,選擇一個相關的代碼片斷](http://stackoverflow.com/questions/2829303)** – hippietrail 2012-10-20 18:02:06

回答

1

好吧,這裏是我使用上述算法制作的黑客攻擊版本。我不認爲這很棒。它使用三個(count em,three!)循環一個數組和兩個列表。但是,好吧,總比沒有好。我還對最大長度進行了硬編碼,而不是將其變爲參數。

private static string FindRelevantSnippets(string infoText, string[] searchTerms) 
    { 
     List<int> termLocations = new List<int>(); 
     foreach (string term in searchTerms) 
     { 
      int termStart = infoText.IndexOf(term); 
      while (termStart > 0) 
      { 
       termLocations.Add(termStart); 
       termStart = infoText.IndexOf(term, termStart + 1); 
      } 
     } 

     if (termLocations.Count == 0) 
     { 
      if (infoText.Length > 250) 
       return infoText.Substring(0, 250); 
      else 
       return infoText; 
     } 

     termLocations.Sort(); 

     List<int> termDistances = new List<int>(); 
     for (int i = 0; i < termLocations.Count; i++) 
     { 
      if (i == 0) 
      { 
       termDistances.Add(0); 
       continue; 
      } 
      termDistances.Add(termLocations[i] - termLocations[i - 1]); 
     } 

     int smallestSum = int.MaxValue; 
     int smallestSumIndex = 0; 
     for (int i = 0; i < termDistances.Count; i++) 
     { 
      int sum = termDistances.Skip(i).Take(5).Sum(); 
      if (sum < smallestSum) 
      { 
       smallestSum = sum; 
       smallestSumIndex = i; 
      } 
     } 
     int start = Math.Max(termLocations[smallestSumIndex] - 128, 0); 
     int len = Math.Min(smallestSum, infoText.Length - start); 
     len = Math.Min(len, 250); 
     return infoText.Substring(start, len); 
    } 

我能想到的一些改進會返回多個「片斷」與加起來的長度較長短的長度 - 這樣的文檔的多個部分進行採樣。

+0

請注意,在實踐中,您必須標記輸入字符串並比較令牌 - 否則您的用戶可能會在Sussex和Essex中開始查找SEX :-)(某些網絡過濾程序存在問題!) – MZB 2010-07-27 19:39:37

0

如果你使用CONTAINSTABLE,你會得到一個RANK,這實際上是一個密度值--RANK值越高,密度越高。這樣,您只需運行一個查詢即可獲得您想要的結果,而不必在返回數據時對其進行按摩。

+0

This用於顯示搜索結果。它們按等級順序顯示。但爲了展示它們,我需要一個來自文檔的相關文本的google-esque片段 - 而不是整個文檔。 – 2008-12-12 20:30:16

2

這是一個很好的問題:)

我想我會創建一個索引矢量:對於每一個字,創造一個條目1,如果搜索詞或否則爲0。然後找到我,使得總和(索引向量[我:我+ maxlength])被最大化。

這實際上可以相當有效地完成。從第一個最大長度單詞中的searchterms開始。然後,當你繼續前進的時候,如果indexvector [i] = 1(即你增加i時你將失去那個搜索項),那麼減少你的計數器,如果indexvector [i + maxlength + 1] = 1,則增加它。隨你走,跟蹤最高櫃檯值的我。一旦你得到了你最喜歡的我,你仍然可以進行微調,比如看看你是否可以在不損害你的櫃檯的情況下減小實際尺寸。爲了找到句子邊界或其他。或者像挑選一些具有相同計數器值的正確的i。

不知道這是否比您的更好 - 這是另一回事。

你可能也想看看有關該主題的文件,該文件帶有尚未另一個基準:http://citeseerx.ist.psu.edu/viewdoc/download?doi=10.1.1.72.4357&rep=rep1&type=pdf

4

我知道這個帖子已經過時了,但是我上週給了這個試試,這是背面的痛苦。這遠非完美,但這是我想出的。

的片段生成:

public static string SelectKeywordSnippets(string StringToSnip, string[] Keywords, int SnippetLength) 
    { 
     string snippedString = ""; 
     List<int> keywordLocations = new List<int>(); 

     //Get the locations of all keywords 
     for (int i = 0; i < Keywords.Count(); i++) 
      keywordLocations.AddRange(SharedTools.IndexOfAll(StringToSnip, Keywords[i], StringComparison.CurrentCultureIgnoreCase)); 

     //Sort locations 
     keywordLocations.Sort(); 

     //Remove locations which are closer to each other than the SnippetLength 
     if (keywordLocations.Count > 1) 
     { 
      bool found = true; 
      while (found) 
      { 
       found = false; 
       for (int i = keywordLocations.Count - 1; i > 0; i--) 
        if (keywordLocations[i] - keywordLocations[i - 1] < SnippetLength/2) 
        { 
         keywordLocations[i - 1] = (keywordLocations[i] + keywordLocations[i - 1])/2; 

         keywordLocations.RemoveAt(i); 

         found = true; 
        } 
      } 
     } 

     //Make the snippets 
     if (keywordLocations.Count > 0 && keywordLocations[0] - SnippetLength/2 > 0) 
      snippedString = "... "; 
     foreach (int i in keywordLocations) 
     { 
      int stringStart = Math.Max(0, i - SnippetLength/2); 
      int stringEnd = Math.Min(i + SnippetLength/2, StringToSnip.Length); 
      int stringLength = Math.Min(stringEnd - stringStart, StringToSnip.Length - stringStart); 
      snippedString += StringToSnip.Substring(stringStart, stringLength); 
      if (stringEnd < StringToSnip.Length) snippedString += " ... "; 
      if (snippedString.Length > 200) break; 
     } 

     return snippedString; 

    } 

它會發現所有的關鍵字的索引示例文本

private static List<int> IndexOfAll(string haystack, string needle, StringComparison Comparison) 
    { 
     int pos; 
     int offset = 0; 
     int length = needle.Length; 
     List<int> positions = new List<int>(); 
     while ((pos = haystack.IndexOf(needle, offset, Comparison)) != -1) 
     { 
      positions.Add(pos); 
      offset = pos + length; 
     } 
     return positions; 
    } 

這是在其執行一個有點笨拙的功能。它的工作方式是通過查找字符串中所有關鍵字的位置。然後檢查沒有任何關鍵字比期望的片段長度更接近彼此,以便片段不會重疊(這就是它有點兒可能......)。然後抓住以關鍵字位置爲中心的所需長度的子串並將整個東西拼接在一起。

我知道這是晚年,但如果只是張貼它可能會幫助別人過這個問題來了。

2
public class Highlighter 
{   
    private class Packet 
    { 
     public string Sentence; 
     public double Density; 
     public int Offset; 
    } 

    public static string FindSnippet(string text, string query, int maxLength) 
    { 
     if (maxLength < 0) 
     { 
      throw new ArgumentException("maxLength"); 
     } 
     var words = query.Split(' ').Where(w => !string.IsNullOrWhiteSpace(w)).Select(word => word.ToLower()).ToLookup(s => s);    
     var sentences = text.Split('.'); 
     var i = 0; 
     var packets = sentences.Select(sentence => new Packet 
     { 
      Sentence = sentence, 
      Density = ComputeDensity(words, sentence), 
      Offset = i++ 
     }).OrderByDescending(packet => packet.Density); 
     var list = new SortedList<int, string>();    
     int length = 0;     
     foreach (var packet in packets) 
     { 
      if (length >= maxLength || packet.Density == 0) 
      { 
       break; 
      } 
      string sentence = packet.Sentence; 
      list.Add(packet.Offset, sentence.Substring(0, Math.Min(sentence.Length, maxLength - length))); 
      length += packet.Sentence.Length; 
     } 
     var sb = new List<string>(); 
     int previous = -1; 
     foreach (var item in list) 
     { 
      var offset = item.Key; 
      var sentence = item.Value; 
      if (previous != -1 && offset - previous != 1) 
      { 
       sb.Add("."); 
      } 
      previous = offset;    
      sb.Add(Highlight(sentence, words));     
     } 
     return String.Join(".", sb); 
    } 

    private static string Highlight(string sentence, ILookup<string, string> words) 
    { 
     var sb = new List<string>(); 
     var ff = true; 
     foreach (var word in sentence.Split(' ')) 
     { 
      var token = word.ToLower(); 
      if (ff && words.Contains(token)) 
      { 
       sb.Add("[[HIGHLIGHT]]"); 
       ff = !ff; 
      } 
      if (!ff && !string.IsNullOrWhiteSpace(token) && !words.Contains(token)) 
      { 
       sb.Add("[[ENDHIGHLIGHT]]"); 
       ff = !ff; 
      } 
      sb.Add(word); 
     } 
     if (!ff) 
     { 
      sb.Add("[[ENDHIGHLIGHT]]"); 
     } 
     return String.Join(" ", sb); 
    } 

    private static double ComputeDensity(ILookup<string, string> words, string sentence) 
    {    
     if (string.IsNullOrEmpty(sentence) || words.Count == 0) 
     { 
      return 0; 
     } 
     int numerator = 0; 
     int denominator = 0; 
     foreach(var word in sentence.Split(' ').Select(w => w.ToLower())) 
     { 
      if (words.Contains(word)) 
      { 
       numerator++; 
      } 
      denominator++; 
     } 
     if (denominator != 0) 
     { 
      return (double)numerator/denominator; 
     } 
     else 
     { 
      return 0; 
     } 
    } 
} 

實施例:

亮點「光流被定義爲結構光的圖像中的變化,例如,在視網膜上或相機的傳感器,由於眼球或相機之間的相對運動。場面從文獻中進一步定義突出光流的」,‘光流’

輸出不同的性質:

[[突出顯示]]光學流[[ENDHIGHLIGHT]]被定義爲結構化 光的圖像中的變化,E ...從文獻亮點的diff [[突出顯示]]光流的 erent性質[[進一步定義ENDHIGHLIGHT]]

0

剛剛寫了一個函數來做到這一點。你想在傳遞:

輸入:

文檔文本
這是你採取一個片段文檔的全文。最有可能的是你會想從這個文檔中刪除任何BBCode/HTML。

原始查詢
用戶輸入他們的搜索

片段長度要顯示的片段的
長度的字符串。

返回值:文檔文本的

開始指數採取從片段。要獲取片段只需執行documentText.Substring(returnValue, snippetLength)。這有一個好處,你知道如果片段是從開始/結束/中間,所以你可以添加一些裝飾,如...如果你希望在片段開始/結束。

性能

resolution設置爲1會找到最好的片段但隨着1噸焦炭每次移動窗口。將此值設置得更高以加快執行速度。

調整菜譜方案

,但是你想你可以計算出score。在這個例子中,我已經完成了​​來支持更長的單詞。

private static int GetSnippetStartPoint(string documentText, string originalQuery, int snippetLength) 
{ 
    // Normalise document text 
    documentText = documentText.Trim(); 
    if (string.IsNullOrWhiteSpace(documentText)) return 0; 

    // Return 0 if entire doc fits in snippet 
    if (documentText.Length <= snippetLength) return 0; 

    // Break query down into words 
    var wordsInQuery = new HashSet<string>(); 
    { 
     var queryWords = originalQuery.Split(' '); 
     foreach (var word in queryWords) 
     { 
      var normalisedWord = word.Trim().ToLower(); 
      if (string.IsNullOrWhiteSpace(normalisedWord)) continue; 
      if (wordsInQuery.Contains(normalisedWord)) continue; 
      wordsInQuery.Add(normalisedWord); 
     } 
    } 

    // Create moving window to get maximum trues 
    var windowStart = 0; 
    double maxScore = 0; 
    var maxWindowStart = 0; 

    // Higher number less accurate but faster 
    const int resolution = 5; 

    while (true) 
    { 
     var text = documentText.Substring(windowStart, snippetLength); 

     // Get score of this chunk 
     // This isn't perfect, as window moves in steps of resolution first and last words will be partial. 
     // Could probably be improved to iterate words and not characters. 
     var words = text.Split(' ').Select(c => c.Trim().ToLower()); 
     double score = 0; 
     foreach (var word in words) 
     { 
      if (wordsInQuery.Contains(word)) 
      { 
       // The longer the word, the more important. 
       // Can simply replace with score += 1 for simpler model. 
       score += Math.Pow(word.Length, 2); 
      }     
     } 
     if (score > maxScore) 
     { 
      maxScore = score; 
      maxWindowStart = windowStart; 
     } 

     // Setup next iteration 
     windowStart += resolution; 

     // Window end passed document end 
     if (windowStart + snippetLength >= documentText.Length) 
     { 
      break; 
     } 
    } 

    return maxWindowStart; 
} 

地段更多您可以添加到這一點,例如,而不是比較確切的話也許你可能想嘗試比較,你體重的同音的SOUNDEX匹配不到完全匹配。