Introdução ao Apache Lucene

Introdução ao Apache Lucene

1. Visão geral

Apache Lucene é um mecanismo de busca de texto completo que pode ser usado em várias linguagens de programação.

Neste artigo, tentaremos entender os conceitos básicos da biblioteca e criar um aplicativo simples.

2. Configuração do Maven

Para começar, vamos adicionar as dependências necessárias primeiro:


    org.apache.lucene
    lucene-core
    7.1.0

A versão mais recente pode ser encontradahere.

Além disso, para analisar nossas consultas de pesquisa, precisaremos:


    org.apache.lucene
    lucene-queryparser
    7.1.0

Verifique a versão mais recentehere.

3. Conceitos principais

3.1. Indexação

Simplificando, Lucene usa uma “indexação invertida” de dados -instead of mapping pages to keywords, it maps keywords to pages, assim como um glossário no final de qualquer livro.

Isso permite respostas de pesquisa mais rápidas, à medida que pesquisa em um índice, em vez de pesquisar diretamente em texto.

3.2. Documentos

Aqui, um documento é uma coleção de campos e cada campo tem um valor associado a ele.

Os índices normalmente são compostos de um ou mais documentos, e os resultados da pesquisa são conjuntos de documentos que melhor correspondem.

Nem sempre é um documento de texto simples, também pode ser uma tabela de banco de dados ou uma coleção.

3.3. Campos

Os documentos podem ter dados de campo, em que um campo geralmente é uma chave que contém um valor de dados:

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

Observe que aquititle ebody são campos e podem ser pesquisados ​​juntos ou individualmente.

3.4. Análise

Uma análise está convertendo o texto fornecido em unidades menores e precisas para facilitar a busca.

O texto passa por várias operações de extração de palavras-chave, remoção de palavras e pontuação comuns, alteração de palavras para minúsculas etc.

Para esse fim, existem vários analisadores internos:

  1. StandardAnalyzer - analisa com base na gramática básica, remove palavras irrelevantes como “a”, “uma” etc. Também converte em minúsculas

  2. SimpleAnalyzer - quebra o texto com base em caracteres sem letras e converte em minúsculas

  3. WhiteSpaceAnalyzer - quebra o texto com base em espaços em branco

Há mais analisadores disponíveis para usarmos e personalizarmos também.

3.5. Procurando

Depois que um índice é construído, podemos pesquisar esse índice usandoQueryeIndexSearcher.. O resultado da pesquisa é normalmente um conjunto de resultados contendo os dados recuperados.

Observe que umIndexWritter é responsável pela criação do índice e umIndexSearcher pela busca do índice.

3.6. Sintaxe de consulta

O Lucene fornece uma sintaxe de consulta muito dinâmica e fácil de escrever.

Para pesquisar um texto livre, usaríamos apenas um textoString como consulta.

Para pesquisar um texto em um campo específico, usaríamos:

fieldName:text

eg: title:tea

Pesquisas por intervalo:

timestamp:[1509909322,1572981321]

Também podemos pesquisar usando caracteres curinga:

dri?nk

procuraria um único caractere no lugar do curinga "?"

d*k

procura por palavras começando com "d" e terminando em "k", com vários caracteres no meio.

uni*

encontrará palavras que começam com "uni".

Também podemos combinar essas consultas e criar consultas mais complexas. E inclua um operador lógico como AND, NOT ou OR:

title: "Tea in breakfast" AND "coffee"

Mais sobre a sintaxe de consultahere.

4. Uma aplicação simples

Vamos criar um aplicativo simples e indexar alguns documentos.

Primeiro, vamos criar um índice na memória e adicionar alguns documentos a ele:

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

Aqui, criamos um documento comTextFielde adicionamos ao índice usandoIndexWriter. O terceiro argumento no construtorTextField indica se o valor do campo também deve ser armazenado ou não .

Os analisadores são usados ​​para dividir os dados ou o texto em partes e depois filtrar as palavras de parada. Palavras de parada são palavras como ‘a ',‘ am', ‘é 'etc. Isso depende completamente do idioma especificado.

A seguir, vamos criar uma consulta de pesquisa e pesquisar o índice do documento adicionado:

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

No métodosearch(), o segundo argumento inteiro indica quantos resultados de pesquisa principais ele deve retornar.

Agora vamos testar:

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

Aqui, adicionamos um documento simples ao índice, com dois campos "title" e "body" e, em seguida, tentamos pesquisar o mesmo usando uma consulta de pesquisa.

6. Lucene Queries

Como agora estamos confortáveis ​​com o básico de indexação e pesquisa, vamos nos aprofundar um pouco mais.

Nas seções anteriores, vimos a sintaxe de consulta básica e como convertê-la em uma instânciaQuery usando oQueryParser.

O Lucene também fornece várias implementações concretas:

6.1. TermQuery

UmTerm é uma unidade básica para pesquisa, contendo o nome do campo junto com o texto a ser pesquisado.

TermQuery é a mais simples de todas as consultas que consistem em um único termo:

@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

Para pesquisar um documento com uma palavra "começa com":

@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

Como o nome sugere, podemos usar caracteres curinga "*" ou "?" para pesquisar:

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

6.4. PhraseQuery

É usado para pesquisar uma sequência de textos em um documento:

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

Observe que o primeiro argumento do construtorPhraseQuery é chamadoslop,, que é a distância, em número de palavras, entre os termos a serem correspondidos.

6.5. FuzzyQuery

Podemos usar isso ao procurar algo semelhante, mas não necessariamente idêntico:

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

Tentamos procurar o texto "Halloween", mas com "hallowen" com a grafia incorreta.

6.6. BooleanQuery

Às vezes, podemos precisar executar pesquisas complexas, combinando dois ou mais tipos diferentes de consultas:

// ...
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. Classificação dos resultados da pesquisa

Também podemos classificar os documentos dos resultados da pesquisa com base em determinados campos:

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

Tentamos classificar os documentos buscados por campos de título, que são os nomes dos rios. O argumento booleano para o construtorSortField serve para inverter a ordem de classificação.

8. Remover documentos do índice

Vamos tentar remover alguns documentos do índice com base em um determinadoTerm:

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

Vamos testar isso:

@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. Conclusão

Este artigo foi uma introdução rápida aos primeiros passos do Apache Lucene. Além disso, executamos várias consultas e classificamos os documentos recuperados.

Como sempre, o código dos exemplos pode ser encontradoover on Github.