Uma implementação simples de marcação com o Elasticsearch

Uma implementação simples de marcação com o Elasticsearch

1. Visão geral

A marcação é um padrão de design comum que nos permite categorizar e filtrar itens em nosso modelo de dados.

Neste artigo, implementaremos a marcação usando Spring e Elasticsearch. Estaremos usando Spring Data e a API Elasticsearch.

Em primeiro lugar, não vamos cobrir o básico de como obter Elasticsearch e Spring Data - você pode explorar esseshere.

2. Adicionando Tags

The simplest implementation of tagging is an array of strings. Podemos implementar isso adicionando um novo campo ao nosso modelo de dados como este:

@Document(indexName = "blog", type = "article")
public class Article {

    // ...

    @Field(type = Keyword)
    private String[] tags;

    // ...
}

Observe o uso do tipo de campoKeyword . Queremos apenas correspondências exatas de nossas tags para filtrar um resultado. Isso nos permite usar tags semelhantes, mas separadas, comoelasticsearchIsAwesomeeelasticsearchIsTerrible.

Os campos analisados ​​retornariam ocorrências parciais, o que é um comportamento errado neste caso.

3. Criação de consultas

As tags nos permitem manipular nossas consultas de maneiras interessantes. Podemos pesquisá-los como qualquer outro campo ou podemos usá-los para filtrar nossos resultados nas consultas dematch_all. Também podemos usá-los com outras consultas para aumentar nossos resultados.

3.1. Pesquisando Tags

O novo campotag que criamos em nosso modelo é como qualquer outro campo em nosso índice. Podemos procurar qualquer entidade que tenha uma tag específica como esta:

@Query("{\"bool\": {\"must\": [{\"match\": {\"tags\": \"?0\"}}]}}")
Page
findByTagUsingDeclaredQuery(String tag, Pageable pageable);

Este exemplo usa um Spring Data Repository para construir nossa consulta, mas também podemos usar umRest Template para consultar o cluster Elasticsearch manualmente.

Da mesma forma, podemos usar a API Elasticsearch:

boolQuery().must(termQuery("tags", "elasticsearch"));

Suponha que usemos os seguintes documentos em nosso índice:

[
    {
        "id": 1,
        "title": "Spring Data Elasticsearch",
        "authors": [ { "name": "John Doe" }, { "name": "John Smith" } ],
        "tags": [ "elasticsearch", "spring data" ]
    },
    {
        "id": 2,
        "title": "Search engines",
        "authors": [ { "name": "John Doe" } ],
        "tags": [ "search engines", "tutorial" ]
    },
    {
        "id": 3,
        "title": "Second Article About Elasticsearch",
        "authors": [ { "name": "John Smith" } ],
        "tags": [ "elasticsearch", "spring data" ]
    },
    {
        "id": 4,
        "title": "Elasticsearch Tutorial",
        "authors": [ { "name": "John Doe" } ],
        "tags": [ "elasticsearch" ]
    },
]

Agora podemos usar esta consulta:

Page
articleByTags = articleService.findByTagUsingDeclaredQuery("elasticsearch", PageRequest.of(0, 10)); // articleByTags will contain 3 articles [ 1, 3, 4] assertThat(articleByTags, containsInAnyOrder( hasProperty("id", is(1)), hasProperty("id", is(3)), hasProperty("id", is(4))) );

3.2. Filtrando todos os documentos

Um padrão de design comum é criar umFiltered List View na IU que mostra todas as entidades, mas também permite que o usuário filtre com base em critérios diferentes.

Digamos que desejamos retornar todos os artigos filtrados por qualquer tag que o usuário selecionar:

@Query("{\"bool\": {\"must\": " +
  "{\"match_all\": {}}, \"filter\": {\"term\": {\"tags\": \"?0\" }}}}")
Page
findByFilteredTagQuery(String tag, Pageable pageable);

Mais uma vez, estamos usando Spring Data para construir nossa consulta declarada.

Consequentemente, a consulta que estamos usando é dividida em duas partes. A consulta de pontuação é o primeiro termo, neste caso,match_all. A consulta de filtro é a próxima e informa ao Elasticsearch quais resultados serão descartados.

Aqui está como usamos esta consulta:

Page
articleByTags = articleService.findByFilteredTagQuery("elasticsearch", PageRequest.of(0, 10)); // articleByTags will contain 3 articles [ 1, 3, 4] assertThat(articleByTags, containsInAnyOrder( hasProperty("id", is(1)), hasProperty("id", is(3)), hasProperty("id", is(4))) );

É importante perceber que, embora isso retorne os mesmos resultados do exemplo acima, essa consulta terá um desempenho melhor.

3.3. Filtrando consultas

Às vezes, uma pesquisa retorna muitos resultados para ser utilizável. Nesse caso, é bom expor um mecanismo de filtragem que pode executar novamente a mesma pesquisa, apenas com os resultados restritos.

Aqui está um exemplo em que restringimos os artigos que um autor escreveu, apenas aqueles com uma tag específica:

@Query("{\"bool\": {\"must\": " +
  "{\"match\": {\"authors.name\": \"?0\"}}, " +
  "\"filter\": {\"term\": {\"tags\": \"?1\" }}}}")
Page
findByAuthorsNameAndFilteredTagQuery( String name, String tag, Pageable pageable);

Mais uma vez, a Spring Data está fazendo todo o trabalho para nós.

Vejamos também como construir essa consulta nós mesmos:

QueryBuilder builder = boolQuery().must(
  nestedQuery("authors", boolQuery().must(termQuery("authors.name", "doe")), ScoreMode.None))
  .filter(termQuery("tags", "elasticsearch"));

É claro que podemos usar essa mesma técnica para filtrar qualquer outro campo do documento. Mas as tags se prestam particularmente bem a esse caso de uso.

Aqui está como usar a consulta acima:

SearchQuery searchQuery = new NativeSearchQueryBuilder().withQuery(builder)
  .build();
List
articles = elasticsearchTemplate.queryForList(searchQuery, Article.class); // articles contains [ 1, 4 ] assertThat(articleByTags, containsInAnyOrder( hasProperty("id", is(1)), hasProperty("id", is(4))) );

4. Contexto do filtro

Quando criamos uma consulta, precisamos diferenciar entre o Contexto da Consulta e o Contexto do Filtro. Toda consulta no Elasticsearch possui um Contexto de Consulta, portanto devemos estar acostumados a vê-las.

Nem todo tipo de consulta suporta o contexto de filtro. Portanto, se queremos filtrar as tags, precisamos saber quais tipos de consulta podemos usar.

The bool query has two ways to access the Filter Context. O primeiro parâmetro,filter, é o que usamos acima. Também podemos usar um parâmetromust_not para ativar o contexto.

The next query type we can filter is constant_score. Isso é útil quando você deseja substituir o Contexto da Consulta pelos resultados do Filtro e atribuir a cada resultado a mesma pontuação.

The final query type that we can filter based on tags is the filter aggregation. Isso nos permite criar grupos de agregação com base nos resultados do nosso filtro. Em outras palavras, podemos agrupar todos os artigos por tag em nosso resultado de agregação.

5. Etiquetagem Avançada

Até agora, falamos apenas de tags usando a implementação mais básica. A próxima etapa lógica é criar tags que sejamkey-value pairs. Isso nos permitiria ficar ainda mais sofisticados com nossas consultas e filtros.

Por exemplo, podemos mudar nosso campo de tag para isso:

@Field(type = Nested)
private List tags;

Em seguida, apenas mudaríamos nossos filtros para usar os tiposnestedQuery.

Uma vez que entendemos como usarkey-value pairs, é um pequeno passo para usar objetos complexos como nossa tag. Poucas implementações precisarão de um objeto completo como tag, mas é bom saber que temos essa opção caso a exijamos.

6. Conclusão

Neste artigo, cobrimos os fundamentos da implementação de marcação usando Elasticsearch.

Como sempre, os exemplos podem ser encontradosover on GitHub.