Recherche plein texte de dialogues avec Apache Lucene : un tutoriel
Publié: 2022-03-11Apache 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.
À 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 :
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 tamponchar[]
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 dansCharTermAttribute
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 :
Si nous avons atteint la fin du flux de jetons (c'est-à-dire
hasNext
est faux), nous avons terminé et revenons simplement.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.
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) :
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 :
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 !