Explore o novo chatbot do Developer Center! O MongoDB AI chatbot pode ser acessado na parte superior da sua navegação para responder a todas as suas perguntas sobre o MongoDB .

Junte-se a nós no Amazon Web Services re:Invent 2024! Saiba como usar o MongoDB para casos de uso de AI .
Desenvolvedor do MongoDB
Central de desenvolvedor do MongoDBchevron-right
Produtoschevron-right
Atlaschevron-right

Como criar um serviço de pesquisa em Java

Erik Hatcher10 min read • Published Feb 13, 2024 • Updated Apr 23, 2024
PesquisaJavaAtlas
Ícone do FacebookÍcone do Twitterícone do linkedin
Avalie esse Artigo
star-empty
star-empty
star-empty
star-empty
star-empty
Precisamos codificar nosso caminho da caixa de pesquisa para o índice de pesquisa. Realizar uma pesquisa e renderizar os resultados de forma apresentável, por si só, não é uma tarefa complicada: envie a consulta do usuário para o servidor de pesquisa e traduza os dados de resposta em alguma tecnologia de interface de usuário. No entanto, há algumas questões importantes que precisam ser abordadas, como segurança, tratamento de erros, desempenho e outras preocupações que merecem isolamento e controle.
Um sistematípico de três camadas tem uma camada de apresentação que envia solicitações do usuário para uma camada intermediária, ou servidor de aplicativos, que faz interface com serviços de dados de back-end. Esses níveis separam as preocupações para que cada uma possa se concentrar em suas próprias responsabilidades.
Arquitetura de três níveis
Se você criou um aplicativo para gerenciar uma coleção de banco de dados, sem dúvidas implementou instalações do Create-Read-Update-Delete (CRUD) que isolam a lógica de negócios em uma camada de aplicativos intermediária.
A pesquisa é um tipo de serviço um pouco diferente, pois é somente de leitura, é acessada com muita frequência, precisa responder rapidamente para ser útil e geralmente retorna mais do que apenas documentos. Metadados adicionais retornados dos resultados da pesquisa geralmente incluem destaque de palavras-chave, pontuações de documentos, facetas e o número de resultados encontrados. Além disso, as pesquisas geralmente correspondem a muito mais documentos do que os razoavelmente apresentáveis e, portanto, a paginação e as pesquisas filtradas são recursos necessários.
Nosso serviço de busca oferece os benefícios de três níveis descritos acima das seguintes maneiras:
  • Segurança: a connection string do banco de dados é isolada no ambiente do serviço. Os parâmetros são validados e higienizados. O cliente/usuário não pode solicitar um grande número de resultados ou fazer paginação profunda.
  • Escalabilidade: O serviço é sem estado e pode ser facilmente implantado várias vezes e com balanceamento de carga.
  • Implantação mais rápida: os endpoints de serviço podem ter controle de versão e continuar em execução enquanto as versões aprimoradas são implantadas. O comportamento pode ser modificado sem afetar necessariamente o nível de apresentação ou o banco de dados e as configurações do índice de pesquisa.
Neste artigo, vamos detalhar um serviço de pesquisa HTTP Java projetado para ser chamado de uma camada de apresentação e, por sua vez, ele traduz a solicitação em um pipeline de agregação que consulta nossa camada de dados do Atlas. Esta é puramente uma implementação de serviço, sem interface do usuário final; A interface do usuário é deixada como um exercício para o leitor. Em outras palavras, o autor tem profunda experiência no fornece serviços de pesquisa para interfaces de usuário, mas não é ele próprio um desenvolvedor de UI. :)

Pré-requisitos

O código para este artigo está no repositório GitHub.
Este projeto foi construído usando:
  • Gradle 8.5
  • Java 21
As APIs Java e servlets padrão são usadas e devem funcionar como estão ou portadas facilmente para versões Java posteriores.
Para executar os exemplos fornecidos aqui, os dados de amostra do Atlas precisam ser carregados e um movies_index, conforme descrito abaixo, criado na coleçãosample_mflix.movies. Se você é novo no Atlas Search, um bom ponto de partida é Usar o Atlas Search do Java.

Design de serviço de pesquisa

A camada de apresentação front-end fornece uma caixa de pesquisa, renderiza os resultados da pesquisa e fornece controles de classificação, paginação e filtragem. Uma camada intermediária, por meio de uma solicitação HTTP, valida e traduz os parâmetros da solicitação de pesquisa em uma especificação de pipeline de agregação que é então enviada para a camada de dados.
Um serviço de pesquisa precisa ser rápido, escalável e lidar com estes parâmetros básicos:
  • A query em si: foi o que o usuário inseriu na caixa de pesquisa.
  • Número de resultados a serem retornados: geralmente, apenas 10 ou mais resultados são necessários de cada vez.
  • Ponto de partida dos resultados da pesquisa: permite a paginação dos resultados da pesquisa.
Além disso, uma query de desempenho só deve pesquisar e retornar um pequeno número de campos, embora não necessariamente os mesmos campos pesquisados precisem ser retornados. Por exemplo, ao pesquisar filmes, você pode pesquisar no campofullplot, mas não retornar o texto potencialmente grande para apresentação. Ou você pode querer incluir o ano em que o filme foi lançado nos resultados, mas não pesquisar o campoyear.
Além disso, um serviço de pesquisa deve oferecer uma maneira de restringir os resultados da pesquisa a, por exemplo, uma categoria, um gênero ou um membro do elenco específico, sem afetar a ordem de relevância dos resultados. Esse recurso de filtragem também pode ser usado para impor o controle de acesso, e uma camada de serviço é o local ideal para adicionar essas restrições nas quais a camada de apresentação pode confiar em vez de gerenciar.

Interface do serviço de pesquisa

Vamos agora definir concretamente a interface de serviço com base no design. Nosso objetivo é oferecer suporte a uma solicitação, como encontrarMusic filmes do gênero " " para a consulta "purple rain" em relação aos title plot campos e , retornando apenas cinco resultados por vez que incluam somente os campos título, gêneros, enredo e ano. Essa solicitação, da perspectiva da nossa camada de apresentação, é esta solicitação HTTP GET:
1http://service_host:8080/search?q=purple%20rain&limit=5&skip=0&project=title,genres,plot,year&search=title,plot&filter=genres:Music
Esses parâmetros, juntamente com um parâmetrodebug, são detalhados na tabela a seguir:
parameterDescrição
qEsta é uma query de texto completo, normalmente o valor inserido pelo usuário em uma caixa de pesquisa.
searchEsta é uma lista de campos separados por vírgula para pesquisar usando o parâmetro de consulta (q).
limitRetorne apenas esse número máximo de resultados, restrito a um máximo de 25 resultados.
skipRetorne os resultados a partir desse número de resultados (até o númerolimit de resultados), com um máximo de 100 resultados ignorados.
projectEsta é uma lista de campos separados por vírgula para retornar para cada documento. Adicione _id se for necessário. _score é um "pseudo-field " usado para incluir a pontuação de relevância computada.
filtersintaxe <field name>:<exact value>; suporta zero ou mais parâmetrosfilter .
debugSe for true, inclua também a saída completa do pipeline de agregação .explain () na resposta.

Resultados retornados

Dada a solicitação especificada, definiremos a estrutura JSON de resposta para retornar os campos solicitados (project) dos documentos correspondentes em uma array docs . Além disso, o serviço de pesquisa retorna uma seçãorequest mostrando os parâmetros explícitos e implícitos usados para construir o Atlas Search do Atlas e uma seçãometa que retornará a contagem total de documentos correspondentes. Essa estrutura é inteiramente nosso design, não pretende ser uma passagem direta da resposta do aggregation pipeline, permitindo-nos isolar, manipular e mapear a resposta da forma como ela melhor se adequa às necessidades do nosso nível de apresentação.
1{
2 "request": {
3 "q": "purple rain",
4 "skip": 0,
5 "limit": 5,
6 "search": "title,plot",
7 "project": "title,genres,plot,year",
8 "filter": [
9 "genres:Music"
10 ]
11 },
12 "docs": [
13 {
14 "plot": "A young musician, tormented by an abusive situation at home, must contend with a rival singer, a burgeoning romance and his own dissatisfied band as his star begins to rise.",
15 "genres": [
16 "Drama",
17 "Music",
18 "Musical"
19 ],
20 "title": "Purple Rain",
21 "year": 1984
22 },
23 {
24 "plot": "Graffiti Bridge is the unofficial sequel to Purple Rain. In this movie, The Kid and Morris Day are still competitors and each runs a club of his own. They make a bet about who writes the ...",
25 "genres": [
26 "Drama",
27 "Music",
28 "Musical"
29 ],
30 "title": "Graffiti Bridge",
31 "year": 1990
32 }
33 ],
34 "meta": [
35 {
36 "count": {
37 "total": 2
38 }
39 }
40 ]
41}

Implementação do serviço de busca

Código! É onde está. Mantendo as coisas o mais simples possível para que nossa implementação seja útil para cada tecnologia de front-end, estamos implementando um HTTP Service que funciona com parâmetros de solicitação GET padrão e retorna JSON facilmente digerível. E Java é a nossa linguagem de escolha aqui, então vamos fazer isso. Codificar é uma tarefa opinativa, então reconhecemos que existem várias maneiras de fazer isso em Java e em outras linguagens — aqui está uma maneira opinativa (e experiente) de fazer isso.
Para executar a configuração apresentada aqui, um bom ponto de partida é começar a usar os exemplos do artigo Como usar o Atlas Search do Java. Depois de executar isso, crie um novo índice, chamado movies_index, com uma configuração de índice personalizada, conforme especificado no seguinte JSON:
1{
2 "analyzer": "lucene.english",
3 "searchAnalyzer": "lucene.english",
4 "mappings": {
5 "dynamic": true,
6 "fields": {
7 "cast": [
8 {
9 "type": "token"
10 },
11 {
12 "type": "string"
13 }
14 ],
15 "genres": [
16 {
17 "type": "token"
18 },
19 {
20 "type": "string"
21 }
22 ]
23 }
24 }
25}
Aqui está o esqueleto da implementação, um ponto de entrada do servletpadrãodoGet , utilizando todos os parâmetros que especificamos:
1public class SearchServlet extends HttpServlet {
2 private MongoCollection<Document> collection;
3 private String indexName;
4
5 private Logger logger;
6
7 // ...
8 @Override
9 protected void doGet(HttpServletRequest request, HttpServletResponse response) throws IOException {
10 String q = request.getParameter("q");
11 String searchFieldsValue = request.getParameter("search");
12 String limitValue = request.getParameter("limit");
13 String skipValue = request.getParameter("skip");
14 String projectFieldsValue = request.getParameter("project");
15 String debugValue = request.getParameter("debug");
16 String[] filters = request.getParameterMap().get("filter");
17
18 // ...
19 }
20}
Observe que foram definidas algumas variáveis de instância, que são inicializadas no método padrão do servletinita partir dos valores especificados no descritor de implantação web.xml, bem como na variável de ambienteATLAS_URI:
1 @Override
2 public void init(ServletConfig config) throws ServletException {
3 super.init(config);
4
5 logger = Logger.getLogger(config.getServletName());
6
7 String uri = System.getenv("ATLAS_URI");
8 if (uri == null) {
9 throw new ServletException("ATLAS_URI must be specified");
10 }
11
12 String databaseName = config.getInitParameter("database");
13 String collectionName = config.getInitParameter("collection");
14 indexName = config.getInitParameter("index");
15
16 MongoClient mongo_client = MongoClients.create(uri);
17 MongoDatabase database = mongo_client.getDatabase(databaseName);
18 collection = database.getCollection(collectionName);
19
20 logger.info("Servlet " + config.getServletName() + " initialized: " + databaseName + " / " + collectionName + " / " + indexName);
21 }
Para a melhor proteção de nossa connection stringATLAS_URI, a definimos no ambiente para que ela não fique codificada nem visível no próprio aplicativo, exceto na inicialização, enquanto especificamos o banco de dados, a collection e os nomes de índice dentro do padrão Descritor de implantação doweb.xml que nos permite definir pontos de conexão para cada índice que queremos suportar. Aqui está uma definição básica de web.xml:
1<web-app>
2 <servlet>
3 <servlet-name>SearchServlet</servlet-name>
4 <servlet-class>com.mongodb.atlas.SearchServlet</servlet-class>
5 <load-on-startup>1</load-on-startup>
6 <!--
7 The connection string must be defined in the
8 `ATLAS_URI` environment variable
9 -->
10 <init-param>
11 <param-name>database</param-name>
12 <param-value>sample_mflix</param-value>
13 </init-param>
14 <init-param>
15 <param-name>collection</param-name>
16 <param-value>movies</param-value>
17 </init-param>
18 <init-param>
19 <param-name>index</param-name>
20 <param-value>movies_index</param-value>
21 </init-param>
22 </servlet>
23
24 <servlet-mapping>
25 <servlet-name>SearchServlet</servlet-name>
26 <url-pattern>/search</url-pattern>
27 </servlet-mapping>
28</web-app>

obtendo os resultados da pesquisa

A solicitação de resultados de pesquisa é uma operação sem monitoração de estado, sem efeitos colaterais no banco de dados e funciona bem como uma solicitação HTTP GET direta, pois a query em si não deve ser uma string muito longa. Nossa camada front-end pode restringir o comprimento adequadamente. Solicitações maiores podem ser suportadas ajustando-se a POST/getPost, se necessário.

Pipeline de agregação nos bastidores

Por fim, para dar suporte às informações que queremos retornar (conforme mostrado acima no exemplo de resposta), o exemplo de solicitação mostrado acima é transformado nessa solicitação de pipeline de agregação:
1[
2 {
3 "$search": {
4 "compound": {
5 "must": [
6 {
7 "text": {
8 "query": "purple rain",
9 "path": [
10 "title",
11 "plot"
12 ]
13 }
14 }
15 ],
16 "filter": [
17 {
18 "equals": {
19 "path": "genres",
20 "value": "Music"
21 }
22 }
23 ]
24 },
25 "index": "movies_index",
26 "count": {
27 "type": "total"
28 }
29 }
30 },
31 {
32 "$facet": {
33 "docs": [
34 {
35 "$skip": 0
36 },
37 {
38 "$limit": 5
39 },
40 {
41 "$project": {
42 "title": 1,
43 "genres": 1,
44 "plot": 1,
45 "year": 1,
46 "_id": 0,
47 }
48 }
49 ],
50 "meta": [
51 {
52 "$replaceWith": "$$SEARCH_META"
53 },
54 {
55 "$limit": 1
56 }
57 ]
58 }
59 }
60]
Há alguns aspectos desse pipeline de agregação gerado que vale a pena explicar melhor:
  • A query (q) é convertida em um text operador sobre os search campos especificados. Ambos os parâmetros são necessários nesta implementação.
  • filter Os parâmetros são traduzidos em filter cláusulas sem pontuação usando o equals operador . O equals operador exige que os campos de string sejam indexados como um token tipo ; é por isso que você vê os genres cast campos e configurados para string serem token tipos e . Esses dois campos podem ser pesquisados em texto completo (por meio do text ou de outros operadores de suporte do tipo string) ou usados como equals filtros de correspondência exata .
  • A contagem de documentos correspondentes é solicitada em $search, que é retornada na variável de agregação $$SEARCH_META. Como esses metadados não são específicos de um documento, eles precisam de um tratamento especial para serem retornados da chamada de agregação para o nosso servidor de pesquisa. É por isso que o estágio $faceté aproveitado, de modo que essas informações sejam puxadas para uma seçãometa da resposta do nosso serviço.
O uso de $facet é um pouco trabalhoso, que também dá ao nosso aggregation pipeline espaço de resposta para expansão futura.
$facet estágio de agregação é confusamente chamado da mesma forma que o Atlas Search coletor defacet . As facetas do resultado da pesquisa fornecem um rótulo de grupo e a contagem desse grupo nos resultados da pesquisa correspondentes. Por exemplo, o facetamento em genres (que requer um ajuste de configuração de índice do exemplo aqui) forneceria, além dos documentos que correspondem aos critérios de pesquisa, uma lista de todos os genres dentro desses resultados de pesquisa e a contagem de quantos de cada um. Adicionar o operadorfacet a esse serviço de pesquisa está no roteiro mencionado abaixo.

$search em código

Dada uma consulta (q), uma lista de campos de pesquisa (search) e filtros (zero ou mais filterparâmetros), construir o estágio$searchprogramaticamente é simples usando os métodos de conveniência do driver Java:
1 // $search
2 List<SearchPath> searchPath = new ArrayList<>();
3 for (String search_field : searchFields) {
4 searchPath.add(SearchPath.fieldPath(search_field));
5 }
6
7 CompoundSearchOperator operator = SearchOperator.compound()
8 .must(List.of(SearchOperator.text(searchPath, List.of(q))));
9 if (filterOperators.size() > 0)
10 operator = operator.filter(filterOperators);
11
12 Bson searchStage = Aggregates.search(
13 operator,
14 SearchOptions.searchOptions()
15 .option("scoreDetails", debug)
16 .index(indexName)
17 .count(SearchCount.total())
18 );
Adicionamos o recurso scoreDetailsdo Atlas Search quandodebug=true, o que nos permite introspectar os detalhes da pontuação do Lucene somente quando desejado; a solicitação de detalhes da pontuação representa um pequeno impacto no desempenho e, em geral, só é útil para a solução de problemas.

Projeção de campo

A última parte interessante de nossa implementação de serviços envolve a projeção de campo. Retornar _id ou não o campo requer tratamento especial. Nosso código de serviço procura a presença de _id no project parâmetro e o desativa explicitamente se não for especificado. Também adicionamos um recurso para incluir a pontuação de relevância computada do documento, se desejado, procurando um _score pseudocampo especial especificado no project parâmetro . A construção programática do estágio de projeção tem a seguinte aparência :
1 List<String> projectFields = new ArrayList<>();
2 if (projectFieldsValue != null) {
3 projectFields.addAll(List.of(projectFieldsValue.split(",")));
4 }
5
6 boolean include_id = false;
7 if (projectFields.contains("_id")) {
8 include_id = true;
9 projectFields.remove("_id");
10 }
11
12 boolean includeScore = false;
13 if (projectFields.contains("_score")) {
14 includeScore = true;
15 projectFields.remove("_score");
16 }
17
18 // ...
19
20 // $project
21 List<Bson> projections = new ArrayList<>();
22 if (projectFieldsValue != null) {
23 // Don't add _id inclusion or exclusion if no `project` parameter specified
24 projections.add(Projections.include(projectFields));
25 if (include_id) {
26 projections.add(Projections.include("_id"));
27 } else {
28 projections.add(Projections.excludeId());
29 }
30 }
31 if (debug) {
32 projections.add(Projections.meta("_scoreDetails", "searchScoreDetails"));
33 }
34 if (includeScore) {
35 projections.add(Projections.metaSearchScore("_score"));
36 }

Agregação e resposta

Bem simples no final da cadeia de parâmetros e da construção do estágio, construímos o pipeline completo, fizemos nossa chamada para o Atlas, construímos uma resposta JSON e a devolvemos ao cliente de chamada. A única coisa única aqui é adicionar a chamada.explain() quando debug=true para que nosso cliente possa ver a imagem completa do que aconteceu da perspectiva do Atlas:
1 AggregateIterable<Document> aggregationResults = collection.aggregate(List.of(
2 searchStage,
3 facetStage
4 ));
5
6 Document responseDoc = new Document();
7 responseDoc.put("request", new Document()
8 .append("q", q)
9 .append("skip", skip)
10 .append("limit", limit)
11 .append("search", searchFieldsValue)
12 .append("project", projectFieldsValue)
13 .append("filter", filters==null ? Collections.EMPTY_LIST : List.of(filters)));
14
15 if (debug) {
16 responseDoc.put("debug", aggregationResults.explain().toBsonDocument());
17 }
18
19 // When using $facet stage, only one "document" is returned,
20 // containing the keys specified above: "docs" and "meta"
21 Document results = aggregationResults.first();
22 if (results != null) {
23 for (String s : results.keySet()) {
24 responseDoc.put(s,results.get(s));
25 }
26 }
27
28 response.setContentType("text/json");
29 PrintWriter writer = response.getWriter();
30 writer.println(responseDoc.toJson());
31 writer.close();
32
33 logger.info(request.getServletPath() + "?" + request.getQueryString());

Indo para produção

Esta é uma extensão de servlet Java padrão projetada para ser executada no Tomcat, Jetty ou outros container compatíveis com API de servlet. A construção executa o Gratty, que permite sem problemas a um desenvolvedor jettyRun ou tomcatRun para iniciar esse exemplo de serviço de pesquisa Java.
Para criar uma distribuição que possa ser implementada em um ambiente de produção, execute:
1./gradlew buildProduct

Roteiro futuro

Nosso serviço de pesquisa, como está, é robusto o suficiente para casos de uso de pesquisa básica, mas há espaço para melhorias. Aqui estão algumas ideias para a evolução futura do serviço:
  • Adicione filtros negativos. Atualmente, aceitamos filtros positivos com o parâmetro filter=field:value . Um filtro negativo pode ter um sinal de menos na frente. Por exemplo, para excluir filmes "Drama ", o suporte parafilter=-genres:Drama pode ser implementado.
  • Destaque de suporte, para retornar trechos de valores de campo que correspondam aos termos da consulta.
  • Implementar facetamento.
  • E assim por diante... Veja a lista de problemas para ideias adicionais e para adicionar as suas próprias.
E com a camada de serviço sendo uma camada intermediária que pode ser implantada de forma independente sem necessariamente ter que fazer alterações de front-end ou camada de dados, algumas delas podem ser adicionadas sem exigir alterações nessas camadas.

Conclusão

A implementação de um serviço de pesquisa de nível intermediário oferece vários benefícios, desde segurança até escalabilidade e capacidade de isolar mudanças e implantações independentemente da camada de apresentação e de outros clientes de pesquisa. Além disso, um Atlas Search permite que os clientes aproveitem facilmente recursos de pesquisa sofisticados usando técnicas padrão de HTTP e JSON.
Para saber os fundamentos do uso do Java com o Atlas Search, consulte Usando o Atlas Search do Java | MongoDB. Conforme você começar a aproveitar o Atlas Search, não deixe de conferir o recursoQuery Analytics para ajudar a melhorar seus resultados de pesquisa.
Principais comentários nos fóruns
Ainda não há comentários sobre este artigo.
Iniciar a conversa

Ícone do FacebookÍcone do Twitterícone do linkedin
Avalie esse Artigo
star-empty
star-empty
star-empty
star-empty
star-empty
Relacionado
Tutorial

Java encontra Queryable Encryption: desenvolvendo um aplicativo de conta bancária seguro


Oct 08, 2024 | 14 min read
Tutorial

Como usar o PyMongo para conectar o MongoDB Atlas ao AWS Lambda


Apr 02, 2024 | 6 min read
Tutorial

Introdução ao Atlas Stream Processing: como criar seu primeiro processador de fluxo


Aug 13, 2024 | 4 min read
Tutorial

Chamando a API de administração do MongoDB Atlas: como fazer isso usando Node, Python e Ruby


Jun 18, 2024 | 4 min read
Sumário