C# Finding relevant document snippets for search result display

后端 未结 8 533
野性不改
野性不改 2021-02-04 12:07

In developing search for a site I am building, I decided to go the cheap and quick way and use Microsoft Sql Server\'s Full Text Search engine instead of something more robust l

相关标签:
8条回答
  • 2021-02-04 12:45
    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;
            }
        }
    }
    

    Example:

    highlight "Optic flow is defined as the change of structured light in the image, e.g. on the retina or the camera’s sensor, due to a relative motion between the eyeball or camera and the scene. Further definitions from the literature highlight different properties of optic flow" "optic flow"

    Output:

    [[HIGHLIGHT]] Optic flow [[ENDHIGHLIGHT]] is defined as the change of structured light in the image, e... Further definitions from the literature highlight diff erent properties of [[HIGHLIGHT]] optic flow [[ENDHIGHLIGHT]]

    0 讨论(0)
  • 2021-02-04 12:52

    Although it is implemented in Java, you can see one approach for that problem here: http://rcrezende.blogspot.com/2010/08/smallest-relevant-text-snippet-for.html

    0 讨论(0)
  • 2021-02-04 12:52

    Wrote a function to do this just now. You want to pass in:

    Inputs:

    Document text
    This is the full text of the document you're taking a snippet from. Most likely you will want to strip out any BBCode/HTML from this document.

    Original query
    The string the user entered as their search

    Snippet length
    Length of the snippet you wish to display.

    Return Value:

    Start index of the document text to take the snippet from. To get the snippet simply do documentText.Substring(returnValue, snippetLength). This has the advantage that you know if the snippet is take from the start/end/middle so you can add some decoration like ... if you wish at the snippet start/end.

    Performance

    A resolution set to 1 will find the best snippet but moves the window along 1 char at a time. Set this value higher to speed up execution.

    Tweaks

    You can work out score however you want. In this example I've done Math.pow(wordLength, 2) to favour longer words.

    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;
    }
    

    Lots more you can add to this, for example instead of comparing exact words perhaps you might want to try comparing the SOUNDEX where you weight soundex matches less than exact matches.

    0 讨论(0)
  • 2021-02-04 12:55

    I took another approach, perhaps it will help someone...

    First it searches if it word appears in my case with IgnoreCase (you change this of course yourself). Then I create a list of Regex matches on each separators and search for the first occurrence of the word (allowing partial case insensitive matches). From that index, I get the 10 matches in front and behind the word, which makes the snippet.

    public static string GetSnippet(string text, string word)
    {
        if (text.IndexOf(word, StringComparison.InvariantCultureIgnoreCase) == -1)
        {
            return "";
        }
    
        var matches = new Regex(@"\b(\S+)\s?", RegexOptions.Singleline | RegexOptions.Compiled).Matches(text);
    
        var p = -1;
        for (var i = 0; i < matches.Count; i++)
        {
            if (matches[i].Value.IndexOf(word, StringComparison.InvariantCultureIgnoreCase) != -1)
            {
                p = i;
                break;
            }
        }
    
        if (p == -1) return "";
        var snippet = "";
        for (var x = Math.Max(p - 10, 0); x < p + 10; x++)
        {
            snippet += matches[x].Value + " ";
        }
        return snippet;
    }
    
    0 讨论(0)
  • 2021-02-04 13:08

    This is a nice problem :)

    I think I'd create an index vector: For each word, create an entry 1 if search term or otherwise 0. Then find the i such that sum(indexvector[i:i+maxlength]) is maximized.

    This can actually be done rather efficiently. Start with the number of searchterms in the first maxlength words. then, as you move on, decrease your counter if indexvector[i]=1 (i.e. your about to lose that search term as you increase i) and increase it if indexvector[i+maxlength+1]=1. As you go, keep track of the i with the highest counter value.

    Once you got your favourite i, you can still do finetuning like see if you can reduce the actual size without compromising your counter, e.g. in order to find sentence boundaries or whatever. Or like picking the right i of a number of is with equivalent counter values.

    Not sure if this is a better approach than yours - it's a different one.

    You might also want to check out this paper on the topic, which comes with yet-another baseline: http://citeseerx.ist.psu.edu/viewdoc/download?doi=10.1.1.72.4357&rep=rep1&type=pdf

    0 讨论(0)
  • 2021-02-04 13:11

    If you use CONTAINSTABLE you will get a RANK back , this is in essence a density value - higher the RANK value, the higher the density. This way, you just run a query to get the results you want and dont have to result to massaging the data when its returned.

    0 讨论(0)
提交回复
热议问题