Apache Lucene est une bibliothèque Java utilisée pour la recherche de 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 nombreuses, elles sont destinées à être utilisées par développeurs de bases de données sur un corpus générique de texte. Si vos documents ont une structure ou un type de contenu spécifique, vous pouvez en profiter pour améliorer la qualité de la recherche et la capacité de requête.
A titre d'exemple de ce type de personnalisation, dans ce tutoriel Lucene nous allons indexer le corpus de 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 dialogue dans ces romans. Ni Lucene, Elasticsearch, ni Solr ne fournissent des 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 doit commencer notre personnalisation.
La Analyse Lucene JavaDoc fournit une bonne vue d'ensemble de toutes les pièces mobiles dans le pipeline d'analyse de texte.
À un niveau élevé, vous pouvez considérer le pipeline d'analyse comme consommant un flux brut de caractères au début et produisant des «termes», correspondant approximativement à des mots, à la fin.
Le pipeline d'analyse standard peut être visualisé En tant que tel:
Nous verrons comment personnaliser ce pipeline pour reconnaître les régions de texte marquées par des guillemets, ce que j'appellerai dialogue, puis augmenter les correspondances qui se produisent lors de la recherche dans ces régions.
Lorsque les documents sont initialement ajoutés à l'index, les caractères sont lus à partir d'un Java Flux d'entrée , et ainsi ils peuvent provenir de fichiers, de bases de données, d'appels de service Web, etc. Pour créer un index pour le projet Gutenberg, nous téléchargeons les livres électroniques et créons 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 très fréquentés, 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);
On voit que chaque e-book correspondra à un seul Lucene Document
Ainsi, plus tard, nos résultats de recherche seront une liste de livres correspondants. Store.YES
indique que nous stockons le Titre champ, qui est juste le nom du fichier. Nous ne voulons pas stocker le corps de l'ebook, cependant, 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
. Le IndexWriter
tire des jetons de la fin du pipeline. Cette extraction revient dans le tube jusqu'à ce que la première étape, le Tokenizer
, lit à partir du InputStream
.
Notez également que nous ne fermons pas le flux, car Lucene s'en charge pour nous.
Le Lucene StandardTokenizer jette la ponctuation, et donc notre personnalisation commencera ici, car nous devons préserver les guillemets.
La documentation pour 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
, ce 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) }
Étant donné un flux d'entrée de [He said, 'Good day'.]
, les jetons produits seraient [He]
, [said]
, ['Good]
, [day']
s-corporation vs c-corporation
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 citation, mais Tokenizer
est également concerné par des détails fastidieux et faciles à bousiller tels que la mise en mémoire tampon et l'analyse, il est donc préférable de conserver votre Tokenizer
simple et nettoyez le flux de jetons plus loin dans le pipeline.
Après le tokenizer vient une série de TokenFilter
objets. Notez, d'ailleurs, que filtre est un peu trompeur, car a TokenFilter
peut ajouter, supprimer ou modifier des jetons.
La plupart des classes de filtres fournies par Lucene attendent des mots uniques, il n'est donc pas nécessaire que nos jetons mixtes de mots et de citations y affluent. Ainsi, la prochaine personnalisation de notre didacticiel Lucene doit être l'introduction d'un filtre qui nettoiera la sortie de QuotationTokenizer
.
Ce nettoyage impliquera la production d'un supplément commencer le devis jeton si la citation apparaît au début d'un mot, ou un fin de citation jeton si le devis apparaît à la fin. Nous mettrons de côté le traitement des mots entre guillemets simples pour plus de simplicité.
Créer un TokenFilter
La sous-classe implique l'implémentation d'une méthode: incrementToken
. Cette méthode doit appeler incrementToken
sur le filtre précédent dans le tube, puis manipulez les résultats de cet appel pour effectuer le travail dont le filtre est responsable. Les résultats de incrementToken
sont disponibles via Attribute
objets, qui décrivent l'état actuel du traitement des jetons. Après notre implémentation de incrementToken
renvoie, on s'attend à ce que les attributs aient été manipulés pour configurer le jeton pour le filtre suivant (ou l'index si nous sommes à la fin du tube).
Les attributs qui nous intéressent à ce stade du pipeline sont:
CharTermAttribute
: Contient un char[]
tampon contenant les caractères du jeton actuel. Nous devrons manipuler cela pour supprimer le devis ou pour produire un jeton de devis.
passer par l'entreprise individuelle vs s corp
TypeAttribute
: Contient le «type» du jeton actuel. Comme 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 changeons le tampon dans CharTermAttribute
pour ne pointer que sur 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 à haute vitesse et à faible coût, grâce à laquelle les jetons et filtres intégrés peuvent rapidement mâcher des gigaoctets de texte tout en n'utilisant que des mégaoctets de mémoire. Pour ce faire, peu ou pas d'allocations sont effectuées lors de la tokenisation et du filtrage, et donc Attribute
les instances mentionnées ci-dessus sont destinées à être allouées une seule fois et réutilisées. Si vos jetons 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 y ferons référence. Il est possible que certains Tokenizer
les implémentations ne fournissent pas ces attributs, nous utilisons donc 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 fera pour vous. Mais ces méthodes impliquent l'attribution d'un State
objet, et peut en fait être plus compliqué 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
remettra le filtre dans son état initial. Donc ici, nous réinitialisons simplement nos champs de jetons supplémentaires.
@Override public boolean incrementToken() throws IOException { if (emitExtraToken) { advanceToExtraToken(); emitExtraToken = false; return true; } ...
Nous arrivons maintenant aux éléments intéressants. Lorsque notre implémentation de incrementToken
est appelé, nous avons la possibilité de ne pas appeler incrementToken
au stade antérieur du pipeline. Ce faisant, nous introduisons effectivement un nouveau jeton, car nous ne tirons pas de jeton depuis Tokenizer
.
Au lieu de cela, nous appelons advanceToExtraToken
pour configurer les attributs de notre jeton supplémentaire, définissez emitExtraToken
à false pour éviter cette branche lors du prochain appel, puis retournez 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 de incrementToken
fera une des trois choses différentes. Rappelez-vous que termBufferAttr
est utilisé pour inspecter le contenu du jeton passant par le tuyau:
Si nous avons atteint la fin du flux de jetons (c'est-à-dire que hasNext
est faux), nous avons terminé et retournons simplement.
Si nous avons un jeton de plus d'un caractère et que l'un de ces caractères est une citation, nous divisons le jeton.
Si le jeton est une citation solitaire, nous supposons qu'il s'agit d'une citation de fin. Pour comprendre pourquoi, notez que les guillemets de début apparaissent toujours à gauche d'un mot (c'est-à-dire sans ponctuation intermédiaire), alors que les guillemets de fin peuvent suivre la ponctuation (comme dans la phrase, [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
définira des attributs pour que le jeton actuel soit un mot ou une citation, et configurera les champs «extra» pour permettre à l'autre moitié d'être consommée plus tard. Les deux méthodes sont similaires, nous allons donc examiner uniquement 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 dans le flux en premier, nous tronquons le tampon en définissant la longueur sur un (c'est-à-dire un caractère; à savoir, le guillemet). Nous ajustons les décalages en conséquence (c'est-à-dire en pointant vers le devis dans le document d'origine) et définissons également le type comme un devis de départ.
prepareExtraTerm
définira le extra*
champs et définir emitExtraToken
à vrai. Il est appelé avec des décalages pointant vers le jeton «supplémentaire» (c'est-à-dire le mot qui suit la citation).
Tutoriel de l'API Web de base .net
L'intégralité de QuotationTokenFilter
est disponible sur GitHub .
En passant, bien que ce filtre ne produise qu'un seul jeton supplémentaire, cette approche peut être étendue pour introduire un nombre arbitraire de jetons supplémentaires. Remplacez simplement le extra*
champs avec une collection ou, mieux encore, un tableau de longueur fixe s'il y a une limite sur le nombre de jetons supplémentaires qui peuvent être produits. Voir SynonymFilter
et ses PendingInput
classe interne pour un exemple de ceci.
Maintenant que nous avons déployé tous ces efforts pour ajouter ces guillemets 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 recherche en fonction du fait que les termes font ou non partie du dialogue, nous devons joindre des métadonnées à ces termes. Lucene fournit PayloadAttribute
dans ce but. Les charges utiles sont des tableaux d'octets qui sont stockés à côté des 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.
Vous trouverez ci-dessous un nouveau filtre, DialoguePayloadTokenFilter
, qui est ajouté à la toute fin du pipeline d'analyse. Il attache la charge utile indiquant si le jeton fait ou non partie 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; } }
Puisque ce filtre n'a besoin que de maintenir un seul é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 citation est supprimé en effectuant un deuxième appel à incrementToken
, donc en effet, commencer le devis ou fin de citation les jetons ne dépassent jamais cette étape du pipeline.
Par exemple, DialoguePayloadTokenFilter
transformera le jeton:
[the], [program], [printed], ['], [hello], [world], [']`
dans ce nouveau flux:
[the][0], [program][0], [printed][0], [hello][1], [world][1]
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
s peut également définir la manière dont ce pipeline est réutilisé entre les analyses. Nous n'avons pas besoin de nous en préoccuper 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 vous assurer que les guillemets ont été séparés. Nous pouvons être plus flexibles dans notre placement de DialoguePayloadTokenFilter
, puisque n'importe où après QuotationTokenFilter
ça ira. Nous le mettons après StopFilter
pour éviter de perdre du temps à injecter la charge utile de dialogue dans mots d'arrêt qui sera finalement supprimé.
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 désormais être utilisé comme n'importe quel autre stock Analyzer
serait, et maintenant nous pouvons construire l'index et passer à la recherche.
Si nous voulions uniquement rechercher le 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é d'effectuer des requêtes qui prennent en compte le dialogue, ou de traiter le dialogue comme n'importe quelle autre partie du texte.
Les bases de l'interrogation d'un index Lucene sont bien documenté . Pour nos besoins, il suffit de savoir que les requêtes sont composées de Term
objets 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 sur la base d'un Similarity
configurable | objet, et ces résultats peuvent être classé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 les deux 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.
Les requêtes utilisent 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 l'ajustement des poids, donc nous l'étendons pour noter également les documents en fonction de la charge utile. La méthode DefaultSimilarity#scorePayload
est prévu à 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
marque simplement les charges utiles sans dialogue comme zéro. Comme chaque Term
peut être apparié plusieurs fois, il aura potentiellement plusieurs scores de charge utile. L'interprétation de ces scores jusqu'au Query
la mise en oeuvre.
Faites très attention 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 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 nouveau Similarity
implémentation, il doit alors être défini sur IndexSearcher
utilisé pour exécuter des requêtes:
IndexSearcher searcher = new IndexSearcher(... reader for index ...); searcher.setSimilarity(new DialogueAwareSimilarity());
Maintenant que notre IndexSearcher
peut marquer les charges utiles, nous devons également construire une requête qui tient compte de la charge utile. PayloadTermQuery
peut être utilisé pour faire correspondre un seul Term
tout en vérifiant également 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 corps field (rappelez-vous que c'est ici que nous mettons 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 termes, nous connectons donc AveragePayloadFunction
, qui fait la moyenne de tous les scores de charge utile. Par exemple, si le terme [hello]
se produit à l'intérieur du dialogue deux fois et à l'extérieur du dialogue une fois, le score final de la charge utile sera de ²⁄₃. Ce score final de charge utile est multiplié par celui fourni par DefaultSimilarity
pour l'ensemble du document.
Nous utilisons une moyenne parce que nous souhaitons désaccentuer 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.
On peut aussi composer plusieurs PayloadTermQuery
objets utilisant 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 les 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:
Pour exécuter la requête, nous la transmettons au IndexSearcher
:
TopScoreDocCollector collector = TopScoreDocCollector.create(10); searcher.search(query, new PositiveScoresOnlyCollector(collector)); TopDocs topDocs = collector.topDocs();
Collector
les objets sont utilisés pour préparer la collection de documents correspondants.
les collecteurs peuvent être composés pour réaliser une combinaison de tri, de limitation et de filtrage. Pour obtenir, par exemple, les dix meilleurs documents qui contiennent au moins un terme dans le dialogue, nous combinons TopScoreDocCollector
et PositiveScoresOnlyCollector
. Prendre uniquement des scores positifs garantit que les correspondances de score nul (c'est-à-dire celles qui n'ont pas de 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 le TopDocs
obtenu par la recherche. Nous utilisons également IndexSearcher#doc
pour récupérer le champ de titre à afficher. Pour notre requête de '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 personnalisé Similarity
l'implémentation a été utilisée dans le scoring, et comment MaxPayloadFunction
produit un multiplicateur de 1.0
pour ces matchs. Cela implique que la charge utile a été chargée et notée, et toutes les correspondances de 'Hello'
eu lieu dans le dialogue, et donc ces résultats sont tout en haut là où nous les attendons.
Il convient également de souligner que l'indice du projet Gutenberg, avec les charges utiles, atteint 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.
Lucene est une puissante bibliothèque de recherche de texte intégral conçue à cet effet qui prend un flux brut de caractères, les regroupe en jetons et les conserve en tant que 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 au domaine dans nos documents pour améliorer la pertinence des résultats ou des requêtes personnalisées.
comment dessiner dans le traitement
Les listes de codes complètes pour ce didacticiel Lucene sont disponible sur GitHub . Le dépôt contient deux applications: LuceneIndexerApp
pour construire l'index, et LuceneQueryApp
pour effectuer des requêtes.
Le corpus du projet Gutenberg, qui peut être obtenu en tant qu'image disque via BitTorrent , contient beaucoup de livres qui valent la peine d'être lus (soit avec Lucene, soit juste à l'ancienne).
Bonne indexation!