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:
-
StandardAnalyzer - analisa com base na gramática básica, remove palavras irrelevantes como “a”, “uma” etc. Também converte em minúsculas
-
SimpleAnalyzer - quebra o texto com base em caracteres sem letras e converte em minúsculas
-
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.