MongoDB: $facet + $bucket + Pesquisa de texto/geoespacial

Última atualização: 2026-08-26

Recursos avançados de agregação — Domine $facet, $bucket, pesquisa de texto e consultas geoespaciais para desenvolver recursos abrangentes de análise de dados.

Os quatro temas principais deste curso: $facet (multicanal paralelo) aborda o desafio dos “resultados multidimensionais a partir de uma única consulta”; $bucket (agrupamento de dados) aborda o desafio das “estatísticas de intervalo”; a pesquisa de texto aborda o desafio da “busca por palavra-chave”; e as consultas geoespaciais abordam o desafio da “distância e alcance”. Embora esses quatro temas possam parecer independentes, eles, juntos, formam os recursos completos de um sistema de busca de comércio eletrônico — resultados de busca + estatísticas facetadas + agrupamento por faixa de preço + lojas próximas.

1. O que você vai aprender

O valor educacional da pesquisa avançada: $facet + $bucket + $text + $geoNear compõem os recursos completos da pesquisa no MongoDB — $facet é a espinha dorsal da arquitetura de pesquisa (processamento paralelo multidimensional), $bucket é uma ferramenta para analisar a distribuição de dados (histogramas), $text é o mecanismo para recuperação por palavra-chave e $geoNear é o núcleo dos serviços de localização. Dominar esses quatro operadores significa que você pode construir um sistema de pesquisa que não dependa do Elasticsearch — o que gera uma economia significativa nos custos de infraestrutura para projetos de pequeno e médio porte.

Evolução dos sistemas de busca: Evolução dos requisitos de busca do projeto — Fase 1 (MVP): $match + $sort simples (filtragem e classificação por categoria/preço); Fase 2 (Busca por facetas): + $facet + $bucket (estatísticas multidimensionais na página de busca); Fase 3 (Busca de texto completo): + indexação de texto + $text (busca por palavra-chave); Fase 4 (LBS): + 2dsphere + $geoNear (busca por proximidade); Fase 5 (Busca Especializada): Migração para o Elasticsearch (segmentação de palavras em chinês, sinônimos e pinyin). A maioria dos projetos permanece nas Fases 3–4 e só avança para a Fase 5 quando o volume de dados ou a complexidade da busca excedem as capacidades do MongoDB.

Considerações de produção para $facet: Existem três limitações principais para o $facet em produção: 1. Limite de memória: cada subpipeline compartilha um limite de memória de 100 MB; a saída total de todas as subpipelines não pode exceder esse limite. Ao lidar com grandes conjuntos de dados, é necessário usar $match para filtrar os dados antes de aplicar $facet; 2. Não use $collation (regras de classificação) nos sub-pipelines; se for necessária a classificação em chinês, faça isso fora do $facet; 3. Evite usar $lookup nos sub-pipelines (o uso de $lookup dentro do $facet pode causar problemas catastróficos de desempenho — cada sub-pipeline executa uma consulta de junção uma vez). Prática recomendada: use $match ou $project antes de $facet para reduzir o volume de entrada; limite os sub-pipelines a estatísticas leves ($group, $bucket, $count) e coloque as operações pesadas ($lookup, $sort) fora de $facet.


100%
graph TB
    Input[Input Set<br/>1000 Document] --> Facet[$facet<br/>Parallel Execution]

    Facet --> B1[Branch 1<br/>topExpensive<br/>Sort by price (ascending) 10]
    Facet --> B2[Branch 2<br/>byCategory<br/>Categorical Statistics]
    Facet --> B3[Branch 3<br/>priceRanges<br/>Prices by Bin]
    Facet --> B4[Branch 4<br/>totalStats<br/>Overview Statistics]

    B1 --> Output[Multidimensional Results<br/>Returns one result]
    B2 --> Output
    B3 --> Output
    B4 --> Output

    style Facet fill:#d4edda
    style Output fill:#cce5ff

Modelo de Execução Paralela do $facet: O paralelismo do $facet não é uma “aceleração multithread” — o MongoDB executa cada subpipeline sequencialmente em um único thread, mas elas compartilham o mesmo instantâneo de entrada, portanto são logicamente paralelas. O verdadeiro benefício de desempenho não está na execução paralela, mas na “redução das idas e voltas na rede” — quatro dimensões estatísticas exigem apenas uma consulta, em vez de quatro. O consumo de memória do $facet é a soma de todos os subpipelines — se forem inseridos 1.000 documentos e cada um dos quatro subpipelines processar 1.000 documentos, o consumo total de memória é aproximadamente equivalente ao de 4.000 documentos.

$facet — Design da estrutura de saída: A saída do $facet é um documento em que as chaves são nomes de subpipelines e os valores são matrizes de resultados de subpipelines. Ao projetar subpipelines, tenha em mente o seguinte: 1. Cada subpipeline é independente — ela não pode fazer referência aos resultados de outras subpipelines; 2. A ordem das subpipelines não afeta os resultados (elas são executadas em paralelo, logicamente); 3. Qualquer etapa de agregação ($match, $group, $sort, $lookup etc.) pode ser usada dentro de um subpipeline; 4. Um resultado vazio retorna uma matriz vazia [] em vez de null. Ao analisar os resultados do $facet no front-end, use Object.keys() para iterar por cada dimensão.

Custo do $facet e alternativas: O custo do $facet = volume de dados de entrada × número de sub-pipelines. Quando o volume de dados de entrada é grande (>100.000 registros) e há muitos sub-pipelines (>5), o consumo de memória pode exceder os limites. Alternativas: 1. Dividir operações grandes do $facet em várias consultas de agregação independentes — sacrificando a eficiência da rede em prol da segurança da memória; 2. Antecipe $match/$project ao $facet para reduzir significativamente o volume de entrada; 3. Use visões materializadas ($merge/$out) para pré-calcular resultados estatísticos. Critérios de seleção: use $facet para pequenos volumes de dados (<10.000 registros) e visões materializadas para grandes volumes de dados.

2. Processamento paralelo multicanal $facet

Descrição do conceito: O $facet permite que vários sub-pipelines independentes sejam executados em paralelo no mesmo conjunto de documentos de entrada. Cada sub-pipeline produz seus próprios resultados, que são, por fim, combinados em um único documento. Essa é a ferramenta ideal para a criação de páginas de resultados de pesquisa (lista + estatísticas facetadas + agrupamento por faixas + totais) — uma única consulta substitui várias idas e voltas.

Como funciona: $facet aceita um objeto em que a chave é o nome do sub-pipeline e o valor é uma matriz de etapas. O MongoDB executa todos os sub-pipelines em paralelo na mesma entrada, com cada um processando os dados de forma independente. A saída é um documento em que cada chave corresponde à matriz de resultados de um sub-pipeline. Observação: Os sub-pipelines em $facet compartilham a entrada, mas não podem acessar os resultados uns dos outros, e o consumo total de memória é a soma da memória utilizada por todos os sub-pipelines.

100%
sequenceDiagram
    participant Input as Input: 1000 Products
    participant F as $facet
    participant P1 as Child pipeline1: topExpensive
    participant P2 as Child pipeline2: byCategory
    participant P3 as Child pipeline3: priceRanges
    participant P4 as Child pipeline4: stats
    
    Input->>F: Incoming Document Stream
    par Parallel Execution
        F->>P1: 1000 Documents → $sort + $limit → 10 items
        F->>P2: 1000 Documents → $group → 8 groups
        F->>P3: 1000 Documents → $bucket → 5 buckets
        F->>P4: 1000 Document → $group → 1 Overview of Articles
    end
    P1-->>F: topExpensive: [...]
    P2-->>F: byCategory: [...]
    P3-->>F: priceRanges: [...]
    P4-->>F: stats: [...]
    F-->>Input: {topExpensive, byCategory, priceRanges, stats}
$facet Subpipeline Usos típicos Combinações comuns de etapas
Lista de resultados Paginação $sort + $skip + $limit + $project
Estatísticas gerais Informações sobre paginação $count
Estatísticas por categoria Filtrar por aspecto $group + $sort
Preço por caixote Filtrar por faixa de preço $bucket / $bucketAuto
Estatísticas gerais Métricas agregadas $group({ _id: null })

Princípios de uma arquitetura de análise multidimensional: O valor central do $facet é “uma consulta, resultados multidimensionais” — as páginas de busca do comércio eletrônico precisam exibir simultaneamente o seguinte: listas de produtos (paginadas), contagem total (informações de paginação), distribuição de preços (filtros) e estatísticas de categorias (barra lateral). Sem o $facet, isso exigiria quatro consultas separadas (quatro idas e voltas na rede); com o $facet, uma única consulta retorna todas as dimensões, resultando em um aumento de quatro vezes na velocidade de renderização do front-end.

Restrições de memória para $facet: A execução paralela do $facet não é verdadeiramente paralela — o MongoDB executa os sub-pipelines na ordem em que são definidos, mas eles compartilham a mesma entrada. O consumo de memória é a soma da memória utilizada por cada sub-pipeline: 1.000 documentos × 4 sub-pipelines = a memória necessária para processar 4.000 documentos. Estratégias de mitigação: 1. Use $match antes de $facet para reduzir o volume de entrada; 2. Use $project nos sub-pipelines para otimizar os campos; 3. Limite o número de sub-pipelines (recomenda-se 3 a 5); 4. Para conjuntos de dados muito grandes, defina allowDiskUse: true.

Isolamento dos subpipelines $facet: Cada subpipeline $facet é completamente isolado — ele não pode fazer referência aos resultados de outros subpipelines, nem compartilhar cálculos intermediários. Se vários subpipelines exigirem o mesmo pré-processamento (como a extração de campos pelo $project), cada subpipeline deverá realizá-lo repetidamente. A vantagem do isolamento é o paralelismo livre de dependências; a desvantagem é o cálculo redundante. A sobrecarga causada por uma pequena quantidade de cálculos redundantes é muito menor do que o tempo de espera associado à execução serial — esse é o principal compromisso no projeto do $facet.

Padrão de projeto para sub-pipes $facet: O projeto dos sub-pipes $facet segue o princípio de “uma dimensão por pipe” — 1. Pipe da lista de resultados: $sort + $skip + $limit + $project (para exibição paginada; $limit deve ser usado para restringir o número de resultados); 2. Pipeline de contagem total: $count (retorna apenas um número; o pipeline mais leve); 3. Pipeline de estatísticas por categoria: $group + $sort + $limit (conta os resultados agrupados por campo de categoria; $limit retorna os N primeiros); 4. Pipeline de agrupamento por faixa de preço: $bucket/$bucketAuto (agrupa os dados por faixa; gera dados de histograma); 5. Pipeline de visão geral: $group({_id: null, avg: {$avg}, sum: {$sum}}) (estatísticas globais; retorna apenas um documento). Esses cinco pipelines cobrem 90% das necessidades de dados para páginas de pesquisa.

Detalhes do modelo de execução do $facet: Embora o $facet pareça ser paralelo, o MongoDB executa cada subpipeline em uma única thread — o verdadeiro paralelismo ocorre apenas em clusters fragmentados (onde cada fragmento processa seus próprios dados de forma independente, e mongos mescla os resultados). Em um ambiente de instância única ou conjunto de réplicas, os subpipelines são executados sequencialmente na ordem em que são definidos. No entanto, o $facet ainda é mais rápido do que várias consultas independentes — 1. Ele varre os dados de entrada apenas uma vez (cada subpipeline compartilha a mesma leitura de dados); 2. Requer apenas uma ida e volta na rede (cliente → MongoDB → retorno dos resultados completos); 3. Evita a sobrecarga de operações $match repetidas (o $match que precede o $facet é executado apenas uma vez).

Cenários de negócios para análise multidimensional: O cenário de negócios mais comum para $facet é uma página de busca de comércio eletrônico — quando um usuário pesquisa por “celulares”, a página precisa exibir simultaneamente o seguinte: 1. Uma lista de produtos (paginada, ordenada, com imagens e preços); 2. Número total de resultados da pesquisa (“256 produtos encontrados”); 3. Gráfico de barras mostrando a distribuição de preços (três faixas: 0–1.000, 1.000–3.000 e 3.000+); 4. Distribuição por marca (Huawei 30%, Apple 25%, Xiaomi 20%, etc.); 5. Distribuição por tamanho de tela, etc. A abordagem tradicional exigia 5 consultas × 50 ms = 250 ms, enquanto uma única consulta $facet leva aproximadamente 80 ms, transformando a experiência do usuário de um “atraso perceptível” para uma “resposta instantânea”.

$facet x Consultas Múltiplas: Um Compromisso: O $facet retorna resultados multidimensionais em uma única consulta, mas também apresenta limitações — 1. Todos os sub-pipelines do $facet devem estar dentro da mesma chamada de agregação e não podem ser carregados sob demanda (por exemplo, se um usuário visualizar apenas a lista de produtos e não as estatísticas, o pipeline de estatísticas ainda será executado); 2. A estrutura do resultado do $facet é fixa, portanto, não é possível adicionar dimensões dinamicamente (por exemplo, adicionar uma dimensão de “distribuição de cores” requer a modificação do pipeline de agregação e uma nova implantação); 3. O tratamento de erros é difícil — se qualquer subpipeline no $facet falhar, toda a agregação falha (não são possíveis retornos parciais). Abordagem alternativa: para cenários em que as dimensões podem mudar dinamicamente, coloque as dimensões de alta frequência dentro do $facet (lista de produtos + contagem total) e as dimensões de baixa frequência em APIs separadas (distribuição por marca/histograma de preços por meio de endpoints separados chamados sob demanda). Isso garante tempos de resposta rápidos para consultas de alta frequência, ao mesmo tempo em que evita que consultas de baixa frequência desperdicem recursos do $facet.

JAVASCRIPT
// === $facet Executing Multiple Independent Pipelines in Parallel ===
db.products.aggregate([
  {
    $facet: {
      // Branch 1:Top results sorted by price 10
      topExpensive: [
        { $sort: { price: -1 } },
        { $limit: 10 }
      ],
      // Branch 2:Categorical Statistics
      byCategory: [
        { $group: { _id: '$category', count: { $sum: 1 } } }
      ],
      // Branch 3:Price Range Distribution
      priceRanges: [
        {
          $bucket: {
            groupBy: '$price',
            boundaries: [0, 100, 500, 1000, 5000],
            default: '5000+',
            output: { count: { $sum: 1 } }
          }
        }
      ],
      // Branch 4:Overview Statistics
      stats: [
        { $group: { _id: null, total: { $sum: 1 }, avgPrice: { $avg: '$price' } } }
      ]
    }
  }
]);
// A single query returns results for all dimensions

Princípios matemáticos do $bucket: A matriz boundaries de $bucket define N-1 buckets, cada um dos quais é o intervalo fechado à esquerda e aberto à direita [boundaries[i], boundaries[i+1]). Os documentos são colocados no bucket correspondente com base em seu valor groupBy. Por exemplo, boundaries=[0, 100, 500, 1000] define 3 buckets: [0,100), [100,500), [500,1000). O _id de um $bucket é o valor do limite esquerdo do bucket. O $bucketAuto seleciona automaticamente os limites dos intervalos com base na distribuição dos dados para garantir que cada intervalo contenha aproximadamente o mesmo número de documentos — tornando-o adequado para análise exploratória de dados (quando a distribuição dos dados é desconhecida).

Cenários de negócios para $bucket: Cenários típicos de negócios para $bucket — 1. Estatísticas de faixas de preço: plataformas de comércio eletrônico contam o número de produtos por faixa de preço (0–100, 100–500, 500–1.000) para uso em filtros de preço nas páginas de busca; 2. Distribuição de avaliações: sistemas de avaliações agrupam as avaliações em faixas (1 estrela/2 estrelas/3 estrelas/4 estrelas/5 estrelas) para exibir gráficos de barras de avaliação; 3. Agrupamento por faixa etária: a criação de perfis de usuários agrupa os usuários por faixa etária (18–25, 25–35, 35–50, 50+), usada para marketing direcionado; 4. Agrupamento por tempo: os registros são agrupados por hora, dia ou mês, usados para gráficos de tendências. $bucket representa essencialmente uma conversão de “valores contínuos → intervalos discretos”.

Estratégia para lidar com faixas vazias: $bucket exibe faixas vazias (contagem = 0), enquanto $bucketAuto não. Isso tem implicações comerciais — o filtro de preço na página de busca precisa exibir todas as faixas de preço (incluindo as vazias); caso contrário, os usuários não saberão quais faixas estão disponíveis. Os gráficos de distribuição de avaliações também precisam exibir barras para todas as classificações por estrelas (incluindo aquelas com 0 avaliações). Portanto, use $bucket (faixas conhecidas) para cenários de filtragem por classificação/preço e $bucketAuto (que detecta automaticamente padrões de distribuição de dados) para análises exploratórias.

Ampliando a saída do $bucket: A saída do $bucket pode fazer mais do que apenas contar usando $sum: 1; ela também pode realizar outras agregações — como a classificação média para cada faixa de preço (avgRating: {$avg: '$rating'}), o preço mais alto (maxPrice: {$max: '$price'}) e uma lista dos produtos incluídos (products: {$push: '$title'}). A ampliação da saída transforma o $bucket de uma simples ferramenta de contagem em uma ferramenta multidimensional de agrupamento e estatística — retornando várias métricas estatísticas para cada intervalo em uma única consulta.

Arquitetura da página de pesquisa com $facet + $bucket: A arquitetura clássica para páginas de pesquisa em comércio eletrônico — uma única consulta $facet retorna: 1. Uma lista de resultados de pesquisa ($sort + $skip + $limit); 2. Contagem total ($count); 3. Distribuição de preços ($bucket, agrupada por faixa de preço); 4. Distribuição por categoria ($group, agrupada por categoria); 5. Distribuição de avaliações ($bucket, agrupada por avaliação). O front-end recupera todos os dados em uma única solicitação e renderiza a página de pesquisa completa — incluindo a lista de produtos, o filtro de preço, a barra lateral de categorias e o gráfico de barras de avaliações.

Considerações de desempenho para consultas $facet: os sub-pipelines do $facet são executados em paralelo, mas compartilham os dados de entrada — 1. Entrada compartilhada: o estágio $match que precede o $facet é executado apenas uma vez; portanto, todos os sub-pipelines processam o mesmo conjunto de dados de entrada; 2. Uso de memória: Cada subpipeline mantém seu próprio estado de memória; N subpipelines = N vezes o uso de memória (se o limite por estágio for 100 MB, 4 subpipelines podem consumir 400 MB); 3. Estratégia de otimização: evite operações $match redundantes dentro das subpipelines (já que a filtragem já foi realizada antes do $facet); execute apenas as operações $project/$group necessárias; 4. Abordagem alternativa: Se o $facet exceder o limite de memória, ele pode ser dividido em várias consultas de agregação independentes (à custa da atomicidade e do aumento das idas e voltas na rede). Em ambientes de produção, o $facet é adequado para conjuntos de dados de pequeno a médio porte (< 100.000 documentos correspondentes); para conjuntos de dados grandes, o uso de memória deve ser avaliado.

3. $bucket: Segmentação de dados por buckets

O princípio estatístico por trás do agrupamento em intervalos: Um $bucket é, essencialmente, uma estrutura de dados do tipo histograma — ele divide um intervalo contínuo de valores em intervalos discretos e conta o número de documentos em cada intervalo. Um histograma é o primeiro passo na exploração de dados: ao observar a distribuição dos valores, é possível determinar se os dados seguem uma distribuição normal, se há valores atípicos e se é necessária uma normalização. O $bucket é adequado para intervalos conhecidos (como faixas de preço 0–100, 100–500 e 500–1000), enquanto o $bucketAuto é adequado para análises exploratórias (selecionando automaticamente buckets uniformes).

$bucket x $switch: Uma comparação entre os métodos de agrupamento: Tanto o $bucket quanto o $switch podem ser usados para classificação por intervalos, mas têm objetivos de projeto diferentes — o $bucket é uma ferramenta estatística (que gera contagens e valores agregados para cada grupo), enquanto o $switch é uma ferramenta de classificação (que gera rótulos de classificação para cada documento). Seleção: Se você precisar de um histograma ou de estatísticas → use $bucket; se precisar atribuir um rótulo a cada documento → use $switch + $addFields.

O algoritmo de agrupamento $bucketAuto: O $bucketAuto utiliza um algoritmo de agrupamento com profundidade aproximadamente igual — o objetivo é que cada intervalo contenha aproximadamente o mesmo número de documentos, em vez de um agrupamento com largura igual (em que cada intervalo tem o mesmo intervalo de valores). Por exemplo, se os preços dos produtos estiverem concentrados no intervalo de 50 a 200, o $bucketAuto criará mais buckets no intervalo de 50 a 200 (onde a densidade de dados é alta) e menos buckets no intervalo de 500 a 5.000 (onde os dados são esparsos). Isso fornece mais informações do que a divisão em intervalos de largura igual — que cria intervalos vazios em áreas esparsas dos dados e amontoa grandes quantidades de dados em um único intervalo nas áreas densas. A opção granularities em $bucketAuto controla o número de intervalos (por exemplo, 5, 10 ou 20 intervalos).

100%
graph LR
    A["price: 599"] --> B{Which bucket??}
    C["price: 29"] --> B
    D["price: 1299"] --> B
    E["price: 7500"] --> B
    
    B --> F["[0, 100): price=29 ✅"]
    B --> G["[100, 500): None"]
    B --> H["[500, 1000): price=599 ✅"]
    B --> I["[1000, 5000): price=1299 ✅"]
    B --> J["default 'Other': price=7500 ✅"]
    
    style F fill:#d4edda
    style H fill:#d4edda
    style I fill:#d4edda
    style J fill:#fff3cd
Dimensão de comparação $bucket $bucketAuto
Limite do intervalo Especificado manualmente Calculado automaticamente
Número de compartimentos Determinado pelo número de limites Especificado buckets: N
Tamanho do balde Pode variar O mais uniforme possível
Casos de uso Intervalos conhecidos (faixas de preço) Exploração da distribuição dos dados
Manuseio de barris vazios Mostrar barris vazios Não mostrar barris vazios

Extensões de saída do $bucket: O parâmetro output de $bucket permite realizar cálculos de agregação dentro de cada bucket — não se limitando a uma simples contagem $sum: 1, mas incluindo também $avg (preço médio), $push (coleta de campos específicos dos documentos no bucket) e $max/$min (valores máximo e mínimo no bucket). Uso típico: $bucket + output: {count: {$sum: 1}, avgPrice: {$avg: '$price'}, topProduct: {$max: '$price'}} retorna a contagem, o preço médio e o item mais caro dentro de um único bucket. Isso faz com que o $bucket não seja apenas uma ferramenta de “contagem por bucket”, mas também uma ferramenta de “análise estatística por bucket”.

Algoritmo e limitações do $bucketAuto: O $bucketAuto utiliza um algoritmo de agrupamento por faixas com profundidade aproximadamente igual — seu objetivo é garantir que cada faixa contenha o mesmo número de documentos, em vez de intervalos com a mesma largura. Isso significa que a faixa de preço de 0 a 100 pode conter 500 documentos (já que há mais itens baratos), enquanto a faixa de 5.000 a 10.000 pode conter apenas 50 documentos (já que há menos itens caros). Limitações: 1. Os limites dos buckets são incontroláveis (calculados automaticamente; intervalos específicos do negócio, como “0–100” ou “100–500”, não podem ser especificados); 2. Baixo desempenho com distorção de dados (por exemplo, 99% dos produtos têm preços entre 0 e 1.000, enquanto 1% custa 10.000 ou mais; a divisão automática em faixas criaria muitas faixas pequenas dentro do intervalo de 0 a 1.000); 3. Não é adequado para apresentação a usuários de negócios (os limites dos intervalos são valores arbitrários, como 47,5–89,3, em vez de intervalos intuitivos como “0–100” ou “100–500”). $bucketAuto é adequado para análise exploratória de dados, enquanto $bucket é adequado para apresentar intervalos fixos a usuários de negócios.

JAVASCRIPT
// === $bucket Custom Bucketing ===
db.products.aggregate([
  {
    $bucket: {
      groupBy: '$price',
      boundaries: [0, 100, 500, 1000, 5000, 10000],  // Barrel Boundary
      default: 'Other',                                // Out of range
      output: {
        count: { $sum: 1 },
        products: { $push: { sku: '$sku', title: '$title', price: '$price' } }
      }
    }
  }
]);
// [
//   { _id: 0, count: 120, products: [...] },
//   { _id: 100, count: 80, products: [...] },
//   ...
// ]

// === $bucketAuto Automatic Bin Sorting ===
db.products.aggregate([
  {
    $bucketAuto: {
      groupBy: '$price',
      buckets: 5  // Automatic Sorting 5 a bucket
    }
  }
]);

Escolha de uma arquitetura de pesquisa: A pesquisa de texto no MongoDB versus o Elasticsearch é uma decisão arquitetônica clássica. Vantagens da pesquisa de texto no MongoDB: 1. Não requer infraestrutura adicional (os dados já estão no MongoDB); 2. Compatível com operações CRUD (mesma linguagem de consulta); 3. Desempenho suficiente para conjuntos de dados pequenos (<1 milhão de registros). Vantagens do Elasticsearch: 1. Segmentação de palavras em chinês (usando segmentadores como o IK Analyzer); 2. Correspondência aproximada e sinônimos; 3. Destaque; 4. Suporte para volumes de dados na casa das dezenas de bilhões. Princípio de seleção: use o MongoDB para buscas simples e o Elasticsearch para buscas complexas. Você pode começar com a busca de texto no MongoDB e migrar para o Elasticsearch assim que seus requisitos de busca se tornarem mais complexos.

O processo de construção de um índice invertido: Ao criar um índice de texto, o MongoDB realiza uma análise lexical nos campos de texto de cada documento — 1. Tokenização (divisão por espaços/sinais de pontuação); 2. Extração de radicais (por exemplo, “running” → “run”, plural → singular); 3. Remoção de palavras irrelevantes (palavras de alta frequência e sem significado, como “o”, “um”, “uma”, etc.); 4. Criação de um índice invertido (palavra → lista de IDs de documentos + frequência do termo). Durante uma consulta, os termos na cláusula $search também passam por tokenização e redução a raiz, e os documentos correspondentes são então recuperados do índice invertido. Observação: o texto CJK (chinês/japonês/coreano) é dividido por caracteres individuais, em vez de palavras (sem tokenização baseada em espaços), o que gera resultados insatisfatórios — por exemplo, uma palavra CJK composta por vários caracteres seria dividida em entradas individuais de um único caractere, em vez de ser reconhecida como uma única unidade semântica.

Pontuação de Relevância BM25: A Pesquisa de Texto do MongoDB utiliza o algoritmo BM25 para calcular as pontuações de relevância, levando em consideração três fatores: 1. Frequência do Termo (TF): quanto mais vezes um termo aparece em um documento, maior é a pontuação; 2. Frequência Inversa do Documento (IDF): quanto menos frequente for a ocorrência de um termo em todos os documentos (quanto mais raro ele for), maior será a pontuação; 3. Normalização do comprimento do documento: uma correspondência em um documento curto é considerada mais valiosa do que uma correspondência em um documento longo. O parâmetro weights na indexação de texto ponderada afeta o cálculo do BM25 — campos com pesos maiores contribuem mais para a pontuação quando uma correspondência é encontrada.

4. Pesquisa de texto

Princípios do sistema de pesquisa: A pesquisa de texto no MongoDB baseia-se em índices invertidos — durante a criação do índice, o texto é tokenizado e submetido a stemming para construir uma tabela de mapeamento “palavra → documento”. Durante uma consulta, o operador $search é usado para especificar palavras-chave, e os documentos correspondentes são pesquisados no índice invertido, com pontuações de relevância calculadas por meio do algoritmo TF-IDF. Limitações: 1. Não suporta tokenização em chinês (divisão por caracteres em vez de tokenização semântica); 2. Não suporta correspondência aproximada (requer a consulta aproximada do Elasticsearch); 3. Não suporta expansão de sinônimos; 4. Apenas um índice de texto por coleção.

Quando usar a Pesquisa de Texto do MongoDB em comparação com o Elasticsearch: A Pesquisa de Texto do MongoDB é adequada para cenários simples — pesquisas exatas ou por frase em conteúdo em inglês e pesquisa de texto completo para pequenos conjuntos de dados. O Elasticsearch é adequado para cenários complexos — segmentação de palavras em chinês, pesquisa aproximada, sinônimos, destaque, pesquisa por pinyin e ponderação em vários campos. Critérios de decisão: 1. Volume de dados < 100.000 e apenas pesquisa em inglês é necessária → o MongoDB é suficiente; 2. É necessária segmentação de palavras em chinês ou pesquisa aproximada → o Elasticsearch é obrigatório; 3. A pesquisa é um recurso essencial → Elasticsearch; 4. A pesquisa é um recurso secundário → o MongoDB oferece uma implementação simples.

A terceira opção para o MongoDB Atlas Search: O Atlas Search é o mecanismo de busca integrado ao serviço em nuvem MongoDB Atlas — baseado no Apache Lucene (o mesmo mecanismo subjacente do Elasticsearch) — que oferece recursos avançados, como segmentação de palavras em chinês, busca aproximada, sinônimos e destaque, além de se integrar perfeitamente ao MongoDB (sem necessidade de sincronizar dados com um cluster externo). Vantagens: 1. Latência zero na sincronização de dados (os índices do Lucene são sincronizados com as coleções do MongoDB em tempo real); 2. Uso direto na etapa $search dos pipelines de agregação (não é necessário aprender uma nova sintaxe de consulta); 3. Carga operacional zero (hospedado pelo Atlas, não é necessário gerenciar um cluster do Elasticsearch). Limitações: 1. Disponível apenas no serviço em nuvem do Atlas (não é compatível com MongoDB auto-hospedado); 2. Aplicam-se custos adicionais; 3. Os recursos não são tão abrangentes quanto os do Elasticsearch (por exemplo, não há buckets de agregação nem tipos aninhados). Para projetos de pequeno e médio porte, o Atlas Search oferece o melhor equilíbrio — ele elimina a necessidade de manter um cluster do Elasticsearch, ao mesmo tempo em que fornece recursos de pesquisa de nível profissional.

Resumo das decisões sobre a seleção do sistema de busca: Escolha entre três soluções de busca — 1. $text nativo do MongoDB: custo zero, manutenção zero, funcionalidade limitada (busca em inglês, sem segmentação de palavras, sem correspondência aproximada), adequado para projetos pequenos em que a busca é um recurso secundário; 2. Atlas Search: baixa manutenção, custo moderado, funcionalidade robusta (segmentação de palavras em chinês, correspondência aproximada, destaque), adequado para projetos de médio porte de usuários do Atlas; 3. Elasticsearch: alta sobrecarga operacional, alto custo, recursos mais robustos (todos os recursos de pesquisa + análises do Kibana), adequado para projetos de grande escala em que a pesquisa é um recurso central. Critérios de seleção: volume de dados (pequeno → MongoDB, grande → ES), idioma (apenas inglês → MongoDB, chinês → ES/Atlas), importância da pesquisa (secundária → MongoDB, essencial → ES), capacidades operacionais (limitadas → Atlas, robustas → ES).

100%
graph TD
    A[Document: title='Smartphone X 5G'<br/>description='Latest 5G phone'] --> B[Text Index<br/>Lexical Analysis]
    B --> C[Inverted Index<br/>smartphone → doc1<br/>phone → doc1<br/>5g → doc1<br/>latest → doc1]
    
    D["$search: 'phone'"] --> E[Searching the Inverted Index<br/>phone → doc1]
    E --> F[✅ Hit]
    
    G["$search: 'phone -cheap'"] --> H[phone → doc1<br/>Including the exclusion of cheap]
    H --> F
    
    style C fill:#d4edda
    style F fill:#d4edda
Tipo de pesquisa Sintaxe Descrição
Pesquisa por palavra $search: 'phone smartphone' Correspondência com qualquer palavra (OU)
Pesquisa por frase $search: '"smartphone 5g"' Deve ser uma frase completa
Excluir da pesquisa $search: 'phone -cheap' Incluir “telefone”, mas excluir “barato”
Ordenar por relevância score: { $meta: 'textScore' } Ordenar por relevância

(1) Criar um índice de texto

Estratégia de projeto de índices de texto: Cada coleção pode ter, no máximo, um índice de texto, mas ele pode abranger vários campos e atribuir pesos — campos com pesos mais altos recebem um textScore mais alto quando correspondidos, resultando em classificações mais razoáveis dos resultados de pesquisa. Considerações de projeto: 1. O título deve receber um peso maior do que a descrição (as correspondências no título são mais importantes para os usuários durante as pesquisas); 2. Não crie índices de texto para campos com baixo conteúdo informativo (por exemplo, status, category); 3. Os índices de texto consomem um espaço de armazenamento significativo (aproximadamente 2 a 3% do volume de dados), portanto, use-os com cautela em coleções grandes; 4. Índices compostos e índices de texto não podem coexistir na mesma consulta.

JAVASCRIPT
// === Create a Text Index(Single field)===
db.products.createIndex({ title: 'text' });

// === Multi-field text index ===
db.products.createIndex({
  title: 'text',
  description: 'text',
  tags: 'text'
});

// === Weighted Text Index ===
db.products.createIndex(
  {
    title: 'text',
    description: 'text'
  },
  {
    weights: {
      title: 10,        // title High weighting
      description: 1
    },
    name: 'TextIndex'
  }
);

(2) Pesquisa de $text

Quatro modos de pesquisa para $text: $text suporta quatro sintaxes de pesquisa — 1. Pesquisa por palavra (semântica OR padrão): 'phone smartphone' encontra documentos que contenham qualquer uma das palavras; 2. Pesquisa por frase (aspas duplas): '"smartphone 5g"' deve conter a frase exata; 3. Pesquisa de exclusão (sinal de menos): 'phone -cheap' contém “phone”, mas não “cheap”; 4. Classificação por relevância: use $meta: 'textScore' para recuperar pontuações e classificar os resultados. Observação: a pesquisa do $text não suporta curingas (*), correspondência aproximada (~) nem expressões regulares.

Custos de criação e manutenção de índices de texto: O custo de criação de índices de texto é maior do que o de índices comuns — 1. Tamanho do índice: os índices de texto criam índices invertidos após a tokenização de cada campo; o tamanho do índice pode chegar a 30–50% dos dados originais (em comparação com cerca de 10% para índices comuns); 2. Tempo de criação: a criação de um índice de texto para grandes conjuntos de dados (> 1 milhão de documentos) pode levar de alguns minutos a várias horas e deve ser realizada fora dos horários de pico; 3. Desempenho de gravação: toda vez que um documento contendo campos de texto é inserido ou atualizado, o índice invertido precisa ser atualizado, aumentando a latência de gravação em 10–30%; 4. Limitações: cada coleção pode ter apenas um índice de texto (embora ele possa abranger vários campos); os índices de texto não suportam pesquisas de texto em consultas $or. Esses custos significam que você deve criar índices de texto apenas para campos que realmente exijam pesquisa de texto completo e evitar criá-los para todos os campos de string.

JAVASCRIPT
// === Basic Search ===
db.products.find({ $text: { $search: 'phone smartphone' } });
// Match documents where title or description contains "phone" or "smartphone"

// === Phrase Search ===
db.products.find({ $text: { $search: '"smartphone 5g"' } });
// Must include the complete phrase

// === Exclude from Search ===
db.products.find({ $text: { $search: 'phone -cheap' } });
// Includes phone But does not include cheap

// === Sort by Relevance ===
db.products.find(
  { $text: { $search: 'phone' } },
  { score: { $meta: 'textScore' } }
).sort({ score: { $meta: 'textScore' } });

Otimização do desempenho de consultas geoespaciais: os índices do 2dsphere são fundamentais para o desempenho das consultas geoespaciais — sem eles, as funções $geoNear e $nearSphere realizarão uma varredura completa da coleção. Dicas de otimização: 1. A função $geoNear deve ser a primeira etapa do pipeline (caso contrário, o índice não poderá ser utilizado); 2. Quanto menor for o maxDistance, menor será o intervalo de varredura e melhor será o desempenho; 3. Os índices compostos {location: '2dsphere', category: 1} suportam consultas do tipo “lojas de uma determinada categoria nas proximidades”; 4. $geoWithin é mais rápido que $geoNear (sem classificação) e deve ser priorizado para consultas de intervalo puro.

Alternativas para pesquisa em chinês: A indexação de texto do MongoDB oferece suporte limitado ao chinês — ela divide o texto por caracteres, em vez de palavras, o que faz com que uma “palavra CJK de vários caracteres” seja dividida em quatro entradas de um único caractere; portanto, uma pesquisa por um termo de vários caracteres não encontrará a palavra composta completa. Três alternativas: 1. Pesquisa por expressão regular $regex: /term/ — simples, mas não permite classificar os resultados e não utiliza o índice; 2. Segmentação de palavras no nível do aplicativo — pré-processe os documentos usando um segmentador como o jieba durante a inserção, armazene os resultados segmentados em um campo de matriz e faça consultas usando índices com várias chaves; 3. Elasticsearch — um mecanismo de busca profissional com um segmentador de chinês integrado (IK Analyzer) que oferece suporte à busca por pinyin e à expansão de sinônimos. A opção 3 é a escolha preferida para ambientes de produção.

Abordagens práticas para a segmentação de palavras em chinês: O principal desafio da pesquisa em chinês é que as “palavras” não possuem delimitadores naturais — 1. Abordagem de segmentação jieba: use o nodejieba do Node.js para segmentação ao inserir documentos, armazene os resultados no campo segmentedTitle: ['term1', 'term2', 'compound'], crie um índice com várias chaves e realize consultas segmentando os termos de pesquisa e usando $all para a correspondência; 2. Abordagem N-gram: Use $regex com correspondência de n-gram (por exemplo, /term1.{0,2}term2/). Isso suporta correspondência aproximada, mas apresenta baixo desempenho (varredura completa do conjunto); 3. Pesquisa por pinyin: Armazene um campo adicional pinyinTitle (convertido usando a biblioteca pinyin) para permitir a pesquisa de produtos em chinês por meio da entrada em pinyin; 4. Solução com Elasticsearch: Crie um pipeline de sincronização MongoDB → Elasticsearch (usando Change Stream ou MongoSync). As consultas são executadas no ES, enquanto os dados permanecem armazenados no MongoDB. A Solução 1 é adequada para projetos de pequeno e médio porte (custo zero), enquanto a Solução 4 é adequada para projetos de grande porte (experiência profissional em pesquisa).

Estrutura interna do índice 2dsphere: O índice 2dsphere codifica coordenadas geográficas como Geohash — uma divisão recursiva da superfície terrestre em uma grade, na qual cada célula da grade é representada por uma sequência de caracteres. Durante uma consulta, $geoNear primeiro identifica as células da grade próximas com base no Geohash do ponto central e, em seguida, calcula a distância esférica com precisão. A natureza de correspondência de prefixos do Geohash permite que consultas de intervalo façam uso eficiente do índice — ao consultar “dentro de 5 km”, apenas algumas grades adjacentes precisam ser verificadas, em vez de todo o conjunto de dados.

Precisão dos cálculos de distância: O MongoDB utiliza geometria esférica (elipsóide WGS84) para calcular distâncias — o valor distanceField retornado por $geoNear é expresso em metros, com uma precisão de aproximadamente 0,5 metros. O raio em $geoWithin e $centerSphere é expresso em radianos — para converter quilômetros em radianos, use km / 6378.1. Observação: 6378,1 é o raio equatorial da Terra (km). Usar essa conversão em regiões de alta latitude pode resultar em um pequeno erro (o raio polar é de 6356,8 km), mas o impacto em aplicativos de LBS é insignificante.

Arquitetura integrada para pesquisa + LBS: Os sistemas de pesquisa de comércio eletrônico frequentemente precisam oferecer suporte tanto à “pesquisa por palavra-chave” quanto à “pesquisa por proximidade” simultaneamente — como, por exemplo, “celulares 5G em um raio de 3 km”. Abordagens de integração para esses dois tipos de pesquisa: 1. Realizar primeiro uma pesquisa $text e, em seguida, filtrar por distância usando $geoNear (adequado para cenários com poucos resultados de pesquisa, nos quais a filtragem por proximidade é rápida); 2. Realizar primeiro uma pesquisa de proximidade com $geoNear, seguida por uma pesquisa por palavra-chave com $match (adequado para cenários com poucos resultados próximos, nos quais a filtragem por palavra-chave é rápida); 3. Executar ambas as pesquisas em paralelo usando $facet e, em seguida, mesclar os resultados (a abordagem mais flexível, mas que consome muita memória). A abordagem 1 é a mais comum — os usuários veem primeiro os produtos relevantes e, em seguida, os classificam por distância.

5. Consultas geoespaciais

Princípios da indexação geoespacial: O índice 2dsphere codifica coordenadas esféricas (longitude/latitude) como Geohashes — um sistema que divide recursivamente a superfície da Terra em grades, cada uma codificada por uma sequência de caracteres. Para consultas de proximidade, o sistema primeiro identifica as grades com o mesmo prefixo de Geohash (filtragem grosseira) e, em seguida, calcula com precisão a distância esférica (filtragem fina). Essa estratégia em duas etapas reduz a complexidade das consultas $geoNear e $nearSphere de O(N) para O(log N).

Seleção de operadores de consulta geoespacial: Cada um dos três operadores de consulta tem seu próprio caso de uso — o $geoNear é usado durante a fase de agregação, gera campos de distância e suporta classificação e filtragem; é mais adequado para cenários de “busca por proximidade + classificação de resultados”; o $nearSphere é um operador de consulta com sintaxe simples, mas não gera valores de distância; é adequado para determinar se um resultado está “dentro de um intervalo”; o $geoWithin não classifica os resultados e é adequado para consultas em lote destinadas a recuperar “todos os resultados dentro de uma região” (como áreas de entrega).

Operador Distância de saída Classificação Compatibilidade com agregação Casos de uso
$geoNear Sim Pesquisa LBS
$nearSphere Não Verificação de distância
$geoWithin Sim Agrupar por região

Restrições específicas para $geoNear: $geoNear deve ser a primeira etapa no pipeline de agregação — essa é uma restrição rígida, pois $geoNear precisa percorrer o índice 2dsphere a partir da raiz. Se for necessário aplicar primeiro a filtragem $match, é preciso especificar isso na opção de consulta $geoNear, em vez de colocar uma etapa $match antes de $geoNear. O distanceField do $geoNear retorna a distância esférica (em metros), e o distanceMultiplier pode ser usado para converter unidades (por exemplo, × 0,001 para converter para quilômetros). O includeLocs: true retorna os pontos de coordenadas correspondentes (útil para documentos com vários locais — como uma empresa com várias filiais).

Modelo arquitetônico de um sistema LBS: Um sistema LBS (Serviço Baseado em Localização) completo consiste em três camadas: 1. Camada de dados: índice 2dsphere + armazenamento de coordenadas geográficas; 2. Camada de consulta: $geoNear/$geoWithin para buscas nas proximidades e consultas por área; 3. Camada de aplicação: classificação por distância + paginação + armazenamento em cache dos resultados. Fluxo típico de negócios: O usuário abre “Cafés nas proximidades” → Recupera a localização do usuário → Pesquisa $geoNear em um raio de 3 km → Classifica por distância → Retorna os 20 primeiros resultados. Principais fatores de desempenho: Índice 2dsphere + limite maxDistance + controle do número de resultados retornados.

Estratégia de armazenamento em cache para LBS: O armazenamento em cache para consultas LBS é mais complexo do que para consultas comuns — pois as localizações são contínuas e os resultados de pesquisa para locais adjacentes se sobrepõem significativamente. Estratégia de armazenamento em cache: 1. Armazenamento em cache por geogrelha (dividir o mapa em grelhas de 1 km × 1 km, armazenar em cache os resultados de pesquisa para cada grelha e retornar os resultados armazenados em cache para consultas dentro da mesma grelha); 2. Armazenamento em cache baseado no usuário (armazena em cache os resultados para “locais que o usuário pesquisou recentemente”, com um TTL de 5 minutos); 3. Armazenamento em cache de áreas populares (pré-calcula os resultados para áreas de pesquisa de alta frequência, como centros urbanos, enquanto realiza consultas em tempo real para áreas menos populares). Observação: expiração do cache — quando as informações da loja são alteradas, o cache da grade afetada deve ser esvaziado.

Escolhendo entre índices 2d e 2dsphere: O MongoDB oferece dois tipos de índices geoespaciais — 2d (coordenadas planas, adequadas para mapas planos e cenários de jogos, armazenadas usando pares de coordenadas legados [x, y]) e 2dsphere (coordenadas esféricas, adequadas para coordenadas reais da Terra, armazenadas usando GeoJSON Point {type: 'Point', coordinates: [lng, lat]}). 99% dos cenários de LBS devem usar o 2dsphere — 1. Suporta cálculos de distância esférica reais (o 2d usa distância euclidiana, o que resulta em erros significativos em altas latitudes); 2. Suporta todos os operadores geoespaciais, como $geoNear, $geoWithin e $near (o 2d suporta apenas funcionalidades parciais do $near); 3. Suporta várias formas GeoJSON (Point/LineString/Polygon). O 2d deve ser usado apenas para cenários puramente planos, como mapas de jogos.

(1) Criar um índice 2dsphere

JAVASCRIPT
// === Add a Location Field ===
db.stores.insertOne({
  name: 'Tokyo Store',
  location: {
    type: 'Point',
    coordinates: [139.6917, 35.6895]  // [longitude, latitude]
  }
});

// === Create 2dsphere Index ===
db.stores.createIndex({ location: '2dsphere' });

(2) Consultas geográficas

Pontos-chave sobre o formato de coordenadas GeoJSON: As consultas geoespaciais do MongoDB utilizam o formato GeoJSON — tipo: 'Point' + coordenadas: [lng, lat]. Dois erros comuns: 1. Ordem invertida de longitude e latitude (o Google Maps usa lat/lng, enquanto o MongoDB usa lng/lat; preste atenção a isso); 2. Valores de coordenadas fora do intervalo permitido (longitude de -180 a 180, latitude de -90 a 90). A unidade de distância padrão para $geoNear é o metro (quando spherical: true); maxDistance: 5000 indica um intervalo de 5 quilômetros.

Conversão de $centerSphere para radianos: As unidades de raio para $geoWithin e $centerSphere são radianos — 1 radiano ≈ 6.378,1 quilômetros (raio equatorial da Terra). Fórmula de conversão: radianos = quilômetros / 6.378,1. Exemplo: 5 quilômetros = 5/6.378,1 ≈ 0,000784 radianos. Essa conversão está sujeita a erros, por isso recomenda-se encapsulá-la como uma função utilitária: kmToRadians(km) { return km / 6.378,1; }.

JAVASCRIPT
// === $geoNear Find Nearby ===
db.stores.aggregate([
  {
    $geoNear: {
      near: { type: 'Point', coordinates: [139.6917, 35.6895] },  // Tokyo Coordinates
      distanceField: 'distance',     // Output Field
      maxDistance: 5000,             // Maximum Distance 5km (meters)
      spherical: true
    }
  }
]);

// === $geoWithin + $centerSphere Range Query ===
db.stores.find({
  location: {
    $geoWithin: {
      $centerSphere: [
        [139.6917, 35.6895],         // Center Point
        5 / 6378.1                   // 5km Radius (radians)
      ]
    }
  }
});

// === $nearSphere Simple Query ===
db.stores.find({
  location: {
    $nearSphere: {
      $geometry: { type: 'Point', coordinates: [139.6917, 35.6895] },
      $maxDistance: 5000
    }
  }
});

Características de desempenho do $geoWithin: O $geoWithin não classifica nem calcula distâncias — ele apenas determina se um ponto está dentro de uma região, por isso é muito mais rápido que o $geoNear. O $geoWithin é adequado para consultas estatísticas do tipo “quantos estão dentro de uma região” (por exemplo, “lojas dentro da área de entrega”), enquanto o $geoNear é adequado para consultas ordenadas do tipo “os mais próximos” (por exemplo, “as 5 cafeterias mais próximas”). O $geoWithin suporta três tipos de formas de área: $centerSphere (círculo), $polygon (polígono) e $box (retângulo).

Escolhendo entre $geoWithin e $geoNear: A escolha do operador depende dos requisitos de negócios — 1. Se for necessário obter “os N resultados mais próximos” → $geoNear (classificados por distância + limite); 2. Se for necessário obter “todos os resultados dentro de uma área” → $geoWithin (sem classificação, melhor desempenho); 3. Se for necessário obter valores de distância → $geoNear (retorna o campo de distância); 4. Necessidade de combinar com $facet → $geoWithin ($geoNear não pode ser usado dentro de $facet, pois deve ser a primeira etapa no pipeline); 5. Consultas de área de polígono → $geoWithin + $polygon ($geoNear suporta apenas áreas circulares). Use $geoWithin para áreas de entrega (polígonos irregulares) e $geoNear para pesquisas nas proximidades (áreas circulares).

Estratégia de armazenamento em cache para o sistema LBS: Os resultados das consultas do LBS podem ser armazenados em cache — os resultados de pesquisa para um usuário na mesma localização permanecem inalterados por um curto período de tempo — 1. Projeto da chave de cache: lbs:{lat}:{lng}:{radius}:{category}:{keyword}, codificar todos os parâmetros de consulta na chave de cache; 2. Granularidade do cache: a latitude e a longitude são mantidas com 3 casas decimais (precisão de aproximadamente 110 metros); usuários dentro da mesma grade compartilham o cache; 3. Configuração de TTL: 3 a 5 minutos (os locais das lojas permanecem inalterados no curto prazo, mas lojas recém-inauguradas precisam ser exibidas imediatamente); 4. Pré-carregamento do cache: os resultados de pesquisa para distritos comerciais populares (como Sanlitun, em Pequim, e a Rua Nanjing, em Xangai) são armazenados em cache antecipadamente; 5. Estratégia de expiração: quando novas lojas entram em operação, o cache da área correspondente é limpo. O armazenamento em cache de LBS pode reduzir as consultas ao banco de dados em mais de 80% e é o principal método para otimizar o desempenho do sistema LBS.

Referência rápida para conversão em radianos: Os parâmetros de distância para $centerSphere e $nearSphere utilizam radianos — km para radianos = km / 6378,1; milhas para radianos = milhas / 3963,2. Conversões comuns: 1 km ≈ 0,00015696 radianos, 5 km ≈ 0,0007848 radianos, 10 km ≈ 0,00157 radianos. O parâmetro maxDistance para $geoNear usa metros (não é necessária conversão), o que é uma das razões pelas quais $geoNear é mais fácil de usar do que $nearSphere.

Precisão dos cálculos de distância em sistemas LBS: As consultas geoespaciais do MongoDB utilizam geometria esférica (aproximada pelo elipsóide WGS84) por padrão — a precisão é maior próximo ao equador (erro < 0,5%) e, embora seja ligeiramente menor em altas latitudes, ainda é muito superior aos cálculos que utilizam geometria plana. A unidade de distância para índices 2dsphere é o metro, e a saída distanceField de $geoNear também é expressa em metros. Considerações comuns sobre precisão: 1. A precisão é suficiente para distâncias curtas (< 100 m) (erro < 1 m); 2. Para longas distâncias (> 1.000 km), o erro pode chegar a dezenas de metros (devido ao erro de aproximação esférica); 3. A precisão é menor para consultas entre polos opostos (embora tais cenários sejam extremamente raros). Para 99% das aplicações de LBS (localização de restaurantes ou lojas próximas), a precisão de distância do MongoDB atende plenamente aos requisitos.


6. Treinamento prático abrangente

Visão geral do conceito: Este exercício prático e abrangente combina $facet, $bucket, pesquisa de texto e consultas geoespaciais para construir um sistema completo de pesquisa para comércio eletrônico. Uma única consulta $facet retorna simultaneamente a lista de resultados da pesquisa, a contagem total, as faixas de preço e as estatísticas por categoria, permitindo que o front-end exiba a página de pesquisa diretamente. As consultas geoespaciais oferecem suporte a cenários de LBS (Serviço Baseado em Localização).

Como funciona: A consulta $facet da API de Pesquisa recebe $match (pesquisa de texto + filtragem por categoria) como entrada comum, e quatro subpipelines a processam em paralelo: itens (lista paginada), total (contagem total), facetas (faixas de preço) e categorias (estatísticas de categorias). As consultas geoespaciais são executadas de forma independente e retornam resultados classificados por distância.

Pontos-chave para o projeto da arquitetura da API de pesquisa: O principal desafio de uma API de pesquisa abrangente é “retornar dados em todas as dimensões em uma única solicitação” — o front-end precisa de uma lista (para exibir os resultados da pesquisa), uma contagem total (para paginação), agrupamento por faixas (para filtros de preço/avaliação) e categorização (para a navegação na barra lateral). O $facet permite que essas quatro dimensões sejam calculadas em paralelo a partir de uma única entrada, evitando quatro consultas separadas. No entanto, observe que o $match que precede o $facet deve satisfazer tanto a pesquisa de texto quanto os filtros de negócios (como categorias e faixas de preço); caso contrário, as entradas vistas por cada subpipeline serão inconsistentes.

Estratégias de classificação dos resultados de pesquisa: A classificação dos resultados de pesquisa afeta diretamente a experiência do usuário — as estratégias de classificação devem ser selecionadas com base no contexto: 1. Classificação por relevância ($meta: 'textScore'): Quando os usuários pesquisam palavras-chave, esperam que os resultados mais relevantes apareçam no topo; 2. Classificação por distância ($geoNear): Ao pesquisar lojas próximas, os usuários esperam que as mais próximas apareçam no topo; 3. Classificação por preço: ao comparar preços, os usuários esperam que os preços mais baixos ou mais altos apareçam no topo; 4. Classificação por volume de vendas: ao fazer uma compra, os usuários esperam que os itens mais populares apareçam no topo; 5. Classificação abrangente (fórmula ponderada): $addFields calcula uma pontuação composta = 0,4 × Relevância + 0,3 × Volume de vendas + 0,2 × Avaliação + 0,1 × Atualização. A classificação abrangente é a solução definitiva para a pesquisa em comércio eletrônico.

Design de Experiência do Usuário para Sistemas de Pesquisa: A pesquisa é mais do que apenas “digitar palavras-chave e exibir resultados” — uma experiência completa de pesquisa inclui: 1. Sugestões de pesquisa (autocompletar à medida que você digita); 2. Resultados da pesquisa (lista + ordenação); 3. Filtragem por facetas (filtros de categorias/preço/marca); 4. Estatísticas dos resultados (contagem total/distribuição por categoria); 5. Tratamento de resultados vazios (recomendação de itens populares ou sugestão de modificações nas palavras-chave). O $facet do MongoDB pode fornecer dados para os itens 2, 3 e 4 em uma única consulta, enquanto as sugestões de pesquisa e o tratamento de resultados vazios exigem consultas adicionais e lógica de negócios.

100%
graph TB
    A[Search Request<br/>q=5G phone<br/>category=Electronics] --> B[$match<br/>$text + category]
    B --> C[$facet]
    
    C --> D[items<br/>$sort+skip+limit<br/>Paginated List]
    C --> E[total<br/>$count<br/>Total]
    C --> F[facets<br/>$bucket<br/>Prices by Bin]
    C --> G[categories<br/>$group<br/>Categorical Statistics]
    
    D --> H[Search Results Page<br/>Returns one result]
    E --> H
    F --> H
    G --> H
    
    style C fill:#d4edda
    style H fill:#cce5ff

(1) Pesquisa de produtos + Estatísticas por facetas

O valor comercial da pesquisa facetada: A pesquisa facetada é o principal modelo de interação para a pesquisa em comércio eletrônico — depois que um usuário digita uma palavra-chave, a página exibe simultaneamente os resultados da pesquisa e os critérios de filtragem em várias dimensões (faixa de preço, categoria, marca, avaliação). Quando um usuário clica em qualquer filtro, os resultados da pesquisa são imediatamente refinados, e as contagens dos filtros são atualizadas em tempo real. Essa experiência iterativa de “pesquisar → filtrar → pesquisar novamente” é 10 vezes mais eficiente do que a abordagem tradicional de “pesquisar → paginar → pesquisar novamente”. O $facet é a melhor ferramenta para implementar a pesquisa facetada — ele retorna estatísticas em todas as dimensões em uma única consulta, eliminando a necessidade de múltiplas solicitações.

Projeto dos parâmetros da API de pesquisa: Os parâmetros da API de pesquisa devem abranger todas as dimensões de filtro — 1. Palavra-chave (q): $text — a consulta de pesquisa; 2. Categoria (category): Correspondência exata do campo “category”; 3. Faixa de preço (minPrice/maxPrice): $gte/$lte para filtrar o campo price; 4. Marca (brand): correspondência exata ou $in para seleções múltiplas; 5. Avaliação (minRating): $gte para filtrar o campo rating; 6. Paginação (page/limit): $skip/$limit para controlar o número de resultados; 7. Ordenação (sort): ordenar por relevância, preço, avaliação ou volume de vendas. Princípios de design dos parâmetros: Todos os parâmetros de filtro são opcionais (retornam todos os resultados quando nenhum parâmetro é fornecido); os parâmetros de paginação têm valores padrão (page=1, limit=20); a ordenação tem uma opção padrão (sort=relevância).

JAVASCRIPT
// === Product Search + Multiple statistical dimensions ===
app.get('/api/products/search', async (req, res) => {
  const { q, category } = req.query;
  const match = {};
  if (q) match.$text = { $search: q };
  if (category) match.category = category;

  const result = await Product.aggregate([
    { $match: match },
    {
      $facet: {
        items: [
          { $sort: { score: { $meta: 'textScore' } } },
          { $skip: 0 },
          { $limit: 20 },
          { $project: { sku: 1, title: 1, price: 1, thumbnail: 1 } }
        ],
        total: [{ $count: 'count' }],
        facets: [
          {
            $bucket: {
              groupBy: '$price',
              boundaries: [0, 100, 500, 1000, 5000],
              default: '5000+',
              output: { count: { $sum: 1 } }
            }
          }
        ],
        categories: [
          {
            $group: {
              _id: '$category',
              count: { $sum: 1 }
            }
          }
        ]
      }
    }
  ]);

  res.json(result[0]);
});

(2) Pesquisar lojas próximas

JAVASCRIPT
// === Find Stores Near You ===
app.get('/api/stores/nearby', async (req, res) => {
  const { lng, lat, maxDistance = 5000 } = req.query;

  const stores = await Store.find({
    location: {
      $nearSphere: {
        $geometry: { type: 'Point', coordinates: [parseFloat(lng), parseFloat(lat)] },
        $maxDistance: parseInt(maxDistance)
      }
    }
  }).limit(20);

  res.json(stores);
});

▶ Exemplo 1: Aplicação prática da pesquisa multidimensional com $facet + lojas próximas por localização

JAVASCRIPT
// Scene 1:Multi-dimensional Product Search(Text + Statistics by Category)
db.products.insertMany([
  { sku: 'PHONE-001', title: 'Smartphone X 5G', description: 'Latest 5G phone', price: 599, category: 'Electronics', location: { type: 'Point', coordinates: [139.6917, 35.6895] } },
  { sku: 'PHONE-002', title: 'Smartphone Pro 5G', description: 'Pro 5G phone', price: 899, category: 'Electronics', location: { type: 'Point', coordinates: [139.7017, 35.6995] } },
  { sku: 'LAPTOP-001', title: 'Laptop Pro', description: 'Powerful laptop', price: 1299, category: 'Computers', location: { type: 'Point', coordinates: [139.6817, 35.6795] } }
]);

db.products.createIndex({ title: 'text', description: 'text' });
db.products.createIndex({ location: '2dsphere' });

// Text Search + Statistics by Category + Prices by Bin
db.products.aggregate([
  { $match: { $text: { $search: '5G phone' } } },
  {
    $facet: {
      // List of Results
      items: [
        { $sort: { score: { $meta: 'textScore' } } },
        { $limit: 10 },
        { $project: { sku: 1, title: 1, price: 1, score: { $meta: 'textScore' } } }
      ],
      // Total
      total: [{ $count: 'count' }],
      // Prices by Bin
      priceBuckets: [
        {
          $bucket: {
            groupBy: '$price',
            boundaries: [0, 500, 1000, 2000],
            default: '2000+',
            output: { count: { $sum: 1 }, avgPrice: { $avg: '$price' } }
          }
        }
      ],
      // Categorical Statistics
      categories: [
        { $group: { _id: '$category', count: { $sum: 1 } } }
      ]
    }
  }
]);

// Scene 2:Find Stores Near You(5km Electronic products inside)
db.products.aggregate([
  {
    $geoNear: {
      near: { type: 'Point', coordinates: [139.6917, 35.6895] },  // Tokyo Station
      distanceField: 'distance',
      maxDistance: 5000,  // 5km
      spherical: true,
      query: { category: 'Electronics' }
    }
  },
  { $project: { sku: 1, title: 1, price: 1, distance: { $round: ['$distance', 0] } } }
]);

// Output: Distance to Each Item (meters), Sort by Distance

Saída: O Cenário 1 retorna resultados multidimensionais (lista + contagem total + faixas de preço + categorias); o Cenário 2 retorna uma lista de produtos classificados por distância.

▶ Exemplo: E-commerce Product Search com $text e $facet (Difficulty ⭐⭐)

JAVASCRIPT
// Scene: ShopHub product search page with autocomplete, filters, and aggregations
db.products.createIndex({ title: 'text', description: 'text' });

db.products.insertMany([
  { sku: 'PHONE-001', title: 'Smartphone X Pro', description: 'Flagship smartphone with AI camera', price: 999, category: 'Electronics', stock: 50 },
  { sku: 'PHONE-002', title: 'Smartphone Y Lite', description: 'Budget smartphone with long battery', price: 299, category: 'Electronics', stock: 120 },
  { sku: 'LAPTOP-001', title: 'Laptop Pro 15', description: 'Powerful laptop for creators', price: 1499, category: 'Electronics', stock: 30 },
  { sku: 'BOOK-001', title: 'MongoDB Guide', description: 'Complete guide to MongoDB', price: 49, category: 'Books', stock: 200 }
]);

// Complete search: text search + facet for filters + price buckets
const searchResults = db.products.aggregate([
  // Stage 1: Full-text search for "smartphone"
  { $match: { $text: { $search: 'smartphone' } } },
  // Stage 2: Calculate relevance score
  { $addFields: { score: { $meta: 'textScore' } } },
  // Stage 3: Facet for multiple dimensions
  { $facet: {
    // Result list with pagination
    products: [
      { $sort: { score: -1 } },
      { $skip: 0 },
      { $limit: 10 },
      { $project: { sku: 1, title: 1, price: 1, score: 1 } }
    ],
    // Total count for pagination
    totalCount: [{ $count: 'count' }],
    // Price distribution by buckets
    priceBuckets: [
      { $bucket: {
        groupBy: '$price',
        boundaries: [0, 100, 500, 1000, 2000],
        default: 'other',
        output: { count: { $sum: 1 } }
      }}
    ],
    // Category breakdown
    byCategory: [
      { $group: { _id: '$category', count: { $sum: 1 } } }
    ]
  }}
]);

console.log(JSON.stringify(searchResults.toArray()[0], null, 2));

Saída:

TEXT 📖 Somente leitura
{
  "products": [
    { "sku": "PHONE-001", "title": "Smartphone X Pro", "price": 999, "score": 1.5 },
    { "sku": "PHONE-002", "title": "Smartphone Y Lite", "price": 299, "score": 1.2 }
  ],
  "totalCount": [{ "count": 2 }],
  "priceBuckets": [
    { "_id": 0, "count": 0 },
    { "_id": 100, "count": 1 },
    { "_id": 500, "count": 0 },
    { "_id": 1000, "count": 1 }
  ],
  "byCategory": [{ "_id": "Electronics", "count": 2 }]
}

▶ Exemplo 2: API de pesquisa multidimensional de produtos do ShopHub

JAVASCRIPT
// Scene:ShopHub E-commerce Search Page,A single query returns a list+Separate into buckets+Categories+Total
db.products.insertMany([
  { sku: 'PHONE-001', title: 'Smartphone X 5G', description: 'Latest 5G phone with amazing camera', price: 599, category: 'Electronics', brand: 'TechCorp' },
  { sku: 'PHONE-002', title: 'Budget Phone 5G', description: 'Affordable 5G phone', price: 299, category: 'Electronics', brand: 'DataFlow' },
  { sku: 'TABLET-001', title: 'Tablet Pro 5G', description: '5G tablet for professionals', price: 899, category: 'Electronics', brand: 'TechCorp' },
  { sku: 'BOOK-001', title: '5G Technology Guide', description: 'Understanding 5G networks', price: 39, category: 'Books', brand: 'AppVenture' },
  { sku: 'WATCH-001', title: 'Smart Watch', description: 'Fitness tracker watch', price: 199, category: 'Wearables', brand: 'DataFlow' }
]);

db.products.createIndex({ title: 'text', description: 'text' });

// Multi-dimensional Search:Text Search + Separate into buckets + Categories + Total
const results = db.products.aggregate([
  { $match: { $text: { $search: '5G' } } },
  {
    $facet: {
      items: [
        { $sort: { score: { $meta: 'textScore' } } },
        { $limit: 10 },
        { $project: { sku: 1, title: 1, price: 1, category: 1, score: { $meta: 'textScore' } } }
      ],
      total: [{ $count: 'count' }],
      priceBuckets: [
        {
          $bucket: {
            groupBy: '$price',
            boundaries: [0, 100, 300, 600, 1000],
            default: '1000+',
            output: { count: { $sum: 1 } }
          }
        }
      ],
      categories: [
        { $group: { _id: '$category', count: { $sum: 1 } } },
        { $sort: { count: -1 } }
      ]
    }
  }
]);

// Output Structure:
// {
//   items: [Smartphone X 5G(score:2.5), Tablet Pro 5G(score:2.0), Budget Phone 5G(score:1.8), 5G Technology Guide(score:1.2)],
//   total: [{count: 4}],
//   priceBuckets: [{_id:0,count:1},{_id:100,count:1},{_id:300,count:1},{_id:600,count:1}],
//   categories: [{_id:'Electronics',count:3},{_id:'Books',count:1}]
// }

Saída: Uma única consulta $facet retorna uma lista de resultados (classificada por relevância), a contagem total, uma distribuição de preços por faixa e estatísticas por categoria, permitindo que o front-end exiba a página de pesquisa diretamente.

❓ Perguntas Frequentes

Explicação das perguntas comuns: Essas quatro perguntas correspondem às principais limitações de quatro áreas-chave — o limite de memória do $facet, o limite de idioma da pesquisa de texto, as convenções de coordenadas para consultas geoespaciais e o limite algorítmico do $bucketAuto. Cada limitação tem suas próprias razões técnicas e estratégias correspondentes. Compreender essas limitações é mais importante do que simplesmente memorizar as respostas — por exemplo, saber que a pesquisa de texto do MongoDB não suporta a segmentação de palavras em chinês permite que você incorpore o Elasticsearch ao projeto de sua arquitetura desde o início.

P: Qual é a diferença entre $facet e consultas múltiplas? R: $facet retorna todas as dimensões em uma única consulta, reduzindo o número de idas e voltas na rede. No entanto, consome muita memória.

P: A pesquisa de texto é compatível com o chinês? R: Por padrão, os índices de texto do MongoDB são separados por palavras (por espaços ou sinais de pontuação), enquanto o chinês é separado por caracteres. Para realizar a segmentação de palavras em chinês, você precisará usar o Elasticsearch.

P: Qual é a ordem das coordenadas geográficas? R: As coordenadas geográficas no MongoDB são [longitude, latitude] (Observação: a longitude vem primeiro).

P: Como o $bucketAuto seleciona os limites dos buckets? R: Ele seleciona automaticamente tamanhos uniformes de buckets com base na distribuição dos dados (por exemplo, 5 buckets, com aproximadamente 20% dos dados em cada um).


📖 Resumo

Integração dos quatro temas principais: $facet + $bucket amplia os recursos de agregação — permitindo que as páginas de resultados de pesquisa retornem dados em todas as dimensões de uma só vez; a pesquisa de texto e as consultas geoespaciais ampliam os recursos de consulta — desde correspondências exatas até pesquisa aproximada, e desde consultas por atributos até consultas espaciais. Combinados, esses quatro recursos formam um sistema completo de pesquisa para comércio eletrônico — pesquisa por palavra-chave + filtragem por facetas + segmentação por faixa de preço + lojas próximas —, cobrindo 90% dos cenários de pesquisa.

Decisões técnicas de seleção para sistemas de busca: Escolhendo entre a busca nativa do MongoDB e o Elasticsearch — 1. O MongoDB é adequado para: projetos pequenos (< 100.000 documentos), lógica de busca simples (filtragem por palavra-chave + categoria + preço) e situações em que não se deseja introduzir uma nova infraestrutura; 2. O Elasticsearch é adequado para: grandes projetos (> 1 milhão de documentos), cenários que exigem segmentação de palavras em chinês, pesquisa por pinyin e expansão de sinônimos, algoritmos complexos de classificação (BM25 + boosts personalizados) e indexação quase em tempo real (atualizações na ordem de milissegundos); 3. Abordagem híbrida: os dados são armazenados no MongoDB e sincronizados com o Elasticsearch por meio do Change Stream, com as consultas executadas no Elasticsearch. Essa abordagem híbrida combina o desempenho de gravação do MongoDB com os recursos de pesquisa do Elasticsearch — é a arquitetura mais comum em ambientes de produção.

Padrões de projeto para subpipelines $facet: Existem três padrões de projeto para subpipelines $facet — 1. Subpipelines independentes (mais comum): cada subpipeline processa de forma independente todo o conjunto de dados, sem dependências entre elas (por exemplo, a subpipeline items retorna uma lista, a subpipeline total retorna uma contagem e a subpipeline facets retorna buckets); 2. $match compartilhado: Todos os subpipelines compartilham os resultados da filtragem do estágio $match que antecede o $facet, evitando filtragens duplicadas; 3. $facet aninhado (não recomendado): $facet aninhado dentro de outro $facet, o que resulta em lógica complexa e dobra o consumo de memória. O padrão 2 é a prática recomendada — aplique a filtragem comum antecipadamente (como por categoria, palavra-chave ou faixa de preço) e faça com que os subpipelines executem apenas suas respectivas lógicas estatísticas.


📝 Exercícios

  1. Pergunta básica (⭐): Crie um índice de texto (título + descrição) para a coleção products e realize uma pesquisa $text.
  2. Problema básico (⭐): Use $bucket para contar o número de itens por faixa de preço.
  3. Problema avançado (⭐⭐): Implemente uma API de pesquisa completa (pesquisa de texto + estatísticas baseadas em buckets + estatísticas baseadas em categorias).
  4. Exercício avançado (⭐⭐): Implemente uma busca por lojas próximas (2dsphere + $nearSphere).
  5. Questão desafiadora (⭐⭐⭐): Sistema de pesquisa integrado (texto + geografia + estatísticas facetadas).

Recomendações para a implementação do desafio: O sistema de busca abrangente é o desafio mais complexo deste curso — ele exige a combinação de quatro funcionalidades: $text (busca de texto), $facet (estatísticas multidimensionais), $bucket (agrupamento por faixa de preço) e $geoNear (classificação por distância). Recomendamos uma implementação passo a passo: 1. Primeiro, implemente a pesquisa $text e a lista de resultados; 2. Em seguida, adicione $facet para estatísticas multidimensionais; 3. Depois, adicione $bucket para agrupamento por faixa de preço; 4. Por fim, adicione a pesquisa de proximidade $geoNear. Passe para a próxima etapa somente após a verificação de cada etapa. Observe que $text e $geoNear não podem ser usados dentro do mesmo $match — $text deve ser a primeira condição em $match, e $geoNear deve ser a primeira etapa no pipeline. Solução: use $facet para executar as duas pesquisas em paralelo ou execute $text primeiro, seguido por $geoNear.

Pontos-chave para a implantação de um sistema de busca abrangente: A versão educacional do desafio ainda está a alguns passos de entrar em produção — 1. Validação de entrada: Todos os parâmetros de consulta devem ser validados (comprimento de q entre 1 e 200, valores de enumeração de categoria, faixa de preço de 0 a 999999, validação do formato de coordenadas); 2. Classificação padrão: classificar por volume de vendas ou avaliação (não em ordem aleatória) quando nenhuma palavra-chave for fornecida; classificar por relevância quando palavras-chave forem fornecidas; 3. Armazenamento em cache dos resultados: armazenar em cache os resultados para os mesmos parâmetros de consulta por 5 minutos (chave = hash(parâmetros)) para reduzir a carga no banco de dados; 4. Proteção contra tempo limite: maxTimeMS: 5000 limita o tempo de execução da agregação; se ocorrer um tempo limite, resultados parciais são retornados juntamente com uma mensagem informando “Os resultados podem estar incompletos”; 5. Monitoramento de consultas lentas: consultas de pesquisa com tempo de execução superior a 1 segundo são registradas para otimizar índices e pipelines. Esses pontos representam as principais lacunas na transição do sistema de pesquisa da fase de demonstração para a produção.

Web-Tutorial.com

Equipe Técnica Web-Tutorial

Uma plataforma de tutoriais mantida por diversos desenvolvedores. Cada tutorial é escrito e revisado por profissionais da área correspondente. Trabalhamos para manter nosso conteúdo preciso e confiável — se encontrar algum problema, avise-nos.

100%