C # Поиск соответствующих фрагментов документа для отображения результатов поиска - PullRequest
10 голосов
/ 11 ноября 2008

При разработке поиска для сайта, который я создаю, я решил пойти по дешевому и быстрому пути и использовать механизм полнотекстового поиска Microsoft Sql Server вместо чего-то более надежного, например Lucene.Net.

Одной из функций, которые я хотел бы иметь, являются соответствующие фрагменты документа, соответствующие требованиям Google. Я быстро обнаружил, что определение «соответствующих» фрагментов сложнее, чем я предполагал.

Я хочу выбирать фрагменты в зависимости от плотности поисковых запросов в найденном тексте. Поэтому, по сути, мне нужно найти наиболее насыщенный поисковый термин в тексте. Где отрывок - это произвольное количество символов (скажем, 200 - но это действительно не имеет значения).

Моя первая мысль - использовать в цикле .IndexOf () и построить массив расстояний между терминами (вычесть индекс найденного термина из ранее найденного термина), а затем ... что? Сложите любые два, любые три, любые четыре, любые пять последовательных элементов массива и используйте элемент с наименьшей суммой (следовательно, наименьшее расстояние между поисковыми терминами).

Это кажется грязным.

Есть ли установленный, лучший или более очевидный способ сделать это, чем я придумал?

Ответы [ 8 ]

6 голосов
/ 12 августа 2010

Хотя это реализовано в Java, вы можете увидеть один подход к этой проблеме здесь: http://rcrezende.blogspot.com/2010/08/smallest-relevant-text-snippet-for.html

4 голосов
/ 24 июня 2011

Я знаю, что этой ветке уже много лет, но я попробовал на прошлой неделе, и это была боль в задней части. Это далеко не идеально, но это то, что я придумал.

Генератор фрагментов:

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 голосов
/ 14 сентября 2013
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;
        }
    }
}

Пример:

выделение "Оптический поток определяется как изменение структурированного света на изображении, например, на сетчатке или датчике камеры, из-за относительного движения глазного яблока или камеры и сцены. Дальнейшие определения из литературы выделяют различные свойства оптического потока "" оптического потока "

Выход:

[[HIGHLIGHT]] Оптический поток [[ENDHIGHLIGHT]] определяется как изменение структурированного свет в изображении, э ... Дальнейшие определения из литературы Различные свойства оптического потока [[HIGHLIGHT]] [[ENDHIGHLIGHT]]

2 голосов
/ 27 июля 2010

Это хорошая проблема:)

Я думаю, что я бы создал индексный вектор: для каждого слова создайте запись 1, если поисковый термин или иначе 0. Затем найдите i такой, что сумма (indexvector [i: i + maxlength]) максимальна.

На самом деле это можно сделать довольно эффективно. Начните с количества поисковых терминов в первых словах максимальной длины. затем, двигаясь дальше, уменьшите свой счетчик, если indexvector [i] = 1 (т.е. вы собираетесь потерять этот поисковый термин при увеличении i), и увеличьте его, если indexvector [i + maxlength + 1] = 1. На ходу следите за i с наибольшим значением счетчика.

Как только вы получили свой любимый i, вы все равно можете выполнить тонкую настройку, как, например, посмотреть, можете ли вы уменьшить реальный размер без ущерба для вашего счетчика, например, чтобы найти границы предложения или что-то еще. Или как выбор правильного значения i из числа с эквивалентными значениями счетчика.

Не уверен, что это лучший подход, чем ваш - это другой.

Возможно, вы также захотите проверить эту статью по теме, которая идет с еще одним базовым уровнем: http://citeseerx.ist.psu.edu/viewdoc/download?doi=10.1.1.72.4357&rep=rep1&type=pdf

1 голос
/ 14 декабря 2018

Я выбрал другой подход, возможно, он кому-нибудь поможет ...

Сначала он ищет, если это слово появляется в моем случае с IgnoreCase (вы, конечно, измените это самостоятельно). Затем я создаю список соответствий регулярных выражений для каждого разделителя и ищу первое вхождение слова (допуская частичное совпадение без учета регистра). Из этого индекса я получаю 10 совпадений впереди и позади слова, что делает фрагмент.

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;
}
1 голос
/ 12 ноября 2008

Хорошо, вот взломанная версия, которую я сделал, используя алгоритм, описанный выше. Я не думаю, что это все так здорово. Он использует три (считайте их, три!) Цикла и два списка. Но это лучше, чем ничего. Я также жестко запрограммировал максимальную длину вместо того, чтобы превратить ее в параметр.

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 голосов
/ 14 августа 2017

Написал функцию, чтобы сделать это только сейчас. Вы хотите пройти:

Входы:

Текст документа
Это полный текст документа, из которого вы берете фрагмент. Скорее всего, вы захотите удалить любой BBCode / HTML из этого документа.

Исходный запрос
Строка, введенная пользователем при поиске

Длина фрагмента
Длина фрагмента, который вы хотите отобразить.

Возвращаемое значение:

Начальный индекс текста документа, из которого будет взят фрагмент. Чтобы получить фрагмент, просто сделайте documentText.Substring(returnValue, snippetLength). Преимущество этого в том, что вы знаете, что фрагмент взят из начала / конца / середины, так что вы можете добавить некоторые украшения, например ..., если хотите в начале / конце фрагмента.

Производительность

A resolution, установленный на 1 , найдет лучший фрагмент , но перемещает окно на 1 символ за раз. Установите это значение выше, чтобы ускорить выполнение.

Tweaks

Вы можете работать score так, как вы хотите. В этом примере я сделал Math.pow(wordLength, 2) в пользу более длинных слов.

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, где вы сопоставляете совпадения с soundex меньше, чем с точными совпадениями.

0 голосов
/ 10 декабря 2008

Если вы используете CONTAINSTABLE, вы получите RANK обратно, по сути это значение плотности - чем выше значение RANK, тем выше плотность. Таким образом, вы просто запускаете запрос, чтобы получить желаемые результаты, и вам не нужно массировать данные при их возврате.

...