Recherche plein texte de dialogues avec Apache Lucene : un tutoriel

Publié: 2022-03-11

Apache Lucene est une bibliothèque Java utilisée pour la recherche en texte intégral de documents et est au cœur des serveurs de recherche tels que Solr et Elasticsearch. Il peut également être intégré dans des applications Java, telles que des applications Android ou des backends Web.

Bien que les options de configuration de Lucene soient étendues, elles sont destinées à être utilisées par les développeurs de bases de données sur un corpus de texte générique. Si vos documents ont une structure ou un type de contenu spécifique, vous pouvez tirer parti de l'un ou l'autre pour améliorer la qualité de la recherche et la capacité de requête.

recherche plein texte avec apache lucene

À titre d'exemple de ce type de personnalisation, dans ce didacticiel Lucene, nous indexerons le corpus du projet Gutenberg, qui propose des milliers de livres électroniques gratuits. Nous savons que beaucoup de ces livres sont des romans. Supposons que nous soyons particulièrement intéressés par le dialogue au sein de ces romans. Ni Lucene, Elasticsearch ni Solr ne fournissent d'outils prêts à l'emploi pour identifier le contenu en tant que dialogue. En fait, ils jetteront la ponctuation dès les premières étapes de l'analyse de texte, ce qui va à l'encontre de la capacité d'identifier les parties du texte qui sont des dialogues. C'est donc dans ces premières étapes que notre personnalisation doit commencer.

Éléments du pipeline d'analyse Apache Lucene

L'analyse Lucene JavaDoc fournit un bon aperçu de toutes les pièces mobiles du pipeline d'analyse de texte.

À un niveau élevé, vous pouvez imaginer que le pipeline d'analyse consomme un flux brut de caractères au début et produit des « termes », correspondant approximativement aux mots, à la fin.

Le pipeline d'analyse standard peut être visualisé comme tel :

Pipeline d'analyse du lucène

Nous verrons comment personnaliser ce pipeline pour reconnaître les régions de texte marquées par des guillemets doubles, que j'appellerai dialogue, puis augmenter les correspondances qui se produisent lors de la recherche dans ces régions.

Caractères de lecture

Lorsque les documents sont initialement ajoutés à l'index, les caractères sont lus à partir d'un Java InputStream, et ils peuvent donc provenir de fichiers, de bases de données, d'appels de services Web, etc. Pour créer un index pour le projet Gutenberg, nous téléchargeons les livres électroniques, et créer une petite application pour lire ces fichiers et les écrire dans l'index. La création d'un index Lucene et la lecture de fichiers sont des chemins bien parcourus, nous ne les explorerons donc pas beaucoup. Le code essentiel pour produire un index est :

 IndexWriter writer = ...; BufferedReader reader = new BufferedReader(new InputStreamReader(... fileInputStream ...)); Document document = new Document(); document.add(new StringField("title", fileName, Store.YES)); document.add(new TextField("body", reader)); writer.addDocument(document);

Nous pouvons voir que chaque e-book correspondra à un seul Document Lucene donc, plus tard, nos résultats de recherche seront une liste de livres correspondants. Store.YES indique que nous stockons le champ de titre , qui est juste le nom du fichier. Cependant, nous ne voulons pas stocker le corps du livre électronique, car il n'est pas nécessaire lors de la recherche et ne ferait que gaspiller de l'espace disque.

La lecture réelle du flux commence par addDocument . L' IndexWriter extrait les jetons de la fin du pipeline. Cette traction revient à travers le tuyau jusqu'à ce que la première étape, le Tokenizer , lise à partir du InputStream .

Notez également que nous ne fermons pas le flux, car Lucene s'en charge pour nous.

Caractères symboliques

Le Lucene StandardTokenizer supprime la ponctuation, et notre personnalisation commencera donc ici, car nous devons conserver les guillemets.

La documentation de StandardTokenizer vous invite à copier le code source et à l'adapter à vos besoins, mais cette solution serait inutilement complexe. Au lieu de cela, nous allons étendre CharTokenizer , qui vous permet de spécifier des caractères à "accepter", où ceux qui ne sont pas "acceptés" seront traités comme des délimiteurs entre les jetons et jetés. Puisque nous nous intéressons aux mots et aux citations qui les entourent, notre Tokenizer personnalisé est simplement :

 public class QuotationTokenizer extends CharTokenizer { @Override protected boolean isTokenChar(int c) { return Character.isLetter(c) || c == '"'; } }

Étant donné un flux d'entrée de [He said, "Good day".] , les jetons produits seraient [He] , [said] , ["Good] , [day"]

Notez comment les guillemets sont intercalés dans les jetons. Il est possible d'écrire un Tokenizer qui produit des jetons séparés pour chaque devis, mais Tokenizer est également concerné par des détails fastidieux et faciles à visser tels que la mise en mémoire tampon et la numérisation, il est donc préférable de garder votre Tokenizer simple et de nettoyer le flux de jetons plus loin dans le pipeline.

Fractionner des jetons à l'aide de filtres

Après le tokenizer vient une série d'objets TokenFilter . Notez, incidemment, que le filtre est un peu impropre, car un TokenFilter peut ajouter, supprimer ou modifier des jetons.

De nombreuses classes de filtres fournies par Lucene attendent des mots uniques, il ne convient donc pas d'y intégrer nos jetons mixtes de mots et de citations. Ainsi, la prochaine personnalisation de notre tutoriel Lucene doit être l'introduction d'un filtre qui nettoiera la sortie de QuotationTokenizer .

Ce nettoyage impliquera la production d'un jeton de citation de début supplémentaire si la citation apparaît au début d'un mot, ou d'un jeton de citation de fin si la citation apparaît à la fin. Nous laisserons de côté la manipulation des mots entre guillemets simples pour plus de simplicité.

La création d'une sous-classe TokenFilter implique la mise en œuvre d'une méthode : incrementToken . Cette méthode doit appeler incrementToken sur le filtre précédent dans le canal, puis manipuler les résultats de cet appel pour effectuer le travail dont le filtre est responsable. Les résultats d' incrementToken sont disponibles via des objets Attribute , qui décrivent l'état actuel du traitement des jetons. Après le retour de notre implémentation d' incrementToken , on s'attend à ce que les attributs aient été manipulés pour configurer le jeton pour le prochain filtre (ou l'index si nous sommes à la fin du tuyau).

Les attributs qui nous intéressent à ce stade du pipeline sont :

  • CharTermAttribute : Contient un tampon char[] contenant les caractères du jeton actuel. Nous devrons manipuler cela pour supprimer la citation ou pour produire un jeton de citation.

  • TypeAttribute : Contient le "type" du jeton actuel. Étant donné que nous ajoutons des guillemets de début et de fin au flux de jetons, nous allons introduire deux nouveaux types à l'aide de notre filtre.

  • OffsetAttribute : Lucene peut éventuellement stocker des références à l'emplacement des termes dans le document d'origine. Ces références sont appelées "décalages", qui ne sont que des indices de début et de fin dans le flux de caractères d'origine. Si nous modifions le tampon dans CharTermAttribute pour qu'il pointe uniquement vers une sous-chaîne du jeton, nous devons ajuster ces décalages en conséquence.

Vous vous demandez peut-être pourquoi l'API de manipulation des flux de jetons est si compliquée et, en particulier, pourquoi nous ne pouvons pas simplement faire quelque chose comme String#split sur les jetons entrants. En effet, Lucene est conçu pour une indexation à grande vitesse et à faible surcharge, grâce à laquelle les tokenizers et les filtres intégrés peuvent parcourir rapidement des gigaoctets de texte tout en n'utilisant que des mégaoctets de mémoire. Pour y parvenir, peu ou pas d'allocations sont effectuées lors de la tokenisation et du filtrage, et donc les instances d' Attribute mentionnées ci-dessus sont destinées à être allouées une fois et réutilisées. Si vos tokenizers et filtres sont écrits de cette manière et minimisent leurs propres allocations, vous pouvez personnaliser Lucene sans compromettre les performances.

Avec tout cela à l'esprit, voyons comment implémenter un filtre qui prend un jeton tel que ["Hello] , et produit les deux jetons, ["] et [Hello] :

 public class QuotationTokenFilter extends TokenFilter { private static final char QUOTE = '"'; public static final String QUOTE_START_TYPE = "start_quote"; public static final String QUOTE_END_TYPE = "end_quote"; private final OffsetAttribute offsetAttr = addAttribute(OffsetAttribute.class); private final TypeAttribute typeAttr = addAttribute(TypeAttribute.class); private final CharTermAttribute termBufferAttr = addAttribute(CharTermAttribute.class);

Nous commençons par obtenir des références à certains des attributs que nous avons vus précédemment. Nous suffixons les noms de champs avec "Attr" afin que ce soit clair plus tard lorsque nous nous y référerons. Il est possible que certaines implémentations de Tokenizer ne fournissent pas ces attributs, nous utilisons addAttribute pour obtenir nos références. addAttribute créera une instance d'attribut si elle est manquante, sinon récupérez une référence partagée à l'attribut de ce type. Notez que Lucene n'autorise pas plusieurs instances du même type d'attribut à la fois.

 private boolean emitExtraToken; private int extraTokenStartOffset, extraTokenEndOffset; private String extraTokenType;

Étant donné que notre filtre introduira un nouveau jeton qui n'était pas présent dans le flux d'origine, nous avons besoin d'un emplacement pour enregistrer l'état de ce jeton entre les appels à incrementToken . Étant donné que nous divisons un jeton existant en deux, il suffit de connaître uniquement les décalages et le type du nouveau jeton. Nous avons également un indicateur qui nous indique si le prochain appel à incrementToken émettra ce jeton supplémentaire. Lucene fournit en fait une paire de méthodes, captureState et restoreState , qui le feront pour vous. Mais ces méthodes impliquent l'allocation d'un objet State et peuvent en fait être plus délicates que de simplement gérer cet état vous-même, nous éviterons donc de les utiliser.

 @Override public void reset() throws IOException { emitExtraToken = false; extraTokenStartOffset = -1; extraTokenEndOffset = -1; extraTokenType = null; super.reset(); }

Dans le cadre de son évitement agressif de l'allocation, Lucene peut réutiliser les instances de filtre. Dans cette situation, on s'attend à ce qu'un appel à reset remette le filtre dans son état initial. Donc ici, nous réinitialisons simplement nos champs de jeton supplémentaires.

 @Override public boolean incrementToken() throws IOException { if (emitExtraToken) { advanceToExtraToken(); emitExtraToken = false; return true; } ...

Nous arrivons maintenant aux parties intéressantes. Lorsque notre implémentation d' incrementToken est appelée, nous avons la possibilité de ne pas appeler incrementToken à l'étape précédente du pipeline. Ce faisant, nous introduisons effectivement un nouveau jeton, car nous ne tirons pas de jeton du Tokenizer .

Au lieu de cela, nous appelons advanceToExtraToken pour configurer les attributs de notre jeton supplémentaire, emitExtraToken la valeur false pour éviter cette branche lors du prochain appel, puis renvoyons true , ce qui indique qu'un autre jeton est disponible.

 @Override public boolean incrementToken() throws IOException { ... (emit extra token) ... boolean hasNext = input.incrementToken(); if (hasNext) { char[] buffer = termBufferAttr.buffer(); if (termBuffer.length() > 1) { if (buffer[0] == QUOTE) { splitTermQuoteFirst(); } else if (buffer[termBuffer.length() - 1] == QUOTE) { splitTermWordFirst(); } } else if (termBuffer.length() == 1) { if (buffer[0] == QUOTE) { typeAttr.setType(QUOTE_END_TYPE); } } } return hasNext; }

Le reste d' incrementToken fera l'une des trois choses différentes. Rappelez-vous que termBufferAttr est utilisé pour inspecter le contenu du jeton passant par le canal :

  1. Si nous avons atteint la fin du flux de jetons (c'est-à-dire hasNext est faux), nous avons terminé et revenons simplement.

  2. Si nous avons un jeton de plus d'un caractère et que l'un de ces caractères est un guillemet, nous divisons le jeton.

  3. Si le jeton est un guillemet solitaire, nous supposons qu'il s'agit d'un guillemet final. Pour comprendre pourquoi, [He told us to "go back the way we came."] que les guillemets de début apparaissent toujours à gauche d'un mot (c. [He told us to "go back the way we came."] ). Dans ces cas, le guillemet de fin sera déjà un jeton séparé, et nous n'avons donc qu'à définir son type.

splitTermQuoteFirst et splitTermWordFirst attributs pour faire du jeton actuel un mot ou une citation, et configureront les champs "supplémentaires" pour permettre à l'autre moitié d'être consommée plus tard. Les deux méthodes sont similaires, nous allons donc examiner simplement splitTermQuoteFirst :

 private void splitTermQuoteFirst() { int origStart = offsetAttr.startOffset(); int origEnd = offsetAttr.endOffset(); offsetAttr.setOffset(origStart, origStart + 1); typeAttr.setType(QUOTE_START_TYPE); termBufferAttr.setLength(1); prepareExtraTerm(origStart + 1, origEnd, TypeAttribute.DEFAULT_TYPE); }

Parce que nous voulons diviser ce jeton avec le guillemet apparaissant en premier dans le flux, nous tronquons le tampon en fixant la longueur à un (c'est-à-dire, un caractère ; à savoir, le guillemet). Nous ajustons les décalages en conséquence (c'est-à-dire pointant vers la citation dans le document d'origine) et définissons également le type comme une citation de départ.

prepareExtraTerm définira les champs extra* et définir emitExtraToken sur true. Il est appelé avec des décalages pointant vers le jeton "extra" (c'est-à-dire le mot suivant le guillemet).

L'intégralité de QuotationTokenFilter est disponible sur GitHub.

Soit dit en passant, bien que ce filtre ne produise qu'un jeton supplémentaire, cette approche peut être étendue pour introduire un nombre arbitraire de jetons supplémentaires. Remplacez simplement les champs extra* par une collection ou, mieux encore, un tableau de longueur fixe s'il y a une limite au nombre de jetons supplémentaires pouvant être produits. Voir SynonymFilter et sa classe interne PendingInput pour un exemple.

Consommer des jetons de devis et marquer le dialogue

Maintenant que nous avons déployé tous ces efforts pour ajouter ces citations au flux de jetons, nous pouvons les utiliser pour délimiter des sections de dialogue dans le texte.

Étant donné que notre objectif final est d'ajuster les résultats de la recherche selon que les termes font partie ou non du dialogue, nous devons joindre des métadonnées à ces termes. Lucene fournit PayloadAttribute à cet effet. Les charges utiles sont des tableaux d'octets qui sont stockés avec les termes dans l'index et peuvent être lus plus tard lors d'une recherche. Cela signifie que notre indicateur occupera inutilement un octet entier, de sorte que des charges utiles supplémentaires pourraient être implémentées sous forme d'indicateurs de bits pour économiser de l'espace.

Ci-dessous se trouve un nouveau filtre, DialoguePayloadTokenFilter , qui est ajouté à la toute fin du pipeline d'analyse. Il attache la charge utile indiquant si le jeton fait partie ou non du dialogue.

 public class DialoguePayloadTokenFilter extends TokenFilter { private final TypeAttribute typeAttr = getAttribute(TypeAttribute.class); private final PayloadAttribute payloadAttr = addAttribute(PayloadAttribute.class); private static final BytesRef PAYLOAD_DIALOGUE = new BytesRef(new byte[] { 1 }); private static final BytesRef PAYLOAD_NOT_DIALOGUE = new BytesRef(new byte[] { 0 }); private boolean withinDialogue; protected DialoguePayloadTokenFilter(TokenStream input) { super(input); } @Override public void reset() throws IOException { this.withinDialogue = false; super.reset(); } @Override public boolean incrementToken() throws IOException { boolean hasNext = input.incrementToken(); while(hasNext) { boolean isStartQuote = QuotationTokenFilter .QUOTE_START_TYPE.equals(typeAttr.type()); boolean isEndQuote = QuotationTokenFilter .QUOTE_END_TYPE.equals(typeAttr.type()); if (isStartQuote) { withinDialogue = true; hasNext = input.incrementToken(); } else if (isEndQuote) { withinDialogue = false; hasNext = input.incrementToken(); } else { break; } } if (hasNext) { payloadAttr.setPayload(withinDialogue ? PAYLOAD_DIALOGUE : PAYLOAD_NOT_DIALOGUE); } return hasNext; } }

Étant donné que ce filtre n'a besoin de maintenir qu'un seul élément d'état, withinDialogue , c'est beaucoup plus simple. Un guillemet de début indique que nous sommes maintenant dans une section de dialogue, tandis qu'un guillemet de fin indique que la section de dialogue est terminée. Dans les deux cas, le jeton de devis est supprimé en effectuant un deuxième appel à incrementToken , donc en fait, les jetons de début de devis ou de fin de devis ne dépassent jamais cette étape dans le pipeline.

Par exemple, DialoguePayloadTokenFilter transformera le flux de jetons :

 [the], [program], [printed], ["], [hello], [world], ["]`

dans ce nouveau flux :

 [the][0], [program][0], [printed][0], [hello][1], [world][1]

Lier Tokenizers et filtres ensemble

Un Analyzer est responsable de l'assemblage du pipeline d'analyse, généralement en combinant un Tokenizer avec une série de TokenFilter s. Analyzer peuvent également définir comment ce pipeline est réutilisé entre les analyses. Nous n'avons pas à nous en soucier car nos composants ne nécessitent rien d'autre qu'un appel à reset() entre les utilisations, ce que Lucene fera toujours. Nous avons juste besoin de faire l'assemblage en implémentant Analyzer#createComponents(String) :

 public class DialogueAnalyzer extends Analyzer { @Override protected TokenStreamComponents createComponents(String fieldName) { QuotationTokenizer tokenizer = new QuotationTokenizer(); TokenFilter filter = new QuotationTokenFilter(tokenizer); filter = new LowerCaseFilter(filter); filter = new StopFilter(filter, StopAnalyzer.ENGLISH_STOP_WORDS_SET); filter = new DialoguePayloadTokenFilter(filter); return new TokenStreamComponents(tokenizer, filter); } }

Comme nous l'avons vu précédemment, les filtres contiennent une référence à l'étape précédente du pipeline, c'est ainsi que nous les instancions. Nous glissons également quelques filtres de StandardAnalyzer : LowerCaseFilter et StopFilter . Ces deux doivent venir après QuotationTokenFilter pour s'assurer que toutes les citations ont été séparées. Nous pouvons être plus flexibles dans notre placement de DialoguePayloadTokenFilter , car n'importe où après QuotationTokenFilter fera l'affaire. Nous l'avons mis après StopFilter pour éviter de perdre du temps à injecter la charge utile du dialogue dans des mots vides qui seront finalement supprimés.

Voici une visualisation de notre nouveau pipeline en action (moins les parties du pipeline standard que nous avons supprimées ou déjà vues) :

Nouvelle visualisation de pipeline dans apache lucene

DialogueAnalyzer peut maintenant être utilisé comme n'importe quel autre Analyzer d'actions, et maintenant nous pouvons construire l'index et passer à la recherche.

Recherche plein texte du dialogue

Si nous voulions uniquement rechercher un dialogue, nous aurions pu simplement supprimer tous les jetons en dehors d'une citation et nous aurions terminé. Au lieu de cela, en laissant tous les jetons d'origine intacts, nous nous sommes donné la possibilité soit d'effectuer des requêtes qui prennent en compte le dialogue, soit de traiter le dialogue comme n'importe quelle autre partie du texte.

Les bases de l'interrogation d'un index Lucene sont bien documentées. Pour nos besoins, il suffit de savoir que les requêtes sont composées d'objets Term collés avec des opérateurs tels que MUST ou SHOULD , ainsi que des documents de correspondance basés sur ces termes. Les documents correspondants sont ensuite notés en fonction d'un objet de Similarity configurable, et ces résultats peuvent être triés par score, filtrés ou limités. Par exemple, Lucene nous permet de faire une requête pour les dix premiers documents qui doivent contenir à la fois les termes [hello] et [world] .

La personnalisation des résultats de recherche en fonction du dialogue peut être effectuée en ajustant le score d'un document en fonction de la charge utile. Le premier point d'extension pour cela sera dans Similarity , qui est responsable de la pondération et de la notation des termes correspondants.

Similitude et notation

Les requêtes utiliseront, par défaut, DefaultSimilarity , qui pondère les termes en fonction de leur fréquence d'apparition dans un document. C'est un bon point d'extension pour ajuster les pondérations, nous l'étendons donc pour noter également les documents en fonction de la charge utile. La méthode DefaultSimilarity#scorePayload est prévue à cet effet :

 public final class DialogueAwareSimilarity extends DefaultSimilarity { @Override public float scorePayload(int doc, int start, int end, BytesRef payload) { if (payload.bytes[payload.offset] == 0) { return 0.0f; } return 1.0f; } }

DialogueAwareSimilarity note simplement les charges utiles non-dialogue comme zéro. Comme chaque Term peut être mis en correspondance plusieurs fois, il aura potentiellement plusieurs scores de charge utile. L'interprétation de ces scores jusqu'à l'implémentation de Query .

Portez une attention particulière au BytesRef contenant la charge utile : nous devons vérifier l'octet à offset , car nous ne pouvons pas supposer que le tableau d'octets est la même charge utile que celle que nous avons stockée précédemment. Lors de la lecture de l'index, Lucene ne gaspillera pas de mémoire en allouant un tableau d'octets séparé uniquement pour l'appel à scorePayload , nous obtenons donc une référence dans un tableau d'octets existant. Lors du codage avec l'API Lucene, il est utile de garder à l'esprit que la performance est la priorité, bien avant la commodité du développeur.

Maintenant que nous avons notre nouvelle implémentation de Similarity , elle doit ensuite être définie sur l' IndexSearcher utilisé pour exécuter les requêtes :

 IndexSearcher searcher = new IndexSearcher(... reader for index ...); searcher.setSimilarity(new DialogueAwareSimilarity());

Requêtes et termes

Maintenant que notre IndexSearcher peut évaluer les charges utiles, nous devons également construire une requête sensible à la charge utile. PayloadTermQuery peut être utilisé pour faire correspondre un seul Term tout en vérifiant les charges utiles de ces correspondances :

 PayloadTermQuery helloQuery = new PayloadTermQuery(new Term("body", "hello"), new AveragePayloadFunction());

Cette requête correspond au terme [hello] dans le champ du corps (rappelez-vous que c'est là que nous plaçons le contenu du document). Nous devons également fournir une fonction pour calculer le score final de la charge utile à partir de toutes les correspondances de terme, nous branchons donc AveragePayloadFunction , qui calcule la moyenne de tous les scores de la charge utile. Par exemple, si le terme [hello] apparaît deux fois à l'intérieur du dialogue et une fois à l'extérieur du dialogue, le score de charge utile final sera ²⁄₃. Ce score de charge utile final est multiplié par celui fourni par DefaultSimilarity pour l'ensemble du document.

Nous utilisons une moyenne parce que nous souhaitons minimiser les résultats de recherche où de nombreux termes apparaissent en dehors du dialogue, et produire un score de zéro pour les documents sans aucun terme dans le dialogue.

Nous pouvons également composer plusieurs objets PayloadTermQuery à l'aide d'un BooleanQuery si nous voulons rechercher plusieurs termes contenus dans le dialogue (notez que l'ordre des termes n'est pas pertinent dans cette requête, bien que d'autres types de requêtes soient sensibles à la position) :

 PayloadTermQuery worldQuery = new PayloadTermQuery(new Term("body", "world"), new AveragePayloadFunction()); BooleanQuery query = new BooleanQuery(); query.add(helloQuery, Occur.MUST); query.add(worldQuery, Occur.MUST);

Lorsque cette requête est exécutée, nous pouvons voir comment la structure de la requête et l'implémentation de la similarité fonctionnent ensemble :

pipeline d'analyse de dialogue lucene

Exécution et explication de la requête

Pour exécuter la requête, nous la transmettons à IndexSearcher :

 TopScoreDocCollector collector = TopScoreDocCollector.create(10); searcher.search(query, new PositiveScoresOnlyCollector(collector)); TopDocs topDocs = collector.topDocs();

Les objets Collector sont utilisés pour préparer la collection de documents correspondants.

les collecteurs peuvent être composés pour obtenir une combinaison de tri, de limitation et de filtrage. Pour obtenir, par exemple, les dix meilleurs documents de notation qui contiennent au moins un terme dans le dialogue, nous combinons TopScoreDocCollector et PositiveScoresOnlyCollector . Ne prendre que des scores positifs garantit que les correspondances de score zéro (c'est-à-dire celles sans termes dans le dialogue) sont filtrées.

Pour voir cette requête en action, nous pouvons l'exécuter, puis utiliser IndexSearcher#explain pour voir comment les documents individuels ont été notés :

 for (ScoreDoc result : topDocs.scoreDocs) { Document doc = searcher.doc(result.doc, Collections.singleton("title")); System.out.println("--- document " + doc.getField("title").stringValue() + " ---"); System.out.println(this.searcher.explain(query, result.doc)); }

Ici, nous parcourons les ID de document dans les TopDocs obtenus par la recherche. Nous utilisons également IndexSearcher#doc pour récupérer le champ de titre à afficher. Pour notre requête "hello" , cela se traduit par :

 --- Document whelv10.txt --- 0.072256625 = (MATCH) btq, product of: 0.072256625 = weight(body:hello in 7336) [DialogueAwareSimilarity], result of: 0.072256625 = fieldWeight in 7336, product of: 2.345208 = tf(freq=5.5), with freq of: 5.5 = phraseFreq=5.5 3.1549776 = idf(docFreq=2873, maxDocs=24796) 0.009765625 = fieldNorm(doc=7336) 1.0 = AveragePayloadFunction.docScore() --- Document daved10.txt --- 0.061311778 = (MATCH) btq, product of: 0.061311778 = weight(body:hello in 6873) [DialogueAwareSimilarity], result of: 0.061311778 = fieldWeight in 6873, product of: 3.3166249 = tf(freq=11.0), with freq of: 11.0 = phraseFreq=11.0 3.1549776 = idf(docFreq=2873, maxDocs=24796) 0.005859375 = fieldNorm(doc=6873) 1.0 = AveragePayloadFunction.docScore() ...

Bien que la sortie soit chargée de jargon, nous pouvons voir comment notre implémentation personnalisée de Similarity a été utilisée dans la notation et comment la fonction MaxPayloadFunction a produit un multiplicateur de 1.0 pour ces correspondances. Cela implique que la charge utile a été chargée et notée, et que toutes les correspondances de "Hello" se sont produites dans le dialogue, et donc ces résultats sont juste en haut où nous les attendons.

Il convient également de souligner que l'index du projet Gutenberg, avec les charges utiles, atteint une taille de près de quatre gigaoctets, et pourtant, sur ma modeste machine de développement, les requêtes se produisent instantanément. Nous n'avons sacrifié aucune vitesse pour atteindre nos objectifs de recherche.

Emballer

Lucene est une bibliothèque de recherche en texte intégral puissante et spécialement conçue qui prend un flux brut de caractères, les regroupe dans des jetons et les conserve sous forme de termes dans un index. Il peut rapidement interroger cet index et fournir des résultats classés, et offre de nombreuses possibilités d'extension tout en maintenant l'efficacité.

En utilisant Lucene directement dans nos applications ou dans le cadre d'un serveur, nous pouvons effectuer des recherches en texte intégral en temps réel sur des gigaoctets de contenu. De plus, grâce à une analyse et une notation personnalisées, nous pouvons tirer parti des fonctionnalités spécifiques à un domaine dans nos documents pour améliorer la pertinence des résultats ou des requêtes personnalisées.

Les listes complètes de code pour ce didacticiel Lucene sont disponibles sur GitHub. Le référentiel contient deux applications : LuceneIndexerApp pour créer l'index et LuceneQueryApp pour effectuer des requêtes.

Le corpus du projet Gutenberg, qui peut être obtenu sous forme d'image disque via BitTorrent, contient de nombreux livres à lire (soit avec Lucene, soit simplement à l'ancienne).

Bonne indexation !