Question

En développant la recherche du site que je construis, j'ai décidé de choisir le moyen le plus rapide et le moins cher et d'utiliser le moteur de recherche en texte intégral de Microsoft Sql Server au lieu de quelque chose de plus robuste, comme Lucene.Net.

L’une des fonctionnalités que j’aimerais avoir, toutefois, est les extraits de documents pertinents de Google. J'ai rapidement trouvé déterminant "pertinent". Les extraits sont plus difficiles que je ne le pensais.

Je souhaite choisir des extraits en fonction de la densité des termes de recherche dans le texte trouvé. Il est donc essentiel que je trouve le passage le plus dense dans le texte recherché. Un passage contient un nombre arbitraire de caractères (disons 200, mais cela n'a pas d'importance).

Ma première pensée est d’utiliser .IndexOf () dans une boucle et de construire un tableau de distances (soustrayez l’indice du terme trouvé du terme précédemment trouvé), puis ... quoi? Ajoutez deux, trois, quatre, cinq, cinq éléments de tableau séquentiels et utilisez celui dont la somme est la plus petite (d'où la plus petite distance entre les termes de recherche).

Cela semble en désordre.

Existe-t-il un moyen établi, meilleur ou plus évident de le faire que ce que j'ai proposé?

Était-ce utile?

La solution

Bien qu'il soit implémenté en Java, vous pouvez voir une approche pour ce problème ici: http://rcrezende.blogspot.com/2010/08 /smallest-relevant-text-snippet-for.html

Autres conseils

Je sais que ce fil est très ancien, mais je l’ai essayé la semaine dernière et c’était pénible à l’arrière. C’est loin d’être parfait, mais c’est ce que j’ai trouvé.

Le générateur d'extraits de code:

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;

    }

La fonction qui trouvera l'index de tous les mots-clés dans l'exemple de texte

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

C'est un peu maladroit dans son exécution. Cela fonctionne en trouvant la position de tous les mots-clés dans la chaîne. Ensuite, vérifiez qu'aucun mot-clé n'est plus proche les uns des autres que la longueur d'extrait souhaitée, afin que les extraits ne se chevauchent pas (c'est là que c'est un peu incertain ...). Puis, saisit des sous-chaînes de la longueur souhaitée centrées sur la position des mots-clés et assemble le tout.

Je sais que nous avons plusieurs années de retard, mais poster juste au cas où cela aiderait quelqu'un qui tombe sur cette question.

C’est un problème intéressant:)

Je pense que je créerais un vecteur d'indexation: pour chaque mot, créez une entrée 1 si le terme recherché ou autrement 0. Trouvez ensuite le i tel que somme (indexvector [i: i + maxlength]) soit maximisé.

Cela peut être fait assez efficacement. Commencez par le nombre de termes de recherche dans les premiers mots maxlength. Ensuite, diminuez votre compteur si indexvecteur [i] = 1 (c'est-à-dire si vous êtes sur le point de perdre ce terme de recherche à mesure que vous augmentez i) et augmentez-le si indexvector [i + maxlength + 1] = 1. Au fur et à mesure, gardez une trace du i avec la valeur de compteur la plus élevée.

Une fois que vous avez votre i préféré, vous pouvez toujours effectuer un réglage précis, comme si vous pouviez réduire la taille réelle sans compromettre votre compteur, par exemple. afin de trouver des limites de phrase ou autre chose. Ou comme choisir le bon i d'un certain nombre est avec des valeurs de compteur équivalentes.

Je ne suis pas sûr qu'il s'agisse d'une meilleure approche que la votre - c'est une approche différente.

Vous pouvez également consulter ce document sur le sujet, qui vient avec encore une autre base de référence: http://citeseerx.ist.psu.edu/viewdoc/download?doi=10.1.12.4357&rep=rep1&type=pdf

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

Exemple:

  

surligner "Le flux optique est défini comme le changement de lumière structurée dans l'image, par ex. sur la rétine ou le capteur de l'appareil photo, en raison d'un mouvement relatif entre le globe oculaire ou l'appareil photo et la scène. D'autres définitions de la littérature mettent en évidence différentes propriétés du flux optique " "flux optique"

Sortie:

[[HIGHLIGHT]] Le flux optique [[ENDHIGHLIGHT]] est défini comme le changement de structure  la lumière dans l'image, e ... Les autres définitions de la littérature mettent en évidence les diff Propriétés actuelles du flux optique [[HIGHLIGHT]] [[ENDHIGHLIGHT]]

Eh bien, voici la version piratée que j'ai créée en utilisant l'algorithme que j'ai décrit ci-dessus. Je ne pense pas que ce soit génial. Il utilise trois (compte em, trois!) Boucles d'un tableau et deux listes. Mais bon, c'est mieux que rien. J'ai également codé en dur la longueur maximale au lieu de la transformer en paramètre.

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

Certaines améliorations auxquelles je pouvais penser seraient de renvoyer plusieurs "extraits". avec une longueur plus courte qui s’ajoute à la longueur la plus longue - de cette façon, plusieurs parties du document peuvent être échantillonnées.

J'ai adopté une autre approche, cela aidera peut-être quelqu'un ...

Il commence par rechercher si le mot apparaît dans mon cas avec IgnoreCase (vous le modifiez bien sûr vous-même). Ensuite, je crée une liste de correspondances Regex sur chaque séparateur et recherche la première occurrence du mot (permettant des correspondances non sensibles à la casse partielle). À partir de cet index, les 10 correspondances se trouvent devant et derrière le mot, ce qui constitue l'extrait de code.

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

Si vous utilisez CONTAINSTABLE, vous obtiendrez un RANK, il s’agit essentiellement d’une valeur de densité: plus la valeur RANK est élevée, plus la densité est élevée. De cette façon, il vous suffit d’exécuter une requête pour obtenir les résultats souhaités sans avoir à massacrer les données lorsqu’elles sont renvoyées.

A écrit une fonction pour le faire tout à l'heure. Vous voulez transmettre:

Entrées:

Texte du document
Ceci est le texte intégral du document dont vous prenez un extrait. Très probablement, vous voudrez supprimer tous les BBCodes / HTML de ce document.

Requête d'origine
La chaîne que l'utilisateur a entrée comme recherche

Longueur de l'extrait
Longueur de l'extrait que vous souhaitez afficher.

Valeur renvoyée:

Début de l'index du texte du document à partir duquel l'extrait est extrait. Pour obtenir l'extrait, il suffit de faire documentText.Substring (returnValue, snippetLength) . Vous avez ainsi l'avantage de savoir si l'extrait est pris depuis le début / la fin / le milieu et vous pouvez ajouter des décorations telles que ... si vous le souhaitez au début / à la fin de l'extrait.

Performances

Un résolution réglé sur 1 trouvera le meilleur extrait mais déplacera la fenêtre d'un caractère à la fois. Définissez cette valeur plus élevée pour accélérer l'exécution.

Réglages

Vous pouvez définir le score comme bon vous semble. Dans cet exemple, j'ai utilisé Math.pow (wordLength, 2) pour privilégier les mots plus longs.

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

Vous pouvez ajouter beaucoup plus à cela, par exemple, au lieu de comparer des mots exacts, essayez de comparer le SOUNDEX où vous pondérez les correspondances soundex avec des correspondances inférieures aux correspondances exactes.

Licencié sous: CC-BY-SA avec attribution
Non affilié à StackOverflow
scroll top