Introduction à Apache Lucene

1. Vue d'ensemble

Apache Lucene est un moteur de recherche en texte intégral qui peut être utilisé à partir de divers langages de programmation.

Dans cet article, nous essaierons de comprendre les concepts de base de la bibliothèque et de créer une application simple.

2. Configuration de Maven

Pour commencer, ajoutons d'abord les dépendances nécessaires:

 org.apache.lucene lucene-core 7.1.0 

La dernière version peut être trouvée ici.

De plus, pour analyser nos requêtes de recherche, nous aurons besoin de:

 org.apache.lucene lucene-queryparser 7.1.0 

Vérifiez la dernière version ici.

3. Concepts de base

3.1. Indexage

En termes simples, Lucene utilise une «indexation inversée» des données - au lieu de mapper des pages sur des mots-clés, elle mappe des mots-clés sur des pages comme un glossaire à la fin de n'importe quel livre.

Cela permet des réponses de recherche plus rapides, car il recherche dans un index, au lieu de rechercher directement dans le texte.

3.2. Des documents

Ici, un document est une collection de champs et chaque champ est associé à une valeur.

Les indices sont généralement constitués d'un ou plusieurs documents, et les résultats de la recherche sont des ensembles de documents les mieux adaptés.

Ce n'est pas toujours un document en texte brut, il peut également s'agir d'une table de base de données ou d'une collection.

3.3. Des champs

Les documents peuvent avoir des données de champ, où un champ est généralement une clé contenant une valeur de données:

title: Goodness of Tea body: Discussing goodness of drinking herbal tea...

Notez qu'ici le titre et le corps sont des champs et peuvent être recherchés ensemble ou individuellement.

3.4. Une analyse

Une analyse convertit le texte donné en unités plus petites et précises pour faciliter la recherche.

Le texte passe par diverses opérations d'extraction de mots-clés, de suppression de mots courants et de ponctuations, de changement de mots en minuscules, etc.

Pour cela, il existe plusieurs analyseurs intégrés:

  1. StandardAnalyzer - analyse basée sur la grammaire de base, supprime les mots vides tels que «a», «an», etc. Convertit également en minuscules
  2. SimpleAnalyzer - casse le texte en fonction du caractère sans lettre et convertit en minuscules
  3. WhiteSpaceAnalyzer - casse le texte en fonction des espaces blancs

Nous avons également d'autres analyseurs à utiliser et à personnaliser.

3.5. Recherche

Une fois qu'un index est construit, nous pouvons rechercher cet index à l'aide d'une requête et d'un IndexSearcher. Le résultat de la recherche est généralement un jeu de résultats contenant les données extraites.

Notez qu'un IndexWritter est responsable de la création de l'index et un IndexSearcher pour rechercher l'index.

3.6. Syntaxe de la requête

Lucene fournit une syntaxe de requête très dynamique et facile à écrire.

Pour rechercher un texte libre, nous utiliserions simplement une chaîne de texte comme requête.

Pour rechercher un texte dans un champ particulier, nous utiliserions:

fieldName:text eg: title:tea

Recherches par plage:

timestamp:[1509909322,1572981321] 

Nous pouvons également effectuer une recherche à l'aide de caractères génériques:

dri?nk

rechercherait un seul caractère à la place du caractère générique «?»

d*k

recherche des mots commençant par «d» et se terminant par «k», avec plusieurs caractères entre les deux.

uni*

trouvera des mots commençant par «uni».

Nous pouvons également combiner ces requêtes et créer des requêtes plus complexes. Et incluez un opérateur logique comme AND, NOT, OR:

title: "Tea in breakfast" AND "coffee"

En savoir plus sur la syntaxe des requêtes ici.

4. Une application simple

Créons une application simple et indexons quelques documents.

Tout d'abord, nous allons créer un index en mémoire et y ajouter des documents:

... Directory memoryIndex = new RAMDirectory(); StandardAnalyzer analyzer = new StandardAnalyzer(); IndexWriterConfig indexWriterConfig = new IndexWriterConfig(analyzer); IndexWriter writter = new IndexWriter(memoryIndex, indexWriterConfig); Document document = new Document(); document.add(new TextField("title", title, Field.Store.YES)); document.add(new TextField("body", body, Field.Store.YES)); writter.addDocument(document); writter.close(); 

Here, we create a document with TextField and add them to the index using the IndexWriter. The third argument in the TextField constructor indicates whether the value of the field is also to be stored or not.

Analyzers are used to split the data or text into chunks, and then filter out the stop words from them. Stop words are words like ‘a', ‘am', ‘is' etc. These completely depend on the given language.

Next, let's create a search query and search the index for the added document:

public List searchIndex(String inField, String queryString) { Query query = new QueryParser(inField, analyzer) .parse(queryString); IndexReader indexReader = DirectoryReader.open(memoryIndex); IndexSearcher searcher = new IndexSearcher(indexReader); TopDocs topDocs = searcher.search(query, 10); List documents = new ArrayList(); for (ScoreDoc scoreDoc : topDocs.scoreDocs) { documents.add(searcher.doc(scoreDoc.doc)); } return documents; }

In the search() method the second integer argument indicates how many top search results it should return.

Now let's test it:

@Test public void givenSearchQueryWhenFetchedDocumentThenCorrect() { InMemoryLuceneIndex inMemoryLuceneIndex = new InMemoryLuceneIndex(new RAMDirectory(), new StandardAnalyzer()); inMemoryLuceneIndex.indexDocument("Hello world", "Some hello world"); List documents = inMemoryLuceneIndex.searchIndex("body", "world"); assertEquals( "Hello world", documents.get(0).get("title")); }

Here, we add a simple document to the index, with two fields ‘title' and ‘body', and then try to search the same using a search query.

6. Lucene Queries

As we are now comfortable with the basics of indexing and searching, let us dig a little deeper.

In earlier sections, we've seen the basic query syntax, and how to convert that into a Query instance using the QueryParser.

Lucene provides various concrete implementations as well:

6.1. TermQuery

A Term is a basic unit for search, containing the field name together with the text to be searched for.

TermQuery is the simplest of all queries consisting of a single term:

@Test public void givenTermQueryWhenFetchedDocumentThenCorrect() { InMemoryLuceneIndex inMemoryLuceneIndex = new InMemoryLuceneIndex(new RAMDirectory(), new StandardAnalyzer()); inMemoryLuceneIndex.indexDocument("activity", "running in track"); inMemoryLuceneIndex.indexDocument("activity", "Cars are running on road"); Term term = new Term("body", "running"); Query query = new TermQuery(term); List documents = inMemoryLuceneIndex.searchIndex(query); assertEquals(2, documents.size()); }

6.2. PrefixQuery

To search a document with a “starts with” word:

@Test public void givenPrefixQueryWhenFetchedDocumentThenCorrect() { InMemoryLuceneIndex inMemoryLuceneIndex = new InMemoryLuceneIndex(new RAMDirectory(), new StandardAnalyzer()); inMemoryLuceneIndex.indexDocument("article", "Lucene introduction"); inMemoryLuceneIndex.indexDocument("article", "Introduction to Lucene"); Term term = new Term("body", "intro"); Query query = new PrefixQuery(term); List documents = inMemoryLuceneIndex.searchIndex(query); assertEquals(2, documents.size()); }

6.3. WildcardQuery

As the name suggests, we can use wildcards “*” or “?” for searching:

// ... Term term = new Term("body", "intro*"); Query query = new WildcardQuery(term); // ...

6.4. PhraseQuery

It's used to search a sequence of texts in a document:

// ... inMemoryLuceneIndex.indexDocument( "quotes", "A rose by any other name would smell as sweet."); Query query = new PhraseQuery( 1, "body", new BytesRef("smell"), new BytesRef("sweet")); List documents = inMemoryLuceneIndex.searchIndex(query); // ...

Notice that the first argument of the PhraseQuery constructor is called slop, which is the distance in the number of words, between the terms to be matched.

6.5. FuzzyQuery

We can use this when searching for something similar, but not necessarily identical:

// ... inMemoryLuceneIndex.indexDocument("article", "Halloween Festival"); inMemoryLuceneIndex.indexDocument("decoration", "Decorations for Halloween"); Term term = new Term("body", "hallowen"); Query query = new FuzzyQuery(term); List documents = inMemoryLuceneIndex.searchIndex(query); // ...

We tried searching for the text “Halloween”, but with miss-spelled “hallowen”.

6.6. BooleanQuery

Sometimes we might need to execute complex searches, combining two or more different types of queries:

// ... inMemoryLuceneIndex.indexDocument("Destination", "Las Vegas singapore car"); inMemoryLuceneIndex.indexDocument("Commutes in singapore", "Bus Car Bikes"); Term term1 = new Term("body", "singapore"); Term term2 = new Term("body", "car"); TermQuery query1 = new TermQuery(term1); TermQuery query2 = new TermQuery(term2); BooleanQuery booleanQuery = new BooleanQuery.Builder() .add(query1, BooleanClause.Occur.MUST) .add(query2, BooleanClause.Occur.MUST) .build(); // ...

7. Sorting Search Results

We may also sort the search results documents based on certain fields:

@Test public void givenSortFieldWhenSortedThenCorrect() { InMemoryLuceneIndex inMemoryLuceneIndex = new InMemoryLuceneIndex(new RAMDirectory(), new StandardAnalyzer()); inMemoryLuceneIndex.indexDocument("Ganges", "River in India"); inMemoryLuceneIndex.indexDocument("Mekong", "This river flows in south Asia"); inMemoryLuceneIndex.indexDocument("Amazon", "Rain forest river"); inMemoryLuceneIndex.indexDocument("Rhine", "Belongs to Europe"); inMemoryLuceneIndex.indexDocument("Nile", "Longest River"); Term term = new Term("body", "river"); Query query = new WildcardQuery(term); SortField sortField = new SortField("title", SortField.Type.STRING_VAL, false); Sort sortByTitle = new Sort(sortField); List documents = inMemoryLuceneIndex.searchIndex(query, sortByTitle); assertEquals(4, documents.size()); assertEquals("Amazon", documents.get(0).getField("title").stringValue()); }

Nous avons essayé de trier les documents récupérés par champs de titre, qui sont les noms des rivières. L'argument booléen du constructeur SortField sert à inverser l'ordre de tri.

8. Supprimer des documents de l'index

Essayons de supprimer certains documents de l'index en fonction d'un terme donné :

// ... IndexWriterConfig indexWriterConfig = new IndexWriterConfig(analyzer); IndexWriter writer = new IndexWriter(memoryIndex, indexWriterConfig); writer.deleteDocuments(term); // ...

Nous allons tester ceci:

@Test public void whenDocumentDeletedThenCorrect() { InMemoryLuceneIndex inMemoryLuceneIndex = new InMemoryLuceneIndex(new RAMDirectory(), new StandardAnalyzer()); inMemoryLuceneIndex.indexDocument("Ganges", "River in India"); inMemoryLuceneIndex.indexDocument("Mekong", "This river flows in south Asia"); Term term = new Term("title", "ganges"); inMemoryLuceneIndex.deleteDocument(term); Query query = new TermQuery(term); List documents = inMemoryLuceneIndex.searchIndex(query); assertEquals(0, documents.size()); }

9. Conclusion

Cet article était une introduction rapide à la prise en main d'Apache Lucene. De plus, nous avons exécuté diverses requêtes et trié les documents récupérés.

Comme toujours, le code des exemples peut être trouvé sur Github.