Introduction à Apache Lucene

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 allons essayer de comprendre les concepts de base de la bibliothèque et de créer une application simple.

2. Maven Setup

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éehere.

De plus, pour analyser nos requêtes de recherche, nous aurons besoin des éléments suivants:


    org.apache.lucene
    lucene-queryparser
    7.1.0

Recherchez la dernière versionhere.

3. Concepts de base

3.1. Indexage

En termes simples, Lucene utilise une «indexation inversée» des données -instead of mapping pages to keywords, it maps keywords to pages comme un glossaire à la fin de n'importe quel livre.

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

3.2. Les documents

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

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

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ée:

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

Notez qu'icititle etbody 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 consistant à extraire des mots-clés, à supprimer des mots courants et des signes de ponctuation, à changer des mots en minuscules, etc.

À cette fin, il existe plusieurs analyseurs intégrés:

  1. StandardAnalyzer - analyses basées sur la grammaire de base, supprime les mots vides comme «a», «an», etc. Convertit également en minuscule

  2. SimpleAnalyzer - coupe le texte en fonction du caractère sans lettre et convertit en minuscules

  3. WhiteSpaceAnalyzer - coupe le texte en fonction des espaces blancs

Nous pouvons également utiliser et personnaliser d'autres analyseurs.

3.5. Recherche

Une fois qu'un index est construit, nous pouvons rechercher cet index en utilisant unQuery et unIndexSearcher.. Le résultat de la recherche est généralement un ensemble de résultats, contenant les données récupérées.

Notez qu'unIndexWritter est responsable de la création de l'index et unIndexSearcher 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 utilisons simplement un texteString comme requête.

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

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 les mots commençant par “d” et finissant par “k”, avec plusieurs caractères entre les deux.

uni*

trouvera les 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 de requêtehere.

4. Une application simple

Créons une application simple et indexons certains 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();

Ici, nous créons un document avecTextField et les ajoutons à l'index en utilisant leIndexWriter. Le troisième argument dans le constructeurTextField indique si la valeur du champ doit également être stockée ou non .

Les analyseurs permettent de scinder les données ou le texte en morceaux, puis d’en filtrer les mots vides. Les mots vides sont des mots tels que "a", "am", "est", etc. Ceux-ci dépendent complètement de la langue donnée.

Ensuite, créons une requête de recherche et recherchons dans l'index le document ajouté:

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;
}

Dans la méthodesearch(), le deuxième argument entier indique le nombre de premiers résultats de recherche qu'il doit renvoyer.

Maintenant, testons-le:

@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"));
}

Nous ajoutons ici un simple document à l’index, avec les deux champs ‘title’ et ‘body’, puis nous essayons de faire la même recherche en utilisant une requête de recherche.

6. Requêtes Lucene

Maintenant que nous sommes à l'aise avec les bases de l'indexation et de la recherche, approfondissons un peu.

Dans les sections précédentes, nous avons vu la syntaxe de requête de base et comment la convertir en une instanceQuery à l'aide desQueryParser.

Lucene fournit également diverses implémentations concrètes:

6.1. TermQuery

UnTerm est une unité de base pour la recherche, contenant le nom du champ avec le texte à rechercher.

TermQuery est la plus simple de toutes les requêtes consistant en un seul terme:

@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

Pour rechercher un document avec un mot «commence par»:

@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

Comme son nom l'indique, nous pouvons utiliser des caractères génériques «*» ou «?» Pour la recherche:

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

6.4. PhraseQuery

Il est utilisé pour rechercher une séquence de textes dans un 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);
// ...

Notez que le premier argument du constructeurPhraseQuery s'appelleslop, qui est la distance en nombre de mots, entre les termes à mettre en correspondance.

6.5. FuzzyQuery

Nous pouvons l'utiliser lorsque vous recherchez quelque chose de similaire, mais pas nécessairement identique:

// ...
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);
// ...

Nous avons essayé de rechercher le texte «Halloween», mais avec «mal Halloween» mal orthographié.

6.6. BooleanQuery

Parfois, nous pouvons avoir besoin d'exécuter des recherches complexes, en combinant deux types de requêtes ou plus:

// ...
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. Tri des résultats de recherche

Nous pouvons également trier les résultats de la recherche en fonction de certains champs:

@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 en fonction des champs de titre, qui correspondent aux noms des rivières. L'argument booléen du constructeurSortField sert à inverser l'ordre de tri.

8. Supprimer des documents de l'index

Essayons de supprimer certains documents de l'index en fonction d'unTerm: 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 mise en route d’Apache Lucene. Nous avons également exécuté diverses requêtes et trié les documents récupérés.

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