API de pesquisa de texto completo facetada Java usando o Atlas Search
Avalie esse Tutorial
Este será um tutorial prático e prático que demonstra como construir uma API de pesquisa de texto completo facetada em Java (como as que alimentam sites como a Amazon)!
Usaremos um conjunto de dados interessante que mostra como você pode emparelhar efetivamente dados gerados por aprendizado de máquina/AI com pesquisa mais tradicional para produzir mecanismos de pesquisa rápidos, baixos, repetíveis e intuitivos.
TL;DR
Se você (como eu) tem menos palavras e mais código, pode pular imediatamente. Confira e execute localmente assim:
1 git clone https://github.com/luketn/atlas-search-coco 2 cd atlas-search-coco 3 docker compose up java-app
Se você encontrar um problema ao executar o container e estiver no Mac OS Sequoia,15.2 poderá adicionar a variável de ambiente como uma solução alternativa.
Tudo bem, vamos analisar a criação dessa solução passo a passo. No final, você terá todas as ferramentas necessárias para criar suas próprias APIs de pesquisa facetadas incríveis.
Usaremos os poderosos recursos do Atlas Search combinados com a forte modelagem de dados e o alto desempenho do Java para construí-lo.
Você precisará do seguinte em sua máquina:
- Kit de desenvolvedor Java (JDK) 21+ e um IDE Java
- O IntelliJ também pode baixar e configurar o JDK para você (por exemplo, Amazon Correto 21).
- Se preferir outro IDE, você poderá traduzir as instruções com bastante facilidade.
- Área de trabalho do Docker
Vamos criar um novo projeto Java !
Abra seu IDE Java e crie um novo projeto baseado em Maven para Java 21.
No IntelliJ, selecione Arquivo -> Novo Projeto...
Dê um clique e você verá algumas imagens incríveis e (importante para nós!), Boas legendas e rótulos para as categorias de objetos em cada imagem.
Os dados aqui foram gerados aplicando modelos de aprendizado de máquina a imagens para segmentar, capturar e rotular os recursos que elas incluem. Nossa pesquisa, no entanto, será lexical, rápida e independente de qualquer modelo de aprendizado de máquina ou IA.
Ao trabalhar com dados no MongoDB, uma das etapas mais importantes é a modelagem de dados. Considere a estrutura e o design das collections, e o esquema de documento de cada collection.
No conjunto de dados bruto de COCO, existem as seguintes entidades, cada uma separada e vinculada por um ID inteiro:
Imagem: a altura, a largura, o URL da imagem , o ID da licença e a data de captura
Licença: Indica a licença sob a qual a imagem está e links para o documento de licença (1-n)
Anotação (Legenda): um ID de imagem e uma string de legenda descrevendo a imagem (n-1)
Anotação (objeto: : uma caixa delimitadora, ID da imagem e ID da categoria (n-1)
Categoria: um nome de superCategoria e categoria, por exemplo, veículo e carro
Quando você está modelando dados no MongoDB, a principal consideração é como seu aplicação será consultado. Em nosso caso, vamos expor um endpoint de pesquisa da API REST como:
/search?vehcycle=car&text=red
Nos resultados, vamos querer todos os detalhes das imagens correspondentes aos parâmetros de facetas (categorias) e texto completo (caption).
Para dar suporte a esse padrão de query pretendido, vamos recolher todas essas entidades em um único esquema de documento hierárquico, "Image," que torna a pesquisa eficiente e simples.
Aqui está nosso esquema, definido como um registro Java :
1 import java.util.Date; 2 import java.util.List; 3 4 public record Image( 5 int _id, 6 //The caption describes the contents of the image and can be searched on using full text search. 7 String caption, 8 String url, 9 int height, 10 int width, 11 Date dateCaptured, 12 String licenseName, 13 String licenseUrl, 14 //True if the image shows a person. 15 boolean hasPerson, 16 //Following fields are 'super categories'. 17 // Each item in the list is a category. 18 // One or more objects of each category listed is present in the image. 19 List<String> accessory, 20 List<String> animal, 21 List<String> appliance, 22 List<String> electronic, 23 List<String> food, 24 List<String> furniture, 25 List<String> indoor, 26 List<String> kitchen, 27 List<String> outdoor, 28 List<String> sports, 29 List<String> vehicle 30 ) { }
Vamos ter duas collections em nosso MongoDB:
- Imagem: Terá documentos no esquema acima
- Categoria: terá uma lista das categorias (e suas supercategorias) que podem ser selecionadas para filtragem
1 public record Category( 2 int _id, 3 String superCategory, 4 String name 5 ) { }
Crie os dois registros acima em seu projeto Java (fique à vontade para usar o namespace de sua preferência):
Você pode escolher se deseja executar o MongoDB Atlas localmente em um container Docker:
1 docker run -d --name mongodb-atlas -p 27017:27017 mongodb/mongodb-atlas-local:8.0.3
Se você encontrar um problema ao executar o container e estiver no Mac OS Sequoia,15.2 poderá adicionar a variável de ambiente como uma solução alternativa.
string de conexão local:
1 mongodb://localhost:27017/?directConnection=true
(Anote a string de conexão com o cluster para a próxima etapa.)
Só para simplificar um pouco as coisas, baixei o conjunto de dados COCO e o transformei no modelo de dados.
Você terá dois arquivos de dados: um para cada uma das nossas collections:
- AtlasSearchCoco.Category.json
- AtlasSearchCoco.Image.json
Em seguida, usaremos a UI do MongoDB Compass para consumir os dados JSON e criar nosso banco de dados.
Abra o aplicativo Compass, conecte-se ao MongoDB usando a string de conexão que você anotou na etapa anterior e crie um novo banco de dados usando o + ícone na conexão:
Insira atlasSearchCoco como o nome do banco de dados e imagem como o nome da coleção inicial: 
Clique em Criar Banco de Dados para começar.
Clique em Importar Dados para trazer nossos dados JSON:
Selecione o arquivo AtlasSearchCoco.Image.json que você baixou e clique em Importar.
Você deve ver uma observação de que importou 118,287 documentos.
Em seguida, crie a coleção de categorias clicando no ícone + no banco de dados atlasSearchCoco :
Clique em Criar collection.
Como acima, clique em Importar Dados mas, desta vez, selecione o arquivo AtlasSearchCoco.Category.json:
Clique em Importar para concluir a importação das categorias e deverá ver uma observação de que importou 80 documentos.
Parabéns! Agora você tem um cluster MongoDB Atlas , com o conjunto de dados da imagem COCO carregado nele.
Em seguida, criaremos nosso índice Atlas Search .
Esse é o cerne de tudo que vamos fazer em Java a seguir, portanto, dedicaremos um pouco do tempo para percorrer cada parte do índice e explicar o que ele significa.
Para criar o índice, vá para a aba Índices da coleção de imagens na interface do usuário do MongoDB Compass .
Clique em Índices de pesquisa para selecionar a aba Índice do Atlas Search :
Em seguida, clique em Criar Índice do Atlas Search:
Deixe o nome como padrão e cole a definição de índice:
1 { 2 "mappings": { 3 "fields": { 4 "caption": [{"type": "string"}], 5 "hasPerson": {"type": "boolean"}, 6 "accessory": [{"type": "token"}, {"type": "stringFacet"}], 7 "animal": [{"type": "token"}, {"type": "stringFacet"}], 8 "appliance": [{"type": "token"}, {"type": "stringFacet"}], 9 "electronic": [{"type": "token"}, {"type": "stringFacet"}], 10 "food": [{"type": "token"}, {"type": "stringFacet"}], 11 "furniture": [{"type": "token"}, {"type": "stringFacet"}], 12 "indoor": [{"type": "token"}, {"type": "stringFacet"}], 13 "kitchen": [{"type": "token"}, {"type": "stringFacet"}], 14 "outdoor": [{"type": "token"}, {"type": "stringFacet"}], 15 "sports": [{"type": "token"}, {"type": "stringFacet"}], 16 "vehicle": [{"type": "token"}, {"type": "stringFacet"}] 17 } 18 } 19 }
Clique em "criar índice de pesquisa".
Após um momento, você deverá ver a mudança de status para READY, indicando que o índice foi criado e pronto para pesquisa:
Aqui estão os campos de índice e o que cada uma de suas definições de tipo faz:
legenda
1 "caption": [{"type": "string"}],
A definição de tipo deste campo é enganosamente simples: string.
Os campos de string podem ser pesquisados de formas complexas usando o mecanismo de pesquisa Lucene subjacente, e esse campo será o principal pesquisado pelo nosso serviço de pesquisa Java .
Quando você cria um campo do tipo string, o Lucene itera todos os documentos, tokenizando a string do campo de legenda em pequenos ramos de palavras, fazendo uma pequena lista de tokens exclusivos. Este é o molho secreto para o que torna a pesquisa de texto com Lucene tão rápida.
Cada documento recebe um ID de documento inteiro no índice Lucene, e eles podem ser armazenados com eficiência porque os intervalos podem ser compactados ou ignorados durante a pesquisa.
hasPerson
1 "hasPerson": {"type": "boolean"},
Este campo é um tipo muito simples de índice, essencialmente dividindo os documentos em três grupos: booleanos verdadeiros, falsos e indefinidos.
campos de categoria - acessório, animal, dispositivo, eletrônico, comida, móveis, interno, cozinha, ao ar livre, Eventos, veículo
1 "accessory": [{"type": "token"}, {"type": "stringFacet"}], 2 "animal": [{"type": "token"}, {"type": "stringFacet"}], 3 "appliance": [{"type": "token"}, {"type": "stringFacet"}], 4 "electronic": [{"type": "token"}, {"type": "stringFacet"}], 5 "food": [{"type": "token"}, {"type": "stringFacet"}], 6 "furniture": [{"type": "token"}, {"type": "stringFacet"}], 7 "indoor": [{"type": "token"}, {"type": "stringFacet"}], 8 "kitchen": [{"type": "token"}, {"type": "stringFacet"}], 9 "outdoor": [{"type": "token"}, {"type": "stringFacet"}], 10 "sports": [{"type": "token"}, {"type": "stringFacet"}], 11 "vehicle": [{"type": "token"}, {"type": "stringFacet"}]
Todos esses campos têm valores de array de strings na collection. Por exemplo, na array animal:
1 "animal": ["dog"]
Eles são indexados com o Atlas Search de duas maneiras diferentes:
token: os índices do tipo de token estão em valores que podem ser usados para filtragem de correspondência exata, mas não podem ser usados para pesquisas avançadas de texto. Isso é ideal para o nosso caso de uso porque permitiremos que a API inclua determinadas categorias como filtros.
stringFacet: os índices do tipo faceta de string são usados para contar possíveis correspondências exatas para um determinado valor de campo . Usaremos isso para mostrar o número de documentos que corresponderiam a cada categoria se selecionadas.
Ao combinar esses campos em um único índice, podemos (de uma só vez) realizar uma pesquisa avançada de texto na legenda, filtrar por qualquer categoria(s) que quisermos e coletar as contagens de faceta das categorias.
Por exemplo, esta pesquisa abreviada encontrará imagens com "frisbee" na legenda e um cachorro na imagem.
Usaremos um filtro composto para combinar várias cláusulas de filtro em nossa consulta de exemplo . Também contaremos o número de possíveis correspondências para as categorias animal e esportiva usando o coletor de faceta do Atlas Search . A query é bem complexa, mas não se deve preocupar muito com ela por enquanto — analisaremos cada parte dessa query em mais detalhes à medida que a implementamos em Java. Por enquanto, experimente no Compass e observe que você pode ver os resultados da consulta e os facets retornados juntos. Você pode executar o exemplo no Compass clicando na guia Agregações na coleção de imagens e colando o seguinte JSON na visualização de texto:
1 [ 2 { 3 $search: { 4 facet: { 5 operator: { 6 compound: { 7 filter: [ 8 { 9 text: { 10 path: "caption", 11 query: "frisbee" 12 } 13 }, 14 { 15 equals: { 16 path: "animal", 17 value: "dog" 18 } 19 } 20 ] 21 } 22 }, 23 facets: { 24 animal: { 25 type: "string", 26 path: "animal", 27 numBuckets: 10 28 }, 29 sports: { 30 type: "string", 31 path: "sports", 32 numBuckets: 10 33 } 34 } 35 }, 36 count: { 37 type: "total" 38 } 39 } 40 }, 41 { 42 $facet: { 43 docs: [], 44 meta: [ 45 { 46 $replaceWith: "$$SEARCH_META" 47 }, 48 { 49 $limit: 1 50 } 51 ] 52 } 53 } 54 ]
Resultado do exemplo:
1 { 2 "docs": [ 3 { 4 "_id": 394, 5 "caption": "A dog is holding a frisbee standing on grass.", 6 "url": "http://images.cocodataset.org/train2017/000000000394.jpg", 7 "height": 611, 8 "width": 640, 9 "dateCaptured": { 10 "$date": "2013-11-18T09:44:51.000Z" 11 }, 12 "licenseName": "Attribution-NonCommercial-NoDerivs License", 13 "licenseUrl": "http://creativecommons.org/licenses/by-nc-nd/2.0/", 14 "hasPerson": false, 15 "animal": [ 16 "dog" 17 ], 18 "sports": [ 19 "frisbee" 20 ] 21 }, 22 ... 23 ], 24 "meta": [ 25 { 26 "count": { 27 "total": { 28 "$numberLong": "366" 29 } 30 }, 31 "facet": { 32 "sports": { 33 "buckets": [ 34 { 35 "_id": "frisbee", 36 "count": { 37 "$numberLong": "364" 38 } 39 }, 40 { 41 "_id": "sports ball", 42 "count": { 43 "$numberLong": "4" 44 } 45 }, 46 { 47 "_id": "baseball glove", 48 "count": { 49 "$numberLong": "1" 50 } 51 } 52 ] 53 }, 54 "animal": { 55 "buckets": [ 56 { 57 "_id": "dog", 58 "count": { 59 "$numberLong": "366" 60 } 61 } 62 ] 63 } 64 } 65 } 66 ] 67 }
Tudo bem! Vamos implementar nossa classe de serviço Java .
Não vamos medir palavras. A sintaxe do Atlas Search é complexa, especialmente se você entrar em várias cláusulas e condições compostas. A facetagem adiciona uma camada extra de complexidade, e obter os resultados da pesquisa junto com as facets em uma única query complica ainda mais isso.
Pense que o driver do MongoDB Java , e o Java em geral, ajudam a mitigar a complexidade de algumas maneiras:
- Tipos fortes: depois de estabelecer seu modelo de dados, você poderá defini-lo facilmente como tipos de registro Java imutáveis, e o driver tratará da serialização/deserialização. Depois de implementar isso, você poderá ter certeza de que seu código de lógica de negócios está correto e seguro.
- sintaxe do construtor fluente : a maioria das operações que você precisa usar no Atlas Search tem métodos assistente para ajudá-lo a compor as operações de pesquisa e criar sua query.
- Grandes testes: Depois de construir soluções semelhantes com NodeJS e Python, considero as soluções de teste e o vigor em torno da cobertura de código e da integridade dos testes mais fáceis de obter e com melhor suporte no Java. Ter um conjunto abrangente de testes em torno de seu código de pesquisa é superimportante para deixar a intenção do código clara, ensaiar os caminhos Dourados e os casos extremos e evitar a regressão.
Começando com o básico, vamos criar uma classe EntradaPoint , inicializar uma conexão com o MongoDB e retornar alguns dados:
Primeiro, adicione as dependências para MongoDB, um serializador JSON (Jackson) e uma estrutura de registro simples ao seu Maven pom.xml:
1 <dependencies> 2 <dependency> 3 <groupId>org.mongodb</groupId> 4 <artifactId>mongodb-driver-sync</artifactId> 5 <version>5.2.0</version> 6 </dependency> 7 <dependency> 8 <groupId>com.fasterxml.jackson.core</groupId> 9 <artifactId>jackson-databind</artifactId> 10 <version>2.17.2</version> 11 </dependency> 12 <dependency> 13 <groupId>org.slf4j</groupId> 14 <artifactId>slf4j-simple</artifactId> 15 <version>2.0.13</version> 16 </dependency> 17 </dependencies>
O serializador JSON será usado para retornar nossos dados da API, e o driver do MongoDB usará o registrador SL4J para escrever seus registros no console (ou onde quer que você o configure para escrever).
Em seguida, crie sua classe EntradaPoint :
1 package com.mycodefu; 2 3 import com.fasterxml.jackson.databind.ObjectMapper; 4 import com.mongodb.client.MongoClient; 5 import com.mongodb.client.MongoClients; 6 import com.mongodb.client.MongoCollection; 7 import com.mongodb.client.MongoDatabase; 8 import com.sun.net.httpserver.HttpServer; 9 10 import java.io.IOException; 11 import java.net.InetSocketAddress; 12 import java.net.URLDecoder; 13 import java.nio.charset.StandardCharsets; 14 import java.util.ArrayList; 15 import java.util.Arrays; 16 import java.util.List; 17 import java.util.Map; 18 import java.util.stream.Collectors; 19 20 public class Main { 21 22 public static void main(String[] args) throws IOException { 23 24 String connectionString = "mongodb://localhost:27017"; 25 MongoClient mongoClient = MongoClients.create(connectionString); 26 MongoDatabase database = mongoClient.getDatabase("atlasSearchCoco"); 27 MongoCollection<Category> categoryCollection = database.getCollection("category", Category.class); 28 MongoCollection<Image> imageCollection = database.getCollection("image", Image.class); 29 30 ObjectMapper objectMapper = new ObjectMapper(); 31 32 HttpServer httpServer = HttpServer.create(new InetSocketAddress("0.0.0.0", 8080), 0); 33 34 httpServer.createContext("/categories", exchange -> { 35 List<Category> categories = categoryCollection.find().into(new ArrayList<>()); 36 String categoriesJson = objectMapper.writeValueAsString(categories); 37 byte[] categoriesJsonBytes = categoriesJson.getBytes(); 38 39 exchange.sendResponseHeaders(200, categoriesJsonBytes.length); 40 exchange.getResponseBody().write(categoriesJsonBytes); 41 exchange.close(); 42 }); 43 44 httpServer.createContext("/images", exchange -> { 45 Map<String, List<String>> params = Arrays.stream(exchange.getRequestURI().getQuery().split("&")) 46 .map(param -> param.split("=", 2)) 47 .map(pair -> new String[]{ 48 URLDecoder.decode(pair[0], StandardCharsets.UTF_8), 49 URLDecoder.decode(pair[1], StandardCharsets.UTF_8) 50 }) 51 .collect(Collectors.groupingBy( 52 pair -> pair[0], 53 Collectors.mapping( 54 pair -> pair[1], 55 Collectors.toList() 56 ) 57 )); 58 59 //TODO: Implement the search for images based on the query parameters. For now we just return the first image. 60 List<Image> images = imageCollection.find().limit(1).into(new ArrayList<>()); 61 //---------------- 62 63 String imagesJson = objectMapper.writeValueAsString(images); 64 byte[] imagesJsonBytes = imagesJson.getBytes(); 65 66 exchange.sendResponseHeaders(200, imagesJsonBytes.length); 67 exchange.getResponseBody().write(imagesJsonBytes); 68 exchange.close(); 69 }); 70 71 httpServer.start(); 72 73 System.out.println("Server started on port http://localhost:8080"); 74 System.out.println("Try listing categories at http://localhost:8080/categories"); 75 System.out.println("Try searching for images at http://localhost:8080/images?caption=motorcycle"); 76 } 77 }
Execute o aplicação e, em seguida, você poderá carregar as URLs no navegador:
E:
Há algumas coisas a observar sobre a implementação da classe principal:
A conexão mongo, banco de dados e instâncias de coleção — a primeira coisa que fazem é estabelecer uma conexão com MongoDB e instâncias digitadas de classes de coleção que nos permitem consultar os dados:
1 String connectionString = "mongodb://localhost:27017"; 2 MongoClient mongoClient = MongoClients.create(connectionString); 3 MongoDatabase database = mongoClient.getDatabase("atlasSearchCoco"); 4 MongoCollection<Category> categoryCollection = database.getCollection("category", Category.class); 5 MongoCollection<Image> imageCollection = database.getCollection("image", Image.class);
Os tipos Categoria e Imagem são os registros que criamos anteriormente, que representam o modelo de domínio COCO para nosso projeto.
Também criamos um ObjectMapper que é o serializador JSON do JSON que usaremos para escrever JSON para o cliente.
Em seguida, usamos o servidor HTTP integrado do Java para criar um servidor de API , consultar o banco de dados e retornar JSON:
1 HttpServer httpServer = HttpServer.create(new InetSocketAddress("0.0.0.0", 8080), 0); 2 3 httpServer.createContext("/categories", exchange -> { 4 List<Category> categories = categoryCollection.find().into(new ArrayList<>()); 5 String categoriesJson = objectMapper.writeValueAsString(categories); 6 byte[] categoriesJsonBytes = categoriesJson.getBytes(); 7 8 exchange.sendResponseHeaders(200, categoriesJsonBytes.length); 9 exchange.getResponseBody().write(categoriesJsonBytes); 10 exchange.close(); 11 }); 12 … 13 httpServer.start();
É claro que poderíamos usar uma estrutura como o Springboot aqui, mas para manter o foco no Atlas Search, usaremos apenas esta pequena implementação básica de servidor HTTP para nosso projeto.
Uma das minhas coisas favoritas sobre o cliente de Java para MongoDB é que podemos aproveitar facilmente a força do sistema de tipos. Nosso modelo de domínio , conforme definido pelo registro imutável de categoria , é fortemente aplicado aqui em toda a lógica que interage com o banco de dados. Embora nosso esquema no MongoDB esteja livre para desenvolver, o lugar onde estamos controlando essa evolução é aqui em nosso código Java . Quaisquer alterações no modelo serão deliberadas e aplicadas no momento da compilação e no tempo de execução em nosso serviço. Normalmente, nosso cliente—digamos, uma interface de usuário do navegador — também seria digitado JSON frouxamente (não forçando o modelo de dados). Paramim, o serviço é o lugar certo para que esse controle seja aplicado, portanto, nosso esquema envolve nossa API.
Em seguida, temos nosso espaço reservado para a pesquisa de imagens:
1 httpServer.createContext("/images", exchange -> { 2 Map<String, List<String>> params = Arrays.stream(exchange.getRequestURI().getQuery().split("&")) 3 .map(param -> param.split("=", 2)) 4 .map(pair -> new String[]{ 5 URLDecoder.decode(pair[0], StandardCharsets.UTF_8), 6 URLDecoder.decode(pair[1], StandardCharsets.UTF_8) 7 }) 8 .collect(Collectors.groupingBy( 9 pair -> pair[0], 10 Collectors.mapping( 11 pair -> pair[1], 12 Collectors.toList() 13 ) 14 )); 15 16 //TODO: Implement the search for images based on the query parameters. For now we just return the first image. 17 List<Image> images = imageCollection.find().limit(1).into(new ArrayList<>()); 18 //---------------- 19 20 String imagesJson = objectMapper.writeValueAsString(images); 21 byte[] imagesJsonBytes = imagesJson.getBytes(); 22 23 exchange.sendResponseHeaders(200, imagesJsonBytes.length); 24 exchange.getResponseBody().write(imagesJsonBytes); 25 exchange.close(); 26 });
Aqui, estamos adicionando uma ingestão segura dos parâmetros de query em um mapa e, por enquanto, apenas retorne a primeira imagem. Observe que o mapa é de string para Lista<string>, o que é importante porque nossos parâmetros de query podem incluir 1-n instâncias do mesmo parâmetro. Usaremos isso na próxima etapa para filtrar vários valores na pesquisa.
Nossa última etapa: a busca!
Vamos adicionar mais um tipo de registro à nossa lista de modelos, o que nos permitirá retornar uma lista paginada de registros de imagem, bem como as contagens de faceta :
1 import java.util.List; 2 3 public record ImageSearchResult(List<Image> docs, List<ImageMeta> meta) { 4 public record ImageMeta (ImageMetaTotal count, ImageMetaFacets facet) { } 5 public record ImageMetaTotal (long total) { } 6 public record ImageMetaFacets ( 7 ImageMetaFacet accessory, 8 ImageMetaFacet animal, 9 ImageMetaFacet appliance, 10 ImageMetaFacet electronic, 11 ImageMetaFacet food, 12 ImageMetaFacet furniture, 13 ImageMetaFacet indoor, 14 ImageMetaFacet kitchen, 15 ImageMetaFacet outdoor, 16 ImageMetaFacet sports, 17 ImageMetaFacet vehicle 18 ) { } 19 public record ImageMetaFacet (List<ImageMetaFacetBucket> buckets) { } 20 public record ImageMetaFacetBucket (String _id, long count) { } 21 }
Salve isso como um novo registro em seu projeto junto com Imagem e Categoria.
Este registro representa o documento de resultado que será retornado por nossa query de pesquisa agregada .
O primeiro campo, Docs, é uma única página de documentos de imagem, e o segundo campo, meta, contém os metadados de todos os documentos que correspondem aos critérios de pesquisa.
Os metadados mostram a contagem total de todos os documentos correspondentes, bem como as contagens por faceta.
Em nosso caso, temos facetas para cada categoria de objeto que podem estar na imagem. Como exemplo, digamos que pesquisamos "dog" na legenda de uma imagem. Podemos esperar ver um valor alto na contagem de faceta animal->cães, mas também veremos muitas outras facetas com contagens.
Por exemplo, você pode notar uma contagem de 68 na faceta esportes->surfboard.
saber disso permitiria filtrar ainda mais os resultados como:
Como há um pouco de complexidade nessa query, vamos criar um novo método na classe Principal para lidar com a pesquisa. Insira o seguinte após o método principal:
1 private static ImageSearchResult search(MongoCollection<Image> imageCollection, String caption, Integer page, Boolean hasPerson, List<String> accessory, List<String> animal, List<String> appliance, List<String> electronic, List<String> food, List<String> furniture, List<String> indoor, List<String> kitchen, List<String> outdoor, List<String> sports, List<String> vehicle) { 2 int skip = 0; 3 int pageSize = 5; 4 if (page != null) { 5 skip = page * pageSize; 6 } 7 8 9 List<SearchOperator> clauses = new ArrayList<>(); 10 if (caption != null) { 11 clauses.add(SearchOperator 12 .text( 13 fieldPath("caption"), 14 caption 15 ) 16 ); 17 } 18 if (hasPerson != null) { 19 clauses.add(equals("hasPerson", hasPerson)); 20 } 21 BiConsumer<String, List<String>> addConditional = (String category, List<String> values) -> { 22 if (values != null) { 23 for (String value : values) { 24 clauses.add(equals(category, value)); 25 } 26 } 27 }; 28 addConditional.accept("accessory", accessory); 29 addConditional.accept("animal", animal); 30 addConditional.accept("appliance", appliance); 31 addConditional.accept("electronic", electronic); 32 addConditional.accept("food", food); 33 addConditional.accept("furniture", furniture); 34 addConditional.accept("indoor", indoor); 35 addConditional.accept("kitchen", kitchen); 36 addConditional.accept("outdoor", outdoor); 37 addConditional.accept("sports", sports); 38 addConditional.accept("vehicle", vehicle); 39 40 List<StringSearchFacet> facets = List.of( 41 stringFacet("accessory", fieldPath("accessory")).numBuckets(10), 42 stringFacet("animal", fieldPath("animal")).numBuckets(10), 43 stringFacet("appliance", fieldPath("appliance")).numBuckets(10), 44 stringFacet("electronic", fieldPath("electronic")).numBuckets(10), 45 stringFacet("food", fieldPath("food")).numBuckets(10), 46 stringFacet("furniture", fieldPath("furniture")).numBuckets(10), 47 stringFacet("indoor", fieldPath("indoor")).numBuckets(10), 48 stringFacet("kitchen", fieldPath("kitchen")).numBuckets(10), 49 stringFacet("outdoor", fieldPath("outdoor")).numBuckets(10), 50 stringFacet("sports", fieldPath("sports")).numBuckets(10), 51 stringFacet("vehicle", fieldPath("vehicle")).numBuckets(10) 52 ); 53 54 55 List<Bson> aggregateStages = List.of( 56 Aggregates.search( 57 SearchCollector.facet( 58 SearchOperator.compound().filter(clauses), 59 facets 60 ), SearchOptions.searchOptions().count(SearchCount.total())), 61 Aggregates.skip(skip), 62 Aggregates.limit(pageSize), 63 Aggregates.facet( 64 new Facet("docs", List.of()), 65 new Facet("meta", List.of( 66 Aggregates.replaceWith("$$SEARCH_META"), 67 Aggregates.limit(1) 68 )) 69 ) 70 ); 71 72 73 ImageSearchResult imageSearchResult = imageCollection.aggregate(aggregateStages, ImageSearchResult.class).first(); 74 75 76 return imageSearchResult; 77 } 78 79 private static SearchOperator equals(String fieldName, Object value) { 80 return SearchOperator.of( 81 new Document("equals", new Document() 82 .append("path", fieldName) 83 .append("value", value) 84 )); 85 }
Em seguida, vamos chamar a nova função do nosso manipulador de serviço /image. Substitua o TODO que você deixou anteriormente:
1 //TODO: Implement the search for images based on the query parameters. For now we just return the first image. 2 List<Image> images = imageCollection.find().limit(1).into(new ArrayList<>()); 3 //----------------
Com:
1 ImageSearchResult images = search(imageCollection, 2 params.containsKey("caption") ? params.get("caption").getFirst() : null, 3 params.containsKey("page") ? Integer.parseInt(params.get("page").getFirst()) : null, 4 params.containsKey("hasPerson") ? Boolean.parseBoolean(params.get("hasPerson").getFirst()) : null, 5 params.get("accessory"), 6 params.get("animal"), 7 params.get("appliance"), 8 params.get("electronic"), 9 params.get("food"), 10 params.get("furniture"), 11 params.get("indoor"), 12 params.get("kitchen"), 13 params.get("outdoor"), 14 params.get("sports"), 15 params.get("vehicle") 16 );
Isso usará os parâmetros de query passados para a API e chamará nossa função de pesquisa. Vamos dar uma volta e depois voltaremos e detalharemos o método de pesquisa pedaço por pedaço.
Agora você pode começar a ver como as facetas funcionam. Vamos pesquisar o termo "riding" na legenda de nossa imagem e filtrar ainda mais para obter apenas imagens com um campo e uma mala:
Incrível. rs
Então, vamos percorrer o método de pesquisa e explicar cada parte, peça por peça.
Nossa pesquisa agregada na coleção de imagens do MongoDB terá os seguintes estágios:
1 [ 2 { 3 $search: { 4 facet: { 5 operator: { 6 compound: { 7 filter: [ <Filter clauses go here!> ] 8 } 9 }, 10 facets: { <List the facets we want returned> } 11 } 12 } 13 }, 14 <Paging> 15 {"$skip": 0},{"$limit": 5}, 16 <Return structure - page of docs + meta (facet counts)> 17 {$facet: {docs: [], meta: [...]}} 18 ]
Você pode visualizar a composição de cada estágio no código Java :
1 int skip = 0; 2 int pageSize = 5; 3 if (page != null) { 4 skip = page * pageSize; 5 }
Primeiro, calculamos o número de documentos a serem ignorados para paginação e definimos o tamanho da página como 5. Eles são passados para os estágios de pular e limitar.
Nesta seção do método de pesquisa, reunimos uma Lista<Bson> que serão as cláusulas de filtro:
1 List<SearchOperator> clauses = new ArrayList<>(); 2 if (caption != null) { 3 clauses.add(SearchOperator 4 .text( 5 fieldPath("caption"), 6 caption 7 ) 8 ); 9 } 10 if (hasPerson != null) { 11 clauses.add(equals("hasPerson", hasPerson)); 12 } 13 BiConsumer<String, List<String>> addConditional = (String category, List<String> values) -> { 14 if (values != null) { 15 for (String value : values) { 16 clauses.add(equals(category, value)); 17 } 18 } 19 }; 20 addConditional.accept("accessory", accessory); 21 addConditional.accept("animal", animal); 22 addConditional.accept("appliance", appliance); 23 addConditional.accept("electronic", electronic); 24 addConditional.accept("food", food); 25 addConditional.accept("furniture", furniture); 26 addConditional.accept("indoor", indoor); 27 addConditional.accept("kitchen", kitchen); 28 addConditional.accept("outdoor", outdoor); 29 addConditional.accept("sports", sports); 30 addConditional.accept("vehicle", vehicle);
Usamos um pequeno método assistente , addConditional, que verifica se o parâmetro era nulo e, se não fosse, adiciona cada valor com uma cláusula igual para as categorias. Outro pequeno método assistente , equals(), constrói o Documento para nossa cláusula igual.
A primeira cláusula usa o operador de pesquisa de texto. Há uma profundidade enorme nesse operador, que não vamos abordar por completo aqui, mas você pode personalizá-lo para dar suporte:
- Pesquisa de texto simples (como estamos usando aqui).
- Pesquisa de texto difusa configurável para lidar com erros de digitação — "ridung" -> equitação.
- Curingas como "rid*".
- Strings de query complexas como "riding AND NOT (bicycle OR horse)".
- Frases para encontrar sequências de várias palavras.
- Combine mais de uma dessas abordagens em uma única pesquisa e classifique os resultados pela melhor interseção dos resultados correspondentes.
Em seguida, reunimos uma lista dos facets que queremos devolver:
1 List<StringSearchFacet> facets = List.of( 2 stringFacet("accessory", fieldPath("accessory")).numBuckets(10), 3 stringFacet("animal", fieldPath("animal")).numBuckets(10), 4 stringFacet("appliance", fieldPath("appliance")).numBuckets(10), 5 stringFacet("electronic", fieldPath("electronic")).numBuckets(10), 6 stringFacet("food", fieldPath("food")).numBuckets(10), 7 stringFacet("furniture", fieldPath("furniture")).numBuckets(10), 8 stringFacet("indoor", fieldPath("indoor")).numBuckets(10), 9 stringFacet("kitchen", fieldPath("kitchen")).numBuckets(10), 10 stringFacet("outdoor", fieldPath("outdoor")).numBuckets(10), 11 stringFacet("sports", fieldPath("sports")).numBuckets(10), 12 stringFacet("vehicle", fieldPath("vehicle")).numBuckets(10) 13 );
Aqui, estamos dizendo que queremos todos os campos que indexamos com stringFacet em nosso índice do Atlas Search , permitindo que eles sejam contados. Também estamos especificando 10 como o número máximo de buckets para contar. Isso significa que obteremos os principais 10 resultados por supercategoria, com suas contagens. Por exemplo, digamos que havia 15 cães com uma contagem para uma pesquisa sobre a legenda "grass." Nesse caso, receberiamos apenas as 10 principais contagens de cães — os cinco menos significativos seriam omitidos.
Finalmente, reunimos todos os estágios agregados e chamamos a pesquisa:
1 List<Bson> aggregateStages = List.of( 2 Aggregates.search( 3 SearchCollector.facet( 4 SearchOperator.compound().filter(clauses), 5 facets 6 ), SearchOptions.searchOptions().count(SearchCount.total())), 7 Aggregates.skip(skip), 8 Aggregates.limit(pageSize), 9 Aggregates.facet( 10 new Facet("docs", List.of()), 11 new Facet("meta", List.of( 12 Aggregates.replaceWith("$$SEARCH_META"), 13 Aggregates.limit(1) 14 )) 15 ) 16 ); 17 18 ImageSearchResult imageSearchResult = imageCollection.aggregate(aggregateStages, ImageSearchResult.class).first(); 19 return imageSearchResult;
A parte mais interessante, e talvez também a parte mais confusa, é o uso do estágio agregado final, faceta. Este é um uso diferente do termo faceta, em que estamos criando duas facets para nosso resultado agregado. Este é um pequeno pedaço da sorte do Atlas Search que nos permite retornar os documentos paginados da pesquisa, juntamente com os metadados para as facetas. Melhor não pensar muito nisso. Cerre os dente e ponha isso ai.
Se você realmente precisa saber, pode encontrar a documentação para usar o coletor de faceta com a variável da estrutura de agregação $$SEARCH_META.
Se quiser, você pode conferir o mesmo código refatorado em algumas classes separadas em uma estrutura geral melhor, que pode ser a base de um servidor API real.
Este projeto também tem o código usado para baixar e transformar o conjunto de dados COCO em nosso modelo de domínio e criar o índice. Existem algumas vantagens se você for explorar o teste de unidade do Atlas Search usando o incrivel projeto Test Containers.
Então aqui está. O início de um serviço surpreendente utilizando Atlas Search para realizar pesquisa avançada de texto, filtragem e contagem de faceta !
O uso do conjunto de dados COCO aqui mostra um exemplo interessante de como os dados gerados usando o aprendizado de máquina podem ser combinados com a pesquisa de texto lexical mais tradicional. Isso fornece resultados de pesquisa repetíveis, consistentes e intuitivos para os consumidores de sua API.
O facet permite criar conjuntos de resultados filtráveis com contagens em cada categoria filtrável retornada. Isso oferece suporte a interfaces de usuário avançadas em uma query de pesquisa única de alto desempenho.
O uso da linguagem Java e da Java Virtual Machine (JVM) como tempo de execução fornece uma plataforma altamente consistente, fortemente digitada e confiável para a criação de APIs escaláveis. As funcionalidades da linguagem , em particular , combinam bem com o MongoDB. Java é uma ótima linguagem para aplicar seu esquema e evoluí-lo ao longo do tempo. Essa combinação de verificações de tempo de compilação e tempo de execução quanto à consistência no esquema de código, juntamente com alterações flexíveis no esquema de banco de dados do MongoDB à medida que você o desenvolve ao longo do tempo, é uma ótima correspondência.
Leia mais sobre Atlas Search, facet do Atlas Search , executando o Atlas localmente com Docker e o driver Java para os recursos de pesquisa do Atlas Search do MongoDB.
Se você tiver alguma dúvida ou quiser saber mais, não hesite em me procurar no LinkedIn.
Boa codificação!
Principais comentários nos fóruns
Ainda não há comentários sobre este artigo.