2008-11-11 147 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匹配不到完全匹配。