Recentemente tive a oportunidade de palestrar no TDC São Paulo sobre como criar um mecanismo de busca com .NET Core e MongoDB. A ideia deste post é servir de apoio à palestra com mais detalhes, os fontes e os slides, para que a audiência consiga estudar em casa com mais calma.
Obviamente, serve também para quem não estava no evento e deseja saber como fazer um site de busca em ASP.NET MVC Core e usando o banco não-relacional MongoDB.
Introdução
Desde 2010, quando estava me formando na faculdade e resolvi criar o Busca Acelerada, o primeiro mecanismo de busca que desenvolvi, acabei gostando bastante desse tipo de aplicação. De lá pra cá tive a oportunidade de desenvolver buscadores de legislação, de fofocas de famosos, de informações da construção civil e muito mais.
A ideia então é eu mostrar, rapidamente, como se cria um site básico de busca que, embora simples, já será muito superior à maioria dos buscadores que os desenvolvedores fazem baseados em SQL. Sim, porque o meu site de busca não usará SQL, mas sim NoSQL, MongoDB para ser mais exato. E para a performance ficar ainda melhor, ele será feito usando .NET Core, a nova plataforma de desenvolvimento da Microsoft!
Para conseguir acompanhar este post, você já deve conhecer .NET Core. Caso não conheça, sugiro começar com este tutorial aqui. Também é altamente recomendado que tenha lido esse meu outro post, sobre como criar mecanismos de busca.
Note que não vou ensinar aqui como se criam crawlers ou qualquer outro algoritmo de coleta de informações para popular seu mecanismo, conforme citado no post sobre mecanismo de busca. Considero aqui que você já tem uma massa de dados que deseja oferecer através de um site de busca. Pode ser uma base SQL tradicional, um XML, um Excel, você que sabe.
Caso você precise realmente de um crawler, leia este post aqui.
Com tudo isso em mente, vamos começar!
Veremos nesse artigo:
- Configurando o Projeto
- Configurando o Banco
- Preparando os dados
- Configurando a Model
- Criando as views
- Programando a busca
Querendo algo mais “pesado” de MongoDB, sugiro este post aqui, focado nesta tecnologia de banco de dados.
Parte 1: Configurando o projeto
Baixe e instale o .NET Core na sua máquina.
Agora, baixe e instale o Visual Studio Community na sua máquina, é gratuito e está disponível para Mac e Windows. Se estiver no Linux, você pode usar linha de comando ou o Visual Studio Code, mas o procedimento será um pouco diferente do mostrado aqui.
Com o Visual Studio aberto, crie um novo projeto ASP.NET Core Web App.
Assim que a estrutura do projeto é criada, o VS começa a restaurar os pacotes padrões do projeto e isso pode demorar alguns instantes.
Agora clique com botão direito na pasta Dependencies/NuGet do projeto e dê um “Add Packages” para abrir o gerenciador de dependências. Busque nele por “mongodb” e instale a extensão oficial:
Se você mandar executar o projeto irá ver o site MVC de exemplo do ASP.NET Core. Os demais arquivos vamos mexer depois.
Parte 2: Configurando o banco
Aqui você tem duas opções: usar um Mongo em uma plataforma, como a MongoDB Atlas, e outra é instalar e configurar tudo na unha. Você escolhe. Falo do Atlas no vídeo abaixo e também neste post.
Opção 2: Solução local
Baixe e instale o MongoDB (é apenas uma extração de pastas e arquivos, que sugiro que faça em C:/). No vídeo abaixo eu mostro todo o passo a passo, se preferir.
Agora abra o prompt de comando, navegue até a pasta onde foi instalado o seu Mongo, geralmente em “c:program filesmongodb”, acesse a subpasta “server/3.2/bin” e dentro dela digite o comando (certificando-se que exista uma pasta data dentro de C:mongodb):
1 2 3 |
c:mongodbserver3.2bin> mongod --dbpath c:mongodbdata |
Isso irá criar e deixar executando uma instância do MongoDB dentro da pasta data do mongodb. Não feche esse prompt para manter o Mongo rodando local. Eu detalho esse e outros conceitos de MongoDB no vídeo abaixo.
Independente da opção escolhida
Agora abra outro prompt de comando (ou use alguma ferramenta de manipulação do MongoDB, como o Compass) e navegue até a pasta bin do MongoDB novamente, digitando o comando “mongo” para iniciar o client do Mongo. Se o seu MongoDB é local, o comando será apenas mongo, no entanto, se for remoto, você vai se conectar da seguinte maneira:
1 2 3 |
c:mongodbbin> ./mongo mongodb://tatooine.mongodb.umbler.com:36947/nomeBanco -u usuario -p senha |
Neste exemplo, meu servidor é tatooine.mongodb.umbler.com, a porta é 36947, meu banco se chama nomeBanco, meu usuário = usuario e minha senha = senha. Altere estas informações conforme a sua configuração!
Depois, chame o comando “use nomeBanco” para se conectar ao banco que usaremos nesse projeto (substituindo nomeBanco pelo nome do seu banco, aqui chamarei de searchengine). Deixe o prompt aberto, usaremos ele em breve para inserir alguns dados de exemplo em nosso banco do buscador.
Parte 3: Preparando os dados
Você pode ter uma base SQL com os dados consolidados do seu negócio e usar o MongoDB apenas como índice e/ou cache de busca. Ou então você pode usar apenas o MongoDB como fonte de dados. Fica à seu critério.
Caso escolha usar SQL e MongoDB, você terá de ter algum mecanismo para mandar os dados que deseja que sejam indexados pelo seu buscador para o Mongo. Este post não cobre migração de dados (mongoimport é o cara aqui), então você deve fazer por sua conta e risco usando os meios que conhecer.
Caso escolha apenas usar o Mongo, você apenas terá de alterar as suas coleções pesquisáveis para incluir um campo com o índice invertido que vamos criar na sequência, com nosso buscador de exemplo.
Em ambos os casos, a sua informação “pesquisável” deve ser armazenada de uma maneira prática de ser pesquisada, o que neste exemplo simples chamaremos de tags. Cada palavra dentro das informações pesquisáveis do seu sistema deve ser transformada em uma tag, que geralmente é um texto todo em maiúsculo (ou minúsculo) e sem acentos ou caracteres especiais.
Por exemplo, se quero tornar pesquisável os nomes dos meus clientes, que no meu SQL estão como “Luiz Júnior”, eu devo normalizá-lo para as tags “LUIZ” e “JUNIOR”, separadas. Assim, quando pesquisarem por luiz, por junior, or luiz junior e por junior luiz, este cliente será encontrado.
Assim, cada registro na sua coleção do MongoDB terá um atributo contendo as suas tags, ou informações pesquisáveis, o que facilmente fazemos com um atributo do tipo array no Mongo. Como abaixo:
1 2 3 4 5 6 7 8 |
{ "_id": ObjectId("123-abc-456-def"), "Nome": "Luiz Fernando Duarte Júnior", "Tags": ["LUIZ", "FERNANDO", "DUARTE", "JUNIOR"], ... } |
Para podermos fazer a busca depois usaremos uma query com um $in ou um $all, que são operadores do Mongo para pesquisar arrays de palavras (seus termos de busca) dentro de arrays de palavras (as tags).
Então, caso esteja migrando dados de um SQL para o Mongo, certifique-se de quebrar e normalizar as informações que deseja pesquisar dentro de um campo tags, como o acima, que será o nosso índice de pesquisa.
Para fins de exemplo, usaremos a massa de dados abaixo (apenas 2 registros) para pré-popular nosso banco com clientes (customers) que já possuem tags normalizadas como mencionado acima. Note que as tags de cada customer são um misto de seus nomes e profissões, o que você pode facilmente fazer com seus dados também.
1 2 3 4 |
custArray = [{"Nome":"Luiz Júnior", "Profissao":"Professor", "Tags":["LUIZ","JUNIOR","PROFESSOR"]},{"Nome":"Luiz Duarte", "Profissao":"Blogueiro", "tags":["LUIZ","DUARTE","BLOGUEIRO"]}] db.Customer.insert(custArray); |
O comando acima deve ser executado no console cliente do Mongo, logo após o “use searchengine”.
Obviamente existem técnicas de modelagem de banco para mecanismos de busca muito mais elaboradas que essa. Aqui estamos tratando todas as informações textualmente sem classificação do que é cada uma, sem se importar com a ordem ou peso delas, etc. Mas a partir daqui você pode fazer as suas próprias pesquisas para melhorar nosso algoritmo.
Mais pra frente, quando fizermos as nossas pesquisas, vamos fazê-las sempre buscando no campo tags, ao invés de ir nos atributos do documento. Até porque nosso buscador terá apenas um campo de busca, assim como o Google, como veremos adiante.
Mas e a performance disso?
Para resolver este problema vamos criar um índice nesse campo no MongoDB. Mas não é qualquer índice, mas sim um índice multi-valorado pois o campo tags é um array de elementos. O Mongo organiza campos multi-valorados em índices invertidos, que são exatamente um dos melhores tipos de índices básicos que podemos querer em um mecanismo de busca simples como o nosso. Eu já mencionei sobre índices invertidos no post sobre Como criar um mecanismo de busca.
1 2 3 |
db.Customer.createIndex({ "Tags": 1 }); |
O comando acima deve ser executado no console cliente do Mongo, logo após o “use searchengine”. Todos os customers inseridos a partir de então respeitarão essa regra do índice no campo tags.
Para verificar se funcionou o nosso índice, teste no console cliente do Mongo consultas como essa que traz todos os clientes que possuam a tag LUIZ (isso funciona para lógica OR também, pois recebe um array de possibilidades):
1 2 3 |
db.Customer.find({"Tags": { $in: ["LUIZ"] }}).pretty() |
Ou esse que traz todos com as tags LUIZ e JUNIOR (aqui temos lógica AND):
1 2 3 |
db.Customer.find({"Tags": { $all: ["LUIZ","JUNIOR"] }}).pretty() |
Caso você tenha gostado de trabalhar com o MongoDB e queira se aprofundar mais no assunto, recomendo o livro abaixo.
Parte 4: Configurando a Model
Adicione uma pasta Models no seu projeto se ela ainda não existir.
Dentro dela, adicione uma classe Customer.cs com o seguinte conteúdo dentro:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
using System; using System.Collections.Generic; using MongoDB.Bson; using MongoDB.Bson.Serialization.Attributes; public class Customer { [BsonId] public ObjectId Id { get; set; } public string Nome { get; set; } public string Profissao { get; set; } public List<string> Tags { get; set; } } |
Essa classe é um espelho de um documento Customer que será armazenado na coleção homônima do MongoDB. Existem diversos attributes que podemos colocar sobre as propriedades desta classe para ajudar a mapear o MongoDB corretamente, sendo que aqui usei apenas o [BsonId] que diz que aquela propriedade é o “_id” do documento, campo obrigatório de existir. Outros attributes possíveis seriam o BsonElement para dizer o nome daquela propriedade na coleção (aqui estamos usando o mesmo nome em ambos), BsonRequired para dizer que uma propriedade é obrigatória e muito mais.
Para fazer a conexão e manipulação do banco, vamos criar uma classe que vai funcionar de maneira semelhante a um DAO ou Repository Pattern. Adicione a classe DataAccess.cs na pasta Models do seu projeto, inicialmente com o seguinte conteúdo:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
public class DataAccess { MongoClient _client; IMongoDatabase _db; public DataAccess() { _client = new MongoClient("mongodb://localhost:27017"); _db = _client.GetDatabase("searchengine"); } public long CountCustomers(){ return _db.GetCollection<Customer>(typeof(Customer).Name).Count(new FilterDefinitionBuilder<Customer>().Empty); } } |
Aqui temos um construtor que faz a conexão com o servidor do Mongo. Nesta conexão, você deverá informar os seus dados de conexão, que caso seja servidor local, deve funcionar do jeito que coloquei no exemplo. Caso seja um banco remoto, você deverá ter uma connection string parecida com essa:
1 2 3 |
mongodb://usuario:senha@servidor:porta |
Já o outro método da DataAccess.cs, CountCustomers() é um método que vai na coleção Customer do MongoDB e contabiliza quantos documentos estão salvos lá (passei um filtro vazio por parâmetro), retornando este número. Usaremos este método mais tarde, para testar se nossa conexão está funcionando.
Pronto, a configuração mínima da model já está ok!
Parte 5: Criando as views
Agora vamos criar a view que vamos utilizar como pesquisa e listagem de resultados (chamada de SERP pelos especialistas: Search Engine Results Page). Vamos fazer as duas em uma, por pura preguiça deste que vos escreve. 😉
Dentro da pasta Views/Shared do seu projeto, abra _Layout.cshtml e adicione um novo item no menu superior apontando para uma página /Search, deixando-o assim (mudou apenas o segundo link):
1 2 3 4 5 6 7 8 |
<ul class="nav navbar-nav"> <li><a asp-area="" asp-controller="Home" asp-action="Index">Home</a></li> <li><a asp-area="" asp-controller="Home" asp-action="Search">Search</a></li> <li><a asp-area="" asp-controller="Home" asp-action="About">About</a></li> <li><a asp-area="" asp-controller="Home" asp-action="Contact">Contact</a></li> </ul> |
Quando este link for clicado (e você pode testar isso mandando executar o projeto), ele enviará o usuário para o endereço /Home/Search, indicando que na sua pasta Controllers deve existir um HomeController.cs com um método Search dentro. Não temos ainda este método, então vamos criá-lo no referido arquivo:
1 2 3 4 5 6 7 8 9 |
// HomeController.cs public IActionResult Search() { ViewData["Message"] = "Search page."; ViewData["Count"] = new DataAccess().CountCustomers(); return View(); } |
Esse método Search é chamado de Action no ASP.NET MVC e ele será disparado automaticamente quando a URL /Home/Search for acessada no navegador (Home=Controller, Search=Action). Nele estamos instanciando o DataAccess, contando quantos customers existem no banco e salvando essa informação na ViewData, que poderá ser acessada mais tarde na view Search.cshtml, que vamos criar agora dentro da pasta Views:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
@{ ViewData["Title"] = "Search Page"; } <div class="row" style="margin-top: 20px"> <form method="GET" action="/Home/Search"> <p><label>Pesquisa: <input type="text" name="q" /></label></p> <p><input type="submit" value="Pesquisar" class="btn btn-primary" /></p> <p>@Html.Raw(ViewData["Count"]) clientes cadastrados!</p> </form> </div> |
Aqui eu criei um formulário de pesquisa bem tosco, apenas para mostrar o conceito funcionando para você. Temos um formulário HTML que faz um GET em /Home/Search submetendo uma variável ‘q’ na querystring (com o conteúdo da pesquisa) quando o botão Pesquisar for clicado.
Logo abaixo do botão eu incluí um código Razor que imprime a quantidade de documentos que tem na base, apenas para testarmos se o ASP.NET Core está conseguindo de fato conectar e consultar o MongoDB. Mande executar e se tudo der certo, você deve ver algo semelhante à imagem abaixo:
Se você pesquisar alguma coisa, como a palavra ‘autor’, verá que não vai funcionar, mas vai aparecer na URL um ‘?q=autor’. Na sequência devemos programar o funcionamento da busca para usar essa query string aí.
Parte 6: Programando a busca
Abra novamente o seu Views/Search.cshtml e inclua o seguinte código Razor na primeira linha do arquivo, antes de tudo:
1 2 3 |
@model IEnumerable<Buscador.Models.Customer> |
Esse código diz que o modelo de dados desta página é a classe Customer, que criamos lá atrás, lembra?
Agora ainda nesta mesma Search.cshtml, adicione o seguinte código Razor no final dela, depois de tudo:
1 2 3 4 5 6 7 8 9 10 11 12 |
<hr /> @if(Model != null) { <ul> @foreach(var item in Model) { <li>@Html.DisplayFor(modelItem => item.Nome)</li> } </ul> } |
Esse código verifica se nosso Model tem algum resultado (!= null) o que significa que foi realizada uma pesquisa com sucesso. Se esse for o caso, ele faz um foreach entre todos os itens do Model imprimindo o nome de cada item em uma lista de elementos HTML.
Obviamente isso ainda não funciona pois nossa Action Search em HomeController.cs ainda não faz pesquisas, apenas conta os customers para fins de teste. Mas antes de voltar a mexer no HomeController.cs, vamos adicionar um novo método no DataAccess.cs para fazer a pesquisa no banco:
1 2 3 4 5 6 7 8 |
public IEnumerable<Customer> GetCustomers(string query) { var tags = query.ToUpper().Split(new string[] { " " }, StringSplitOptions.RemoveEmptyEntries).ToList(); var filter = Builders<Customer>.Filter.All(c => c.Tags, tags); return _db.GetCollection<Customer>(typeof(Customer).Name).Find(filter).ToList(); } |
Nesse novo método eu espero uma String que passa por uma normalização bem simples que consiste em usar apenas caixa-alta e quebrar a pesquisa por tags separadas por espaço em branco. Com essa lista de tags eu criei um Filter.All no campo Tags existente no Customer, ou seja, eu vou filtrar no banco todos os clientes que possuem TODAS (ALL) as tags informadas nesse filtro, mas não precisa ser na ordem, desde que tenha todas.
Com esse filtro pronto, é só fazer um Find na coleção Customer passando o mesmo e teremos como retorno uma lista de Customers que atendem o filtro.
Agora, para continuar, devemos voltar ao HomeController.cs e substituir o antigo método Search por esse novo, que espera um ‘q’ da query string com a possível busca. Digo possível porque caso seja o primeiro acesso à página /home/search, o ‘q’ estará vazio.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
public IActionResult Search(String q) { ViewData["Message"] = "Search page."; var da = new DataAccess(); ViewData["Count"] = da.CountCustomers(); if(!String.IsNullOrEmpty(q)) { return View(da.GetCustomers(q)); } return View(); } |
Nesta nova versão (que substitui a anterior), se vier a variável ‘q’ na query string (o mapeamento é automático) ele pesquisa os clientes que combinam com a pesquisa e retorna eles na View como sendo o Model dela (lembra do @model que adicionamos anteriormente na view Search.cshtml?). Caso contrário, se não vier o ‘q’ na URL, apenas exibe a view normalmente.
Agora se você mandar executar e fazer uma pesquisa por palavras que existam nas tags dos customers, você verá eles listados como abaixo:
Claro que você pode fazer muitas modificações nos seus resultados de pesquisa. Você pode querer jogá-los em uma página separada, com um layout mais profissional. Pode querer colocar links neles que levarão o usuário para páginas com detalhes sobre os clientes. Pode querer implementar algum tipo de autocomplete na caixa de busca, usando o Typeahead do Bootstrap. Pode implementar algum mecanismo para sinônimos, plurais, etc para tornar sua busca mais inteligente.
Há milhares de coisas que você pode fazer e eu poderia escrever um ebook sobre isso. Se tivesse tempo no momento. 😀
De qualquer maneira, acho que já consegui dar uma luz à quem nunca criou um buscador antes. Ou quem criou apenas usando LIKE % do SQL tradicional. :/
No vídeo abaixo, eu mostro como fazer a mesma coisa, mas com Node.js.
Depois que colocar ele em produção (sugestão: na Umbler), visando obter muitas visitas vindas do Google, sugiro ler esse meu artigo aqui sobre SEO para mecanismos de busca.
Um abraço, e até a próxima!
Olá, tudo bem?
O que você achou deste conteúdo? Conte nos comentários.