51 votes

Comment faire une auto-complétion/suggestions de requête dans Lucene?

Je cherche un moyen de faire de l'auto-complétion/des suggestions de requêtes dans Lucene. J'ai fait quelques recherches sur Google et j'ai essayé un peu, mais tous les exemples que j'ai vus semblent configurer des filtres dans Solr. Nous n'utilisons pas Solr et nous n'avons pas l'intention de passer à l'utilisation de Solr dans un avenir proche, et Solr enveloppe clairement Lucene de toute façon, donc je suppose qu'il doit y avoir un moyen de le faire!

J'ai envisagé d'utiliser EdgeNGramFilter, et je réalise que je devrais exécuter le filtre sur les champs d'index et obtenir les jetons, puis les comparer à la requête saisie... J'ai juste du mal à faire le lien entre les deux en un peu de code, donc toute aide est grandement appréciée!

Pour être clair sur ce que je recherche (j'ai réalisé que je n'étais pas très clair, désolé) - je recherche une solution où lors de la recherche d'un terme, elle renverrait une liste de requêtes suggérées. En tapant 'inter' dans le champ de recherche, il reviendrait avec une liste de requêtes suggérées, telles que 'internet', 'international', etc.

0 votes

Lucène dispose désormais de quelques codes spécifiques pour effectuer l'autocomplétion / suggestions. Consultez stackoverflow.com/questions/24968697/… pour une réponse décrivant comment l'utiliser.

38voto

Mat Mannion Points 2072

Basé sur la réponse de @Alexandre Victoor, j'ai écrit une petite classe basée sur le Spellchecker Lucene dans le package contrib (et utilisant le LuceneDictionary inclus dans celui-ci) qui fait exactement ce que je veux.

Cela permet de réindexer à partir d'un seul index source avec un seul champ, et fournit des suggestions pour les termes. Les résultats sont triés par le nombre de documents correspondants avec ce terme dans l'index d'origine, de sorte que les termes les plus populaires apparaissent en premier. Semble fonctionner assez bien :)

import java.io.IOException;
import java.io.Reader;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;

import org.apache.lucene.analysis.Analyzer;
import org.apache.lucene.analysis.ISOLatin1AccentFilter;
import org.apache.lucene.analysis.LowerCaseFilter;
import org.apache.lucene.analysis.StopFilter;
import org.apache.lucene.analysis.TokenStream;
import org.apache.lucene.analysis.ngram.EdgeNGramTokenFilter;
import org.apache.lucene.analysis.ngram.EdgeNGramTokenFilter.Side;
import org.apache.lucene.analysis.standard.StandardFilter;
import org.apache.lucene.analysis.standard.StandardTokenizer;
import org.apache.lucene.document.Document;
import org.apache.lucene.document.Field;
import org.apache.lucene.index.CorruptIndexException;
import org.apache.lucene.index.IndexReader;
import org.apache.lucene.index.IndexWriter;
import org.apache.lucene.index.Term;
import org.apache.lucene.search.IndexSearcher;
import org.apache.lucene.search.Query;
import org.apache.lucene.search.ScoreDoc;
import org.apache.lucene.search.Sort;
import org.apache.lucene.search.TermQuery;
import org.apache.lucene.search.TopDocs;
import org.apache.lucene.search.spell.LuceneDictionary;
import org.apache.lucene.store.Directory;
import org.apache.lucene.store.FSDirectory;

/**
 * Autocompléteur de termes de recherche, fonctionne pour des termes simples (donc à utiliser sur le dernier terme
 * de la requête).
 *

2 votes

Gardez à l'esprit que cela a été créé pour une ancienne version de Lucene. Dans la version actuelle (4.4.0), la méthode abstraite à implémenter dans la classe Analyseur est createComponents(String fieldName, Reader reader). Voir lucene.apache.org/core/4_4_0/core/org/apache/lucene/analysis‌​/…

27voto

ThisIsTheDave Points 645

Voici une translittération de l'implémentation de Mat en C # pour Lucene.NET, ainsi qu'un fragment de code permettant de câbler une zone de texte à l'aide de la fonctionnalité de saisie semi-automatique de jQuery.

 <input id="search-input" name="query" placeholder="Search database." type="text" />
 

... JQuery Autocomplete:

 // don't navigate away from the field when pressing tab on a selected item
$( "#search-input" ).keydown(function (event) {
    if (event.keyCode === $.ui.keyCode.TAB && $(this).data("autocomplete").menu.active) {
        event.preventDefault();
    }
});

$( "#search-input" ).autocomplete({
    source: '@Url.Action("SuggestTerms")', // <-- ASP.NET MVC Razor syntax
    minLength: 2,
    delay: 500,
    focus: function () {
        // prevent value inserted on focus
        return false;
    },
    select: function (event, ui) {
        var terms = this.value.split(/\s+/);
        terms.pop(); // remove dropdown item
        terms.push(ui.item.value.trim()); // add completed item
        this.value = terms.join(" "); 
        return false;
    },
 });
 

... voici le code du contrôleur ASP.NET MVC:

     //
    // GET: /MyApp/SuggestTerms?term=something
    public JsonResult SuggestTerms(string term)
    {
        if (string.IsNullOrWhiteSpace(term))
            return Json(new string[] {});

        term = term.Split().Last();

        // Fetch suggestions
        string[] suggestions = SearchSvc.SuggestTermsFor(term).ToArray();

        return Json(suggestions, JsonRequestBehavior.AllowGet);
    }
 

... et voici le code de Mat en C #:

 using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Lucene.Net.Store;
using Lucene.Net.Index;
using Lucene.Net.Search;
using SpellChecker.Net.Search.Spell;
using Lucene.Net.Analysis;
using Lucene.Net.Analysis.Standard;
using Lucene.Net.Analysis.NGram;
using Lucene.Net.Documents;

namespace Cipher.Services
{
    /// <summary>
    /// Search term auto-completer, works for single terms (so use on the last term of the query).
    /// Returns more popular terms first.
    /// <br/>
    /// Author: Mat Mannion, M.Mannion@warwick.ac.uk
    /// <seealso cref="http://stackoverflow.com/questions/120180/how-to-do-query-auto-completion-suggestions-in-lucene"/>
    /// </summary>
    /// 
    public class SearchAutoComplete {

        public int MaxResults { get; set; }

        private class AutoCompleteAnalyzer : Analyzer
        {
            public override TokenStream  TokenStream(string fieldName, System.IO.TextReader reader)
            {
                TokenStream result = new StandardTokenizer(kLuceneVersion, reader);

                result = new StandardFilter(result);
                result = new LowerCaseFilter(result);
                result = new ASCIIFoldingFilter(result);
                result = new StopFilter(false, result, StopFilter.MakeStopSet(kEnglishStopWords));
                result = new EdgeNGramTokenFilter(
                    result, Lucene.Net.Analysis.NGram.EdgeNGramTokenFilter.Side.FRONT,1, 20);

                return result;
            }
        }

        private static readonly Lucene.Net.Util.Version kLuceneVersion = Lucene.Net.Util.Version.LUCENE_29;

        private static readonly String kGrammedWordsField = "words";

        private static readonly String kSourceWordField = "sourceWord";

        private static readonly String kCountField = "count";

        private static readonly String[] kEnglishStopWords = {
            "a", "an", "and", "are", "as", "at", "be", "but", "by",
            "for", "i", "if", "in", "into", "is",
            "no", "not", "of", "on", "or", "s", "such",
            "t", "that", "the", "their", "then", "there", "these",
            "they", "this", "to", "was", "will", "with"
        };

        private readonly Directory m_directory;

        private IndexReader m_reader;

        private IndexSearcher m_searcher;

        public SearchAutoComplete(string autoCompleteDir) : 
            this(FSDirectory.Open(new System.IO.DirectoryInfo(autoCompleteDir)))
        {
        }

        public SearchAutoComplete(Directory autoCompleteDir, int maxResults = 8) 
        {
            this.m_directory = autoCompleteDir;
            MaxResults = maxResults;

            ReplaceSearcher();
        }

        /// <summary>
        /// Find terms matching the given partial word that appear in the highest number of documents.</summary>
        /// <param name="term">A word or part of a word</param>
        /// <returns>A list of suggested completions</returns>
        public IEnumerable<String> SuggestTermsFor(string term) 
        {
            if (m_searcher == null)
                return new string[] { };

            // get the top terms for query
            Query query = new TermQuery(new Term(kGrammedWordsField, term.ToLower()));
            Sort sort = new Sort(new SortField(kCountField, SortField.INT));

            TopDocs docs = m_searcher.Search(query, null, MaxResults, sort);
            string[] suggestions = docs.ScoreDocs.Select(doc => 
                m_reader.Document(doc.doc).Get(kSourceWordField)).ToArray();

            return suggestions;
        }


        /// <summary>
        /// Open the index in the given directory and create a new index of word frequency for the 
        /// given index.</summary>
        /// <param name="sourceDirectory">Directory containing the index to count words in.</param>
        /// <param name="fieldToAutocomplete">The field in the index that should be analyzed.</param>
        public void BuildAutoCompleteIndex(Directory sourceDirectory, String fieldToAutocomplete)
        {
            // build a dictionary (from the spell package)
            using (IndexReader sourceReader = IndexReader.Open(sourceDirectory, true))
            {
                LuceneDictionary dict = new LuceneDictionary(sourceReader, fieldToAutocomplete);

                // code from
                // org.apache.lucene.search.spell.SpellChecker.indexDictionary(
                // Dictionary)
                //IndexWriter.Unlock(m_directory);

                // use a custom analyzer so we can do EdgeNGramFiltering
                var analyzer = new AutoCompleteAnalyzer();
                using (var writer = new IndexWriter(m_directory, analyzer, true, IndexWriter.MaxFieldLength.LIMITED))
                {
                    writer.SetMergeFactor(300);
                    writer.SetMaxBufferedDocs(150);

                    // go through every word, storing the original word (incl. n-grams) 
                    // and the number of times it occurs
                    foreach (string word in dict)
                    {
                        if (word.Length < 3)
                            continue; // too short we bail but "too long" is fine...

                        // ok index the word
                        // use the number of documents this word appears in
                        int freq = sourceReader.DocFreq(new Term(fieldToAutocomplete, word));
                        var doc = MakeDocument(fieldToAutocomplete, word, freq);

                        writer.AddDocument(doc);
                    }

                    writer.Optimize();
                }

            }

            // re-open our reader
            ReplaceSearcher();
        }

        private static Document MakeDocument(String fieldToAutocomplete, string word, int frequency)
        {
            var doc = new Document();
            doc.Add(new Field(kSourceWordField, word, Field.Store.YES,
                    Field.Index.NOT_ANALYZED)); // orig term
            doc.Add(new Field(kGrammedWordsField, word, Field.Store.YES,
                    Field.Index.ANALYZED)); // grammed
            doc.Add(new Field(kCountField,
                    frequency.ToString(), Field.Store.NO,
                    Field.Index.NOT_ANALYZED)); // count
            return doc;
        }

        private void ReplaceSearcher() 
        {
            if (IndexReader.IndexExists(m_directory))
            {
                if (m_reader == null)
                    m_reader = IndexReader.Open(m_directory, true);
                else
                    m_reader.Reopen();

                m_searcher = new IndexSearcher(m_reader);
            }
            else
            {
                m_searcher = null;
            }
        }


    }
}
 

1 votes

Serait-il possible pour vous d'ajouter un extrait de pilote C# qui exécute votre code, ainsi que le code pour construire l'index ? Je peux faire en sorte que votre code compile très bien, mais j'ai du mal à comprendre comment construire mon Répertoire afin qu'il puisse être interrogé par le code ci-dessus.

1 votes

Est-ce que cela importe comment le répertoire a été indexé précédemment? Est-ce que je peux exécuter ceci sur un index qui a été créé en utilisant l'analyseur snowball? Ou devrais-je utiliser un champ qui n'a pas été du tout analysé? (posé la même question ci-dessus)

0 votes

Toute exemple utilisant JAVA Solr et Jquery ou javaScript?

5voto

user2098849 Points 41

Mon code basé sur lucene 4.2, peut vous aider

import java.io.File;
import java.io.IOException;

import org.apache.lucene.analysis.miscellaneous.PerFieldAnalyzerWrapper;
import org.apache.lucene.index.DirectoryReader;
import org.apache.lucene.index.IndexWriterConfig;
import org.apache.lucene.index.IndexWriterConfig.OpenMode;
import org.apache.lucene.search.spell.Dictionary;
import org.apache.lucene.search.spell.LuceneDictionary;
import org.apache.lucene.search.spell.PlainTextDictionary;
import org.apache.lucene.search.spell.SpellChecker;
import org.apache.lucene.store.Directory;
import org.apache.lucene.store.FSDirectory;
import org.apache.lucene.store.IOContext;
import org.apache.lucene.store.RAMDirectory;
import org.apache.lucene.util.Version;
import org.wltea4pinyin.analyzer.lucene.IKAnalyzer4PinYin;

/**
 * 
 * 
 * @author 
 * @version 2013-11-25上午11:13:59
 */
public class LuceneSpellCheckerDemoService {

private static final String INDEX_FILE = "/Users/r/Documents/jar/luke/youtui/index";
private static final String INDEX_FILE_SPELL = "/Users/r/Documents/jar/luke/spell";

private static final String INDEX_FIELD = "app_name_quanpin";

public static void main(String args[]) {

    try {
        //
        PerFieldAnalyzerWrapper wrapper = new PerFieldAnalyzerWrapper(new IKAnalyzer4PinYin(
                true));

        //  lire la conf de l'index
        IndexWriterConfig conf = new IndexWriterConfig(Version.LUCENE_42, wrapper);
        conf.setOpenMode(OpenMode.CREATE_OR_APPEND);

        // lire le dictionnaire
        Directory directory = FSDirectory.open(new File(INDEX_FILE));
        RAMDirectory ramDir = new RAMDirectory(directory, IOContext.READ);
        DirectoryReader indexReader = DirectoryReader.open(ramDir);

        Dictionary dic = new LuceneDictionary(indexReader, INDEX_FIELD);

        SpellChecker sc = new SpellChecker(FSDirectory.open(new File(INDEX_FILE_SPELL)));
        //sc.indexDictionary(new PlainTextDictionary(new File("myfile.txt")), conf, false);
        sc.indexDictionary(dic, conf, true);
        String[] strs = sc.suggestSimilar("zhsiwusdazhanjiangshi", 10);
        for (int i = 0; i < strs.length; i++) {
            System.out.println(strs[i]);
        }
        sc.close();
    } catch (IOException e) {
        e.printStackTrace();
    }
}

}

0 votes

Salut, peux-tu me dire quelle est la différence entre Index_file et Index_file_spell?

0 votes

Index_file est l'index des documents. Et Index_file_spell utilise Index_file pour obtenir l'index qui est utilisé pour l'auto-complétion/suggestions.

4voto

megawatts Points 31

En plus de la publication ci-dessus (très appréciée) concernant la conversion en c#, si vous utilisez .NET 3.5, vous devrez inclure le code pour le filtre de jetons EdgeNGramTokenFilter - du moins, c'est ce que j'ai fait - en utilisant Lucene 2.9.2 - ce filtre est manquant dans la version .NET autant que je puisse dire. J'ai dû trouver la version .NET 4 en ligne en 2.9.3 et la rétroporter - j'espère que cela rend la procédure moins douloureuse pour quelqu’un...

Modifier : Veuillez également noter que le tableau retourné par la fonction SuggestTermsFor() est trié par ordre croissant du nombre, vous voudrez probablement le renverser pour obtenir les termes les plus populaires en premier dans votre liste

using System.IO;
using System.Collections;
using Lucene.Net.Analysis;
using Lucene.Net.Analysis.Tokenattributes;
using Lucene.Net.Util;

namespace Lucene.Net.Analysis.NGram
{

/**
 * Tokenise le jeton donné en n-grammes de tailles données.
 * 
 * Ce {@link TokenFilter} crée des n-grammes à partir du bord de début ou du bord de fin d'un jeton d'entrée.
 * 
 */
public class EdgeNGramTokenFilter : TokenFilter
{
    public static Side DEFAULT_SIDE = Side.FRONT;
    public static int DEFAULT_MAX_GRAM_SIZE = 1;
    public static int DEFAULT_MIN_GRAM_SIZE = 1;

    // Remplacer ceci par une énumération lorsque la mise à niveau Java 1.5 sera effectuée, l'implémentation sera simplifiée
    /** Spécifie de quel côté de l'entrée l'n-gramme doit être généré */
    public class Side
    {
        private string label;

        /** Obtient le n-gramme du début de l'entrée */
        public static Side FRONT = new Side("front");

        /** Obtient le n-gramme de la fin de l'entrée */
        public static Side BACK = new Side("back");

        // Constructeur privé
        private Side(string label) { this.label = label; }

        public string getLabel() { return label; }

        // Obtenir le côté approprié à partir d'une chaîne
        public static Side getSide(string sideName)
        {
            if (FRONT.getLabel().Equals(sideName))
            {
                return FRONT;
            }
            else if (BACK.getLabel().Equals(sideName))
            {
                return BACK;
            }
            return null;
        }
    }

    private int minGram;
    private int maxGram;
    private Side side;
    private char[] curTermBuffer;
    private int curTermLength;
    private int curGramSize;
    private int tokStart;

    private TermAttribute termAtt;
    private OffsetAttribute offsetAtt;

    protected EdgeNGramTokenFilter(TokenStream input) : base(input)
    {
        this.termAtt = (TermAttribute)AddAttribute(typeof(TermAttribute));
        this.offsetAtt = (OffsetAttribute)AddAttribute(typeof(OffsetAttribute));
    }

    /**
     * Crée un EdgeNGramTokenFilter qui peut générer des n-grammes dans la plage de tailles donnée
     *
     * @param input {@link TokenStream} contenant l'entrée à tokeniser
     * @param side le {@link Side} à partir duquel découper un n-gramme
     * @param minGram le plus petit n-gramme à générer
     * @param maxGram le plus grand n-gramme à générer
     */
    public EdgeNGramTokenFilter(TokenStream input, Side side, int minGram, int maxGram)
        : base(input)
    {

        if (side == null)
        {
            throw new System.ArgumentException("sideLabel doit être soit front soit back");
        }

        if (minGram < 1)
        {
            throw new System.ArgumentException("minGram doit être supérieur à zéro");
        }

        if (minGram > maxGram)
        {
            throw new System.ArgumentException("minGram ne doit pas être supérieur à maxGram");
        }

        this.minGram = minGram;
        this.maxGram = maxGram;
        this.side = side;
        this.termAtt = (TermAttribute)AddAttribute(typeof(TermAttribute));
        this.offsetAtt = (OffsetAttribute)AddAttribute(typeof(OffsetAttribute));
    }

    /**
     * Crée un EdgeNGramTokenFilter qui peut générer des n-grammes dans la plage de tailles donnée
     *
     * @param input {@link TokenStream} contenant l'entrée à tokeniser
     * @param sideLabel le nom du {@link Side} à partir duquel découper un n-gramme
     * @param minGram le plus petit n-gramme à générer
     * @param maxGram le plus grand n-gramme à générer
     */
    public EdgeNGramTokenFilter(TokenStream input, string sideLabel, int minGram, int maxGram)
        : this(input, Side.getSide(sideLabel), minGram, maxGram)
    {

    }

    public override bool IncrementToken()
    {
        while (true)
        {
            if (curTermBuffer == null)
            {
                if (!input.IncrementToken())
                {
                    return false;
                }
                else
                {
                    curTermBuffer = (char[])termAtt.TermBuffer().Clone();
                    curTermLength = termAtt.TermLength();
                    curGramSize = minGram;
                    tokStart = offsetAtt.StartOffset();
                }
            }
            if (curGramSize <= maxGram)
            {
                if (!(curGramSize > curTermLength         // si l'entrée restante est trop courte, nous ne pouvons pas générer d'n-grams
                    || curGramSize > maxGram))
                {       // si nous avons atteint la fin de notre plage de tailles d'n-grammes, quittons
                    // obtenir gramSize caractères depuis le début ou la fin
                    int start = side == Side.FRONT ? 0 : curTermLength - curGramSize;
                    int end = start + curGramSize;
                    ClearAttributes();
                    offsetAtt.SetOffset(tokStart + start, tokStart + end);
                    termAtt.SetTermBuffer(curTermBuffer, start, curGramSize);
                    curGramSize++;
                    return true;
                }
            }
            curTermBuffer = null;
        }
    }

    public override  Token Next(Token reusableToken)
    {
        return base.Next(reusableToken);
    }
    public override Token Next()
    {
        return base.Next();
    }
    public override void Reset()
    {
        base.Reset();
        curTermBuffer = null;
    }
}
}

0 votes

Est-ce important de savoir comment le répertoire a été indexé précédemment? Puis-je exécuter cette tâche sur un index qui a été créé en utilisant l'analyseur snowball? Ou devrais-je utiliser un champ qui n'a pas été du tout analysé?

4voto

Alexandre Victoor Points 1814

Vous pouvez utiliser la classe PrefixQuery sur un index "dictionnaire". La classe LuceneDictionary pourrait également être utile.

Jetez un œil à cet article lié ci-dessous. Il explique comment implémenter la fonction "Vous vouliez dire ?" disponible dans les moteurs de recherche modernes tels que Google. Vous n'aurez peut-être pas besoin de quelque chose d'aussi complexe que décrit dans l'article. Cependant, l'article explique comment utiliser le package d'orthographe Lucene.

Une façon de construire un index "dictionnaire" serait d'itérer sur un LuceneDictionary.

J'espère que cela vous sera utile

Vous vouliez dire : Lucène? (page 1)

Vous vouliez dire : Lucène? (page 2)

Vous vouliez dire : Lucène? (page 3)

3 votes

Il s'agit d'un exemple classique de la raison pour laquelle les réponses contenant uniquement des liens ne sont pas de bonnes réponses, car ce lien est désormais obsolète.

1 votes

@BenCollins a mis à jour l'article avec des liens de la Wayback Machine.

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X