$facet + $bucket + Text/Geospatial Search
Advanced Aggregation Features—Master $facet, $bucket, text search, and geospatial queries to build comprehensive data analysis capabilities.
The Four Major Themes of This Course: $facet (parallel multi-channel) addresses the challenge of “multidimensional results from a single query”; $bucket (data bucketing) addresses the challenge of “interval statistics”; text search addresses the challenge of “keyword lookup”; and geospatial queries address the challenge of “distance and range.” While these four themes may appear independent, they collectively form the complete capabilities of an e-commerce search system—search results + faceted statistics + price bucketing + nearby stores.
1. What You'll Learn
- $facet Multi-channel parallel computation
- $bucket / $bucketAuto Data Bucketing
- Text index + $text/$search
- 2dsphere Geospatial Index + $geoNear / $geoWithin
The Educational Value of Advanced Search: $facet + $bucket + $text + $geoNear make up the complete capabilities of MongoDB search—$facet is the backbone of the search architecture (multi-dimensional parallel processing), $bucket is a tool for analyzing data distribution (histograms), $text is the engine for keyword retrieval, and $geoNear is the core of location services. Mastering these four operators means you can build a search system that doesn’t rely on Elasticsearch—which saves a significant amount on infrastructure costs for small and medium-sized projects.
Evolution of Search Systems: Evolution of project search requirements—Phase 1 (MVP): Simple $match + $sort (filtering and sorting by category/price); Phase 2 (Faceted Search): + $facet + $bucket (multi-dimensional statistics on the search page); Phase 3 (Full-Text Search): + text indexing + $text (keyword search); Phase 4 (LBS): + 2dsphere + $geoNear (nearby search); Phase 5 (Specialized Search): Migration to Elasticsearch (Chinese word segmentation, synonyms, and pinyin). Most projects remain in Phases 3–4 and only advance to Phase 5 when the volume of data or search complexity exceeds MongoDB’s capabilities.
Production Considerations for $facet: There are three key limitations for $facet in production: 1. Memory limit: Each sub-pipeline shares a 100MB memory limit; the total output of all sub-pipelines cannot exceed this limit. When dealing with large datasets, you must use $match to filter the data before applying $facet; 2. Do not use $collation (sorting rules) in sub-pipelines; if Chinese sorting is required, handle it outside of $facet; 3. Avoid using $lookup in sub-pipelines ($lookup within $facet can lead to catastrophic performance issues—each sub-pipeline executes a join query once). Best practice: Use $match or $project before $facet to reduce the input volume; limit subpipelines to lightweight statistics ($group, $bucket, $count), and place heavy operations ($lookup, $sort) outside of $facet.
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
$facet Parallel Execution Model: $facet’s parallelism is not “multi-threaded acceleration”—MongoDB executes each sub-pipeline sequentially within a single thread, but they share the same input snapshot, so they are logically parallel. The true performance benefit lies not in parallel execution, but in “reducing network round trips”—four statistical dimensions require only one query instead of four. The memory consumption of $facet is the sum of all sub-pipelines—if 1,000 documents are input, and each of the four sub-pipelines processes 1,000 documents, the total memory consumption is approximately equivalent to that of 4,000 documents.
$facet Output Structure Design: The $facet output is a document where the keys are sub-pipeline names and the values are arrays of sub-pipeline results. When designing sub-pipelines, keep the following in mind: 1. Each sub-pipeline is independent—it cannot reference the results of other sub-pipelines; 2. The order of sub-pipelines does not affect the results (they run in parallel logically); 3. Any aggregation stage ($match, $group, $sort, $lookup, etc.) can be used within a sub-pipeline; 4. An empty result returns an empty array [] rather than null. When parsing $facet results on the front end, use Object.keys() to iterate through each dimension.
Cost of $facet and Alternatives: The cost of $facet = input data volume × number of sub-pipelines. When the input data volume is large (>100,000 records) and there are many sub-pipelines (>5), memory consumption may exceed limits. Alternatives: 1. Split large $facet operations into multiple independent aggregation queries—sacrificing network efficiency for memory safety; 2. Prepend $match/$project to $facet to significantly reduce the input volume; 3. Use materialized views ($merge/$out) to precalculate statistical results. Selection criteria: Use $facet for small data volumes (<10,000 records) and materialized views for large data volumes.
2. $facet Multi-Channel Parallel Processing
Concept Description: $facet allows multiple independent sub-pipelines to run in parallel on the same set of input documents. Each sub-pipeline produces its own results, which are ultimately merged into a single document. This is the ideal tool for building search results pages (list + faceted statistics + bucketing + totals)—a single query replaces multiple round trips.
How It Works: $facet accepts an object where the key is the sub-pipeline name and the value is an array of stages. MongoDB executes all sub-pipelines in parallel on the same input, with each processing the data independently. The output is a document where each key corresponds to the result array of a sub-pipeline. Note: The sub-pipelines in $facet share the input but cannot reference each other’s results, and the total memory consumption is the sum of the memory used by all sub-pipelines.
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 Sub-pipeline | Typical Uses | Common Stage Combinations |
|---|---|---|
| Results List | Pagination | $sort + $skip + $limit + $project |
| Total Statistics | Pagination Information | $count |
| Category Statistics | Filter by Aspect | $group + $sort |
| Price by Bin | Filter by Price Range | $bucket / $bucketAuto |
| Overview Statistics | Aggregated Metrics | $group({ _id: null }) |
Principles of a Multi-Dimensional Analysis Architecture: The core value of $facet is “one query, multi-dimensional results”—e-commerce search pages need to display the following simultaneously: product lists (paginated), total count (pagination information), price distribution (filters), and category statistics (sidebar). Without $facet, this would require four separate queries (four network round trips); with $facet, a single query returns all dimensions, resulting in a fourfold increase in front-end rendering speed.
Memory Constraints for $facet: Parallel execution of $facet is not truly parallel—MongoDB executes the sub-pipelines in the order they are defined, but they share the same input. Memory consumption is the sum of the memory used by each sub-pipeline: 1,000 documents × 4 sub-pipelines = the memory required to process 4,000 documents. Mitigation strategies: 1. Use $match before $facet to reduce the input volume; 2. Use $project in sub-pipelines to streamline fields; 3. Limit the number of sub-pipelines (3–5 is recommended); 4. For very large datasets, set allowDiskUse: true.
Isolation of $facet Subpipelines: Each $facet subpipeline is completely isolated—it cannot reference the results of other subpipelines, nor can it share intermediate computations. If multiple subpipelines require the same preprocessing (such as field extraction by $project), each subpipeline must perform it repeatedly. The advantage of isolation is dependency-free parallelism; the disadvantage is redundant computation. The overhead of a small amount of redundant computation is far lower than the wait time associated with serial execution—this is the core trade-off in the $facet design.
Design Pattern for $facet Sub-Pipes: The design of $facet sub-pipes follows the "one dimension per pipe" principle— 1. Result list pipe: $sort + $skip + $limit + $project (for paginated display; $limit must be used to restrict the number of results); 2. Total Count Pipeline: $count (returns only a number; the lightest-weight pipeline); 3. Category Statistics Pipeline: $group + $sort + $limit (counts grouped by category field; $limit returns the Top N); 4. Price Bucketing Pipeline: $bucket/$bucketAuto (buckets data by range; outputs histogram data); 5. Overview pipeline: $group({_id: null, avg: {$avg}, sum: {$sum}}) (global statistics; returns only one document). These five pipelines cover 90% of the data requirements for search pages.
Details of the $facet Execution Model: Although $facet appears to be parallel, MongoDB executes each sub-pipeline in a single thread—true parallelism occurs only in sharded clusters (where each shard processes its own data independently, and mongos merges the results). In a single-instance or replica set environment, sub-pipelines are executed sequentially in the order they are defined. However, $facet is still faster than multiple independent queries—1. It scans the input data only once (each sub-pipeline shares the same data read); 2. It requires only one network round-trip (client → MongoDB → return of complete results); 3. It avoids the overhead of repeated $match operations (the $match preceding $facet is executed only once).
Business Scenarios for Multidimensional Analysis: The most typical business scenario for $facet is an e-commerce search page—when a user searches for "cell phones," the page needs to display the following simultaneously: 1. A list of products (paginated, sorted, with images and prices); 2. Total number of search results (“256 products found”); 3. Bar chart showing price distribution (three ranges: 0–1,000, 1,000–3,000, and 3,000+); 4. Brand distribution (Huawei 30%, Apple 25%, Xiaomi 20%, etc.); 5. Screen size distribution, etc. The traditional approach required 5 queries × 50 ms = 250 ms, while a single $facet query takes approximately 80 ms, transforming the user experience from “noticeable lag” to “instant response.”
$facet vs. Multiple Queries: A Trade-off: $facet returns multidimensional results in a single query, but it also has limitations—1. All $facet sub-pipelines must be within the same aggregation call and cannot be loaded on demand (for example, if a user only views the product list and not the statistics, the statistics pipeline is still executed); 2. The $facet result structure is fixed, so dimensions cannot be dynamically added (e.g., adding a “color distribution” dimension requires modifying the aggregation pipeline and redeploying); 3. Error handling is difficult—if any sub-pipeline in $facet fails, the entire aggregation fails (partial returns are not possible). Alternative approach: For scenarios where dimensions may change dynamically, place high-frequency dimensions within $facet (product list + total count) and low-frequency dimensions in separate APIs (brand distribution/price histogram via separate endpoints called on demand). This ensures fast response times for high-frequency queries while preventing low-frequency queries from wasting $facet resources.
// === $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
Mathematical Principles of $bucket: The boundaries array of $bucket defines N-1 buckets, each of which is the left-closed, right-open interval [boundaries[i], boundaries[i+1]). Documents are placed into the corresponding bucket based on their groupBy value. For example, boundaries=[0, 100, 500, 1000] defines 3 buckets: [0,100), [100,500), [500,1000). The _id of a $bucket is the left boundary value of the bucket. $bucketAuto automatically selects bucket boundaries based on the data distribution to ensure that each bucket contains approximately the same number of documents—making it suitable for exploratory data analysis (when the data distribution is unknown).
Business Scenarios for $bucket: Typical business scenarios for $bucket—1. Price Range Statistics: E-commerce platforms count the number of products by price range (0–100, 100–500, 500–1,000) for use in price filters on search pages; 2. Rating distribution: Review systems group ratings into buckets (1-star/2-star/3-star/4-star/5-star) to display rating bar charts; 3. Age grouping: User profiling groups users by age range (18–25, 25–35, 35–50, 50+), used for targeted marketing; 4. Time grouping: Logs are grouped by hour, day, or month, used for trend charts. $bucket essentially represents a conversion from "continuous values → discrete intervals."
Strategy for Handling Empty Buckets: $bucket displays empty buckets (count=0), while $bucketAuto does not. This has business implications—the price filter on the search page needs to display all price ranges (including empty buckets), otherwise users won’t know which ranges are available. Rating distribution charts also need to display bars for all star ratings (including those with 0 reviews). Therefore, use $bucket (known ranges) for rating/price filtering scenarios, and $bucketAuto (which automatically detects data distribution patterns) for exploratory analysis.
Expanding the $bucket Output: The $bucket output can do more than just count using $sum: 1; it can also perform other aggregations—such as the average rating for each price range (avgRating: {$avg: '$rating'}), the highest price (maxPrice: {$max: '$price'}), and a list of included products (products: {$push: '$title'}). . Extending the output transforms $bucket from a simple counting tool into a multidimensional grouping and statistics tool—returning multiple statistical metrics for each interval in a single query.
$facet + $bucket Search Page Architecture: The classic architecture for e-commerce search pages—a single $facet query returns: 1. A list of search results ($sort + $skip + $limit); 2. Total count ($count); 3. Price distribution ($bucket, grouped by price range); 4. Category distribution ($group, grouped by category); 5. Rating distribution ($bucket, grouped by rating). The front end retrieves all data in a single request and renders the complete search page—including the product list, price filter, category sidebar, and rating bar chart.
Performance Considerations for $facet Queries: $facet’s sub-pipelines run in parallel but share input data— 1. Shared Input: The $match stage preceding $facet is executed only once, so all sub-pipelines process the same input dataset; 2. Memory Usage: Each sub-pipeline maintains its own memory state; N sub-pipelines = N times the memory usage (if the single-stage limit is 100MB, 4 sub-pipelines might consume 400MB); 3. Optimization Strategy: Avoid redundant $match operations within sub-pipelines (since filtering has already been performed before $facet); perform only necessary $project/$group operations; 4. Alternative Approach: If $facet exceeds the memory limit, it can be split into multiple independent aggregation queries (at the cost of atomicity and increased network round trips). In production environments, $facet is suitable for small to medium-sized datasets (< 100,000 matching documents); for large datasets, memory usage must be evaluated.
3. $bucket Data Bucketing
The Statistical Principle Behind Bucketing: A $bucket is essentially a histogram data structure—it divides a continuous value range into discrete intervals and counts the number of documents in each interval. A histogram is the first step in data exploration: by observing the distribution of values, you can determine whether the data is normally distributed, whether there are outliers, and whether normalization is needed. $bucket is suitable for known intervals (such as price ranges 0–100, 100–500, and 500–1000), while $bucketAuto is suitable for exploratory analysis (automatically selecting uniform buckets).
$bucket vs. $switch: A Comparison of Bucketing: Both $bucket and $switch can be used for range classification, but they have different design objectives—$bucket is a statistical tool (outputting counts and aggregated values for each group), while $switch is a classification tool (outputting classification labels for each document). Selection: If you need a histogram or statistics → use $bucket; if you need to assign a label to each document → use $switch + $addFields.
The $bucketAuto Bucketing Algorithm: $bucketAuto uses an approximate equal-depth bucketing algorithm—the goal is to have each bucket contain roughly the same number of documents, rather than equal-width bucketing (where each bucket has the same value range). For example, if product prices are concentrated in the 50–200 range, $bucketAuto will create more buckets in the 50–200 range (where data density is high) and fewer buckets in the 500–5000 range (where data is sparse). This provides more information than equal-width bucketing—which creates empty buckets in sparse areas of the data and crams large amounts of data into a single bucket in dense areas. The granularities option in $bucketAuto controls the number of buckets (e.g., 5, 10, or 20 buckets).
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
| Comparison Dimension | $bucket | $bucketAuto |
|---|---|---|
| Bucket Boundary | Manually Specified | Automatically Calculated |
| Number of buckets | Determined by the number of boundaries | Specified buckets: N |
| Bucket size | May vary | As uniform as possible |
| Use Cases | Known Intervals (Price Brackets) | Exploring Data Distribution |
| Empty Barrel Handling | Show Empty Barrels | Do Not Show Empty Barrels |
$bucket Output Extensions: The output parameter of $bucket allows you to perform aggregation calculations within each bucket—not limited to a simple $sum: 1 count, but also including $avg (average price), $push (collecting specific fields from documents in the bucket), and $max/$min (maximum and minimum values in the bucket). Typical usage: $bucket + output: {count: {$sum: 1}, avgPrice: {$avg: '$price'}, topProduct: {$max: '$price'}} returns the count, average price, and most expensive item within a single bucket. This makes $bucket not just a "count-by-bucket" tool, but also a "statistical analysis by bucket" tool.
$bucketAuto’s Algorithm and Limitations: $bucketAuto uses an approximate equal-depth bucketing algorithm—it aims to ensure that each bucket contains the same number of documents, rather than intervals of the same width. This means that the 0–100 price range might contain 500 documents (since there are more inexpensive items), while the 5,000–10,000 range might contain only 50 documents (since there are fewer expensive items). Limitations: 1. Bucket boundaries are uncontrollable (automatically calculated; business-specific ranges like “0–100” or “100–500” cannot be specified); 2. Poor performance with data skew (e.g., 99% of products are priced between 0 and 1,000, while 1% are priced at 10,000+; automatic bucketing would create many small buckets within the 0–1,000 range); 3. Not suitable for presentation to business users (bucket boundaries are arbitrary values such as 47.5–89.3, rather than intuitive ranges like “0–100” or “100–500”). $bucketAuto is suitable for exploratory data analysis, while $bucket is suitable for presenting fixed ranges to business users.
// === $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
}
}
]);
Choosing a Search Architecture: MongoDB text search vs. Elasticsearch is a classic architectural decision. Advantages of MongoDB text search: 1. No additional infrastructure required (data is already in MongoDB); 2. Consistent with CRUD operations (same query language); 3. Sufficient performance for small datasets (<1 million records). Advantages of Elasticsearch: 1. Chinese word segmentation (using segmenters such as the IK Analyzer); 2. Fuzzy matching and synonyms; 3. Highlighting; 4. Support for data volumes in the tens of billions. Selection principle: Use MongoDB for simple searches and Elasticsearch for complex searches. You can start with MongoDB text search and migrate to Elasticsearch once your search requirements become more complex.
The Process of Building an Inverted Index: When creating a text index, MongoDB performs lexical analysis on the text fields of each document—1. Tokenization (splitting by spaces/punctuation); 2. Stem extraction (e.g., “running” → “run,” plural → singular); 3. Stopword removal (high-frequency, meaningless words such as “the,” “a,” “an,” etc.); 4. Building an inverted index (word → list of document IDs + term frequency). During a query, the terms in the $search clause also undergo tokenization and stemming, and matching documents are then retrieved from the inverted index. Note: CJK (Chinese/Japanese/Korean) text is split by individual characters rather than words (no space-based tokenization), which yields poor results—for example, a multi-character CJK word would be split into individual single-character entries instead of being recognized as a single semantic unit.
BM25 Relevance Score: MongoDB Text Search uses the BM25 algorithm to calculate relevance scores, taking three factors into account: 1. Term Frequency (TF): The more times a term appears in a document, the higher the score; 2. Inverse Document Frequency (IDF): The less frequently a term appears across all documents (the rarer it is), the higher the score; 3. Document-length normalization: A match in a short document is considered more valuable than a match in a long document. The weights parameter in weighted text indexing affects the BM25 calculation—fields with higher weights contribute more to the score when a match is found.
4. Text Search
Search System Principles: MongoDB text search is based on inverted indexes—during index creation, text is tokenized and stemmed to build a "word → document" mapping table. During a query, the $search operator is used to specify keywords, and matching documents are searched for in the inverted index, with relevance scores calculated using the TF-IDF algorithm. Limitations: 1. Does not support Chinese tokenization (splits by characters rather than semantic tokenization); 2. Does not support fuzzy matching (requires Elasticsearch’s fuzzy query); 3. Does not support synonym expansion; 4. Only one text index per collection.
When to Use MongoDB Text Search vs. Elasticsearch: MongoDB Text Search is suitable for simple scenarios—exact/phrase searches for English content and full-text search for small datasets. Elasticsearch is suitable for complex scenarios—Chinese word segmentation, fuzzy search, synonyms, highlighting, pinyin search, and multi-field weighting. Decision criteria: 1. Data volume < 100,000 and only English search is required → MongoDB is sufficient; 2. Chinese word segmentation or fuzzy search is required → Elasticsearch is mandatory; 3. Search is a core feature → Elasticsearch; 4. Search is a secondary feature → MongoDB provides a simple implementation.
The Third Option for MongoDB Atlas Search: Atlas Search is the built-in search engine of the MongoDB Atlas cloud service—based on Apache Lucene (the same underlying engine as Elasticsearch)—that provides advanced features such as Chinese word segmentation, fuzzy search, synonyms, and highlighting, and integrates seamlessly with MongoDB (no need to synchronize data to an external cluster). Advantages: 1. Zero data synchronization latency (Lucene indexes sync with MongoDB collections in real time); 2. Direct use in the $search stage of aggregation pipelines (no need to learn new query syntax); 3. Zero operational burden (hosted by Atlas, no need to manage an Elasticsearch cluster). Limitations: 1. Available only on the Atlas cloud service (not supported on self-hosted MongoDB); 2. Additional costs apply; 3. Features are not as comprehensive as Elasticsearch (e.g., no aggregation buckets or nested types). For small and medium-sized projects, Atlas Search offers the best balance—it eliminates the need to maintain an Elasticsearch cluster while providing professional-grade search capabilities.
Summary of Search System Selection Decisions: Choosing Among Three Search Solutions—1. MongoDB’s native $text: zero cost, zero maintenance, limited functionality (English search, no word segmentation, no fuzzy matching), suitable for small projects where search is a secondary feature; 2. Atlas Search: low maintenance, moderate cost, robust functionality (Chinese word segmentation, fuzzy matching, highlighting), suitable for medium-sized projects by Atlas users; 3. Elasticsearch: High operational overhead, high cost, most robust features (all search capabilities + Kibana analytics), suitable for large-scale projects where search is a core feature. Selection criteria: Data volume (small → MongoDB, large → ES), language (English only → MongoDB, Chinese → ES/Atlas), importance of search (secondary → MongoDB, core → ES), operational capabilities (limited → Atlas, strong → ES).
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
| Search Type | Syntax | Description |
|---|---|---|
| Word-Level Search | $search: 'phone smartphone' |
Match any word (OR) |
| Phrase Search | $search: '"smartphone 5g"' |
Must be a complete phrase |
| Exclude search | $search: 'phone -cheap' |
Include "phone" but exclude "cheap" |
| Sort by Relevance | score: { $meta: 'textScore' } |
Sort by Relevance |
(1) Create a text index
Text Index Design Strategy: Each collection can have at most one text index, but it can cover multiple fields and assign weights—fields with higher weights receive a higher textScore when matched, resulting in more reasonable search result rankings. Design considerations: 1. The title should be weighted higher than the description (title matches are more important to users during searches); 2. Do not create text indexes for fields with low information content (e.g., status, category); 3. Text indexes consume significant storage space (approximately 2–3% of the data volume), so use them with caution for large collections; 4. Composite indexes and text indexes cannot coexist in the same query.
// === 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) $text Search
Four Search Modes for $text: $text supports four search syntaxes—1. Word-level search (default OR semantics): 'phone smartphone' matches documents containing either word; 2. Phrase search (double quotes): '"smartphone 5g"' must contain the exact phrase; 3. Exclusion search (minus sign): 'phone -cheap' contains "phone" but not "cheap"; 4. Relevance sorting: Use $meta: 'textScore' to retrieve scores and sort results. Note: $text search does not support wildcards (*), fuzzy matching (~), or regular expressions.
Costs of Building and Maintaining Text Indexes: The cost of building text indexes is higher than that of regular indexes—1. Index size: Text indexes create inverted indexes after tokenizing each field; the index size may reach 30–50% of the original data (compared to about 10% for regular indexes); 2. Build time: Building a text index for large datasets (> 1 million documents) can take anywhere from several minutes to several hours and should be performed during off-peak hours; 3. Write performance: Every time a document containing text fields is inserted or updated, the inverted index must be updated, increasing write latency by 10–30%; 4. Limitations: Each collection can have only one text index (though it can cover multiple fields); text indexes do not support text searches in $or queries. These costs mean you should create text indexes only for fields that truly require full-text search, and avoid creating them for all string fields.
// === 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' } });
Geospatial Query Performance Optimization: 2dsphere indexes are key to geospatial query performance—without them, $geoNear and $nearSphere will perform a full collection scan. Optimization tips: 1. $geoNear must be the first stage in the pipeline (otherwise, the index cannot be used); 2. The smaller the maxDistance, the smaller the scan range, and the better the performance; 3. Composite indexes {location: '2dsphere', category: 1} support queries for “stores of a certain category nearby”; 4. $geoWithin is faster than $geoNear (without sorting) and should be prioritized for pure range queries.
Alternatives for Chinese Search: MongoDB’s text indexing offers limited support for Chinese—it splits text by characters rather than words, resulting in "multi-character CJK word" being split into four single-character entries, so a search for a multi-character term will not match the full compound word. Three alternatives: 1. Regular expression search $regex: /term/—simple but cannot sort results and does not use the index; 2. Application-level word segmentation—preprocess documents using a segmenter like jieba during insertion, store the segmented results in an array field, and query using multi-key indexes; 3. Elasticsearch—a professional search engine with a built-in Chinese segmenter (IK Analyzer) that supports Pinyin search and synonym expansion. Option 3 is the preferred choice for production environments.
Practical Approaches to Chinese Word Segmentation: The core challenge of Chinese search is that "words" lack natural delimiters—1. jieba segmentation approach: Use Node.js’s nodejieba for segmentation when inserting documents, store the results in the segmentedTitle: ['term1', 'term2', 'compound'] field, create a multi-key index, and perform queries by segmenting the search terms and using $all to match; 2. N-gram approach: Use $regex with n-gram matching (e.g., /term1.{0,2}term2/). This supports fuzzy matching but has poor performance (full-set scan); 3. Pinyin Search: Store an additional pinyinTitle field (converted using the pinyin library) to support searching for Chinese products via pinyin input; 4. Elasticsearch Solution: Create a MongoDB → Elasticsearch synchronization pipeline (using Change Stream or MongoSync). Queries run through ES, while data remains stored in MongoDB. Solution 1 is suitable for small and medium-sized projects (zero cost), while Solution 4 is suitable for large projects (professional search experience).
Internal Structure of the 2dsphere Index: The 2dsphere index encodes geographic coordinates as Geohash—a recursive grid partitioning of the Earth’s surface, where each grid cell is represented by a string of characters. During a query, $geoNear first identifies nearby grid cells based on the center point’s Geohash, then calculates the spherical distance precisely. The prefix-matching nature of Geohash allows range queries to make efficient use of the index—when querying “within 5 km,” only a few adjacent grids need to be scanned, rather than the entire dataset.
Accuracy of Distance Calculations: MongoDB uses spherical geometry (WGS84 ellipsoid) to calculate distances—the distanceField returned by $geoNear is in meters, with an accuracy of approximately 0.5 meters. The radius in $geoWithin and $centerSphere is in radians—to convert kilometers to radians, use km / 6378.1. Note: 6378.1 is the Earth’s equatorial radius (km). Using this conversion in high-latitude regions may result in a slight error (the polar radius is 6356.8 km), but the impact on LBS applications is negligible.
Integrated Architecture for Search + LBS: E-commerce search systems often need to support both "keyword search" and "nearby search" simultaneously—such as "5G phones within 3 km." Integration approaches for these two search types: 1. Perform a $text search first, then filter by distance using $geoNear (suitable for scenarios with few search results where nearby filtering is fast); 2. Perform a $geoNear proximity search first, followed by a $match keyword search (suitable for scenarios with few nearby results where keyword filtering is fast); 3. Run both searches in parallel using $facet and then merge the results (most flexible but memory-intensive). Approach 1 is the most common—users first see relevant products and then sort them by distance.
5. Geospatial Queries
Principles of Geospatial Indexing: The 2dsphere index encodes spherical coordinates (longitude/latitude) as Geohashes—a system that recursively divides the Earth’s surface into grids, each encoded by a string of characters. For proximity queries, the system first matches grids with the same Geohash prefix (coarse filtering), then precisely calculates the spherical distance (fine filtering). This two-stage strategy reduces the query complexity of $geoNear and $nearSphere from O(N) to O(log N).
Selecting Geospatial Query Operators: Each of the three query operators has its own use case—$geoNear is used during the aggregation phase, outputs distance fields, and supports sorting and filtering; it is best suited for “nearby search + result sorting” scenarios; $nearSphere is a query operator with simple syntax but does not output distance values; it is suitable for determining whether a result is “within a range”; $geoWithin does not sort results and is suitable for batch queries to retrieve “all results within a region” (such as delivery areas).
| Operator | Output Distance | Sorting | Aggregation Compatibility | Use Cases |
|---|---|---|---|---|
| $geoNear | ✅ | ✅ | Yes | LBS Search |
| $nearSphere | ❌ | ✅ | No | Distance Check |
| $geoWithin | ❌ | ❌ | Yes | Batch by region |
Unique Restrictions on $geoNear: $geoNear must be the first stage in the aggregation pipeline—this is a hard restriction because $geoNear needs to traverse the 2dsphere index starting from the root. If you need to apply $match filtering first, you must specify this in the $geoNear query option, rather than placing a $match stage before $geoNear. $geoNear’s distanceField outputs spherical distance (in meters), and distanceMultiplier can be used to convert units (e.g., × 0.001 to convert to kilometers). includeLocs: true outputs the matching coordinate points (useful for documents with multiple locations—such as a company with multiple branch locations).
Architectural Model of an LBS System: A complete LBS (Location-Based Service) system consists of three layers: 1. Data Layer: 2dsphere index + geographic coordinate storage; 2. Query Layer: $geoNear/$geoWithin for nearby searches and area queries; 3. Application Layer: Distance sorting + pagination + result caching. Typical business flow: User opens “Cafés Nearby” → Retrieve user location → $geoNear search within 3 km → Sort by distance → Return the top 20 results. Key performance factors: 2dsphere index + maxDistance limit + limit control over the number of results returned.
LBS Caching Strategy: Caching for LBS queries is more complex than for regular queries—because locations are continuous, and search results for adjacent locations overlap significantly. Caching strategy: 1. Geogrid caching (dividing the map into 1 km × 1 km grids, caching search results for each grid, and returning cached results for queries within the same grid); 2. User-based caching (caches results for “locations the user recently searched,” with a 5-minute TTL); 3. Popular area caching (pre-computes results for high-frequency search areas such as city centers, while performing real-time queries for less popular areas). Note: Cache expiration—when store information changes, the cache for the affected grid must be cleared.
Choosing Between 2d and 2dsphere Indexes: MongoDB offers two types of geospatial indexes—2d (planar coordinates, suitable for flat maps and game scenes, stored using legacy coordinate pairs [x, y]) and 2dsphere (spherical coordinates, suitable for real-world Earth coordinates, stored using GeoJSON Point {type: 'Point', coordinates: [lng, lat]}). 99% of LBS scenarios should use 2dsphere— 1. Supports true spherical distance calculations (2d uses Euclidean distance, which results in significant errors at high latitudes); 2. Supports all geospatial operators such as $geoNear, $geoWithin, and $near (2d only supports partial functionality of $near); 3. Supports multiple GeoJSON shapes (Point/LineString/Polygon). 2d should only be used for purely planar scenarios such as game maps.
(1) Create a 2dsphere index
// === 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) Geographic Queries
Key Points on the GeoJSON Coordinate Format: MongoDB geospatial queries use the GeoJSON format—type: 'Point' + coordinates: [lng, lat]. Two common errors: 1. Reversed longitude and latitude order (Google Maps uses lat/lng, while MongoDB uses lng/lat; be sure to note this); 2. Coordinate values are out of range (longitude -180 to 180, latitude -90 to 90). The default distance unit for $geoNear is meters (when spherical: true); maxDistance: 5000 indicates a range of 5 kilometers.
$centerSphere Radians Conversion: The radius units for $geoWithin and $centerSphere are radians—1 radian ≈ 6,378.1 kilometers (Earth’s equatorial radius). Conversion formula: radians = kilometers / 6378.1. Example: 5 kilometers = 5/6378.1 ≈ 0.000784 radians. This conversion is prone to errors, so it is recommended to encapsulate it as a utility function: kmToRadians(km) { return km / 6378.1; }.
// === $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
}
}
});
Performance Characteristics of $geoWithin: $geoWithin does not sort or calculate distances—it only determines whether a point is within a region, so it is much faster than $geoNear. $geoWithin is suitable for statistical queries asking “how many are within a region” (e.g., “stores within the delivery area”), while $geoNear is suitable for sorted queries asking “the closest ones” (e.g., “the 5 nearest coffee shops”) . $geoWithin supports three types of area shapes: $centerSphere (circle), $polygon (polygon), and $box (rectangle).
Choosing Between $geoWithin and $geoNear: Which operator to choose depends on business requirements—1. Need “the N closest results” → $geoNear (sorted by distance + limit); 2. Need “all results within an area” → $geoWithin (no sorting, better performance); 3. Need distance values → $geoNear (outputs distanceField); 4. Need to combine with $facet → $geoWithin ($geoNear cannot be used within $facet because it must be the first stage in the pipeline); 5. Polygon area queries → $geoWithin + $polygon ($geoNear only supports circular areas). Use $geoWithin for delivery areas (irregular polygons) and $geoNear for nearby searches (circular areas).
Caching Strategy for the LBS System: LBS query results can be cached—search results for a user at the same location remain unchanged for a short period of time—1. Cache key design: lbs:{lat}:{lng}:{radius}:{category}:{keyword}, encode all query parameters into the cache key; 2. Cache Granularity: Latitude and longitude are retained to 3 decimal places (approximately 110-meter accuracy); users within the same grid share the cache; 3. TTL Setting: 3–5 minutes (store locations remain unchanged in the short term, but newly opened stores need to be displayed promptly); 4. Cache Pre-loading: Search results for popular commercial districts (such as Sanlitun in Beijing and Nanjing Road in Shanghai) are cached in advance; 5. Expiration Strategy: When new stores go live, the cache for the corresponding area is cleared. LBS caching can reduce database queries by over 80% and is the primary method for optimizing LBS system performance.
Quick Reference for Radians Conversion: The distance parameters for $centerSphere and $nearSphere use radians—km to radians = km / 6378.1; miles to radians = miles / 3963.2. Common conversions: 1 km ≈ 0.00015696 radians, 5 km ≈ 0.0007848 radians, 10 km ≈ 0.00157 radians. The maxDistance parameter for $geoNear uses meters (no conversion needed), which is one of the reasons why $geoNear is easier to use than $nearSphere.
Accuracy of Distance Calculations in LBS Systems: MongoDB geospatial queries use spherical geometry (approximated by the WGS84 ellipsoid) by default—accuracy is highest near the equator (error < 0.5%), and while slightly lower at high latitudes, it is still far superior to calculations using planar geometry. The distance unit for 2dsphere indexes is meters, and the distanceField output of $geoNear is also in meters. Common accuracy considerations: 1. Accuracy is sufficient for short distances (< 100 m) (error < 1 m); 2. For long distances (> 1,000 km), the error may reach tens of meters (due to spherical approximation error); 3. Accuracy is lowest for cross-polar queries (though such scenarios are extremely rare). For 99% of LBS applications (finding nearby restaurants or stores), MongoDB’s distance accuracy fully meets requirements.
6. Comprehensive Practical Training
Concept Overview: This comprehensive hands-on exercise combines $facet, $bucket, text search, and geospatial queries to build a complete e-commerce search system. A single $facet query returns the search results list, total count, price buckets, and category statistics simultaneously, allowing the front end to render the search page directly. Geospatial queries provide support for LBS (Location-Based Service) scenarios.
How It Works: The Search API’s $facet query takes $match (text search + category filtering) as its common input, and four sub-pipelines process it in parallel: items (paginated list), total (total count), facets (price buckets), and categories (category statistics). Geospatial queries are executed independently and return results sorted by distance.
Key Points for Search API Architecture Design: The core challenge of a comprehensive search API is “returning data across all dimensions in a single request”—the front end requires a list (to render search results), a total count (for pagination), bucketing (for price/rating filters), and categorization (for sidebar navigation). $facet enables these four dimensions to be computed in parallel on a single input, avoiding four separate queries. However, note that the $match preceding $facet must satisfy both text search and business filters (such as categories and price ranges); otherwise, the inputs seen by each sub-pipeline will be inconsistent.
Search Results Sorting Strategies: The sorting of search results directly impacts the user experience—sorting strategies should be selected based on the context: 1. Relevance sorting ($meta: 'textScore'): When users search for keywords, they expect the most relevant results to appear at the top; 2. Distance sorting ($geoNear): When searching for nearby stores, users expect the closest ones to appear at the top; 3. Price sorting: When comparing prices, users expect the lowest or highest prices to appear at the top; 4. Sales Volume Sorting: When making a purchase, users expect popular items to appear at the top; 5. Comprehensive Sorting (Weighted Formula): $addFields calculates a composite score = 0.4 × Relevance + 0.3 × Sales Volume + 0.2 × Rating + 0.1 × Freshness. Comprehensive sorting is the ultimate solution for e-commerce search.
User Experience Design for Search Systems: Search is more than just “entering keywords and returning results”—a complete search experience includes: 1. Search suggestions (autocomplete as you type); 2. Search results (list + sorting); 3. Faceted filtering (categories/price/brand filters); 4. Result statistics (total count/distribution by category); 5. Handling empty results (recommending popular items or suggesting keyword modifications). MongoDB’s $facet can provide data for items 2, 3, and 4 in a single query, while search suggestions and handling empty results require additional queries and business logic.
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) Product Search + Faceted Statistics
The Business Value of Faceted Search: Faceted search is the core interaction model for e-commerce search—after a user enters a keyword, the page simultaneously displays search results and filtering criteria across various dimensions (price range, category, brand, rating). When a user clicks any filter, the search results are immediately narrowed down, and the filter counts are updated in real time. This iterative “search → filter → search again” experience is 10 times more efficient than the traditional “search → paginate → search again” approach. $facet is the best tool for implementing faceted search—it returns statistics across all dimensions in a single query, eliminating the need for multiple requests.
Search API Parameter Design: The search API parameters must cover all filter dimensions—1. Keyword (q): $text—the search query; 2. Category (category): Exact match of the “category” field; 3. Price Range (minPrice/maxPrice): $gte/$lte to filter the price field; 4. Brand (brand): Exact match or $in for multiple selections; 5. Rating (minRating): $gte to filter the rating field; 6. Pagination (page/limit): $skip/$limit to control the number of results; 7. Sorting (sort): Sort by relevance, price, rating, or sales volume. Parameter Design Principles: All filter parameters are optional (returns all results when no parameters are provided); pagination parameters have default values (page=1, limit=20); sorting has a default option (sort=relevance).
// === 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) Search for nearby stores
// === 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);
});
▶ Example 1: Practical Application of $facet Multi-Dimensional Search + Nearby Stores by Location
// 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
Output: Scenario 1 returns multidimensional results (list + total count + price buckets + categories); Scenario 2 returns a list of products sorted by distance.
▶ Example 2: ShopHub Multi-Dimensional Product Search API
// 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}]
// }
Output: A single $facet query returns a search list (sorted by relevance), the total count, a price distribution by bin, and category statistics, allowing the front end to render the search page directly.
❓ FAQ
Explanation of Common Questions: These four questions correspond to the core limitations of four key areas—the memory limit of $facet, the language limit of text search, the coordinate conventions for geospatial queries, and the algorithmic limit of $bucketAuto. Each limitation has its own technical reasons and corresponding strategies. Understanding these limitations is more important than simply memorizing the answers—for example, knowing that MongoDB’s text search does not support Chinese word segmentation allows you to incorporate Elasticsearch into your architecture design from the outset.
[longitude, latitude] (Note: Longitude comes first).📖 Summary
- $facet executes multiple independent pipelines in parallel and returns multidimensional results
- $bucket: Custom bucket assignment; $bucketAuto: Automatic bucket assignment
- Text index: text + weighted + $text search
- Geospatial: 2dsphere + $geoNear + $geoWithin + $nearSphere
Integration of the Four Major Themes: $facet + $bucket extends aggregation capabilities—allowing search result pages to return data across all dimensions at once; text search and geospatial queries expand query capabilities—from exact matches to fuzzy search, and from attribute queries to spatial queries. Combined, these four capabilities form a complete e-commerce search system—keyword search + faceted filtering + price bucketing + nearby stores—covering 90% of search scenarios.
Technical Selection Decisions for Search Systems: Choosing Between MongoDB Native Search and Elasticsearch—1. MongoDB is suitable for: small projects (< 100,000 documents), simple search logic (keyword + category + price filtering), and situations where you do not want to introduce new infrastructure; 2. Elasticsearch is suitable for: large projects (> 1 million documents), scenarios requiring Chinese word segmentation, pinyin search, and synonym expansion, complex ranking algorithms (BM25 + custom boosts), and near-real-time indexing (millisecond-level updates); 3. Hybrid Approach: Data is stored in MongoDB and synchronized to Elasticsearch via Change Stream, with queries run on Elasticsearch. This hybrid approach combines MongoDB’s write performance with Elasticsearch’s search capabilities—it is the most common architecture in production environments.
Design Patterns for $facet Subpipelines: There are three design patterns for $facet subpipelines—1. Independent subpipelines (most common): Each subpipeline independently processes the entire dataset, with no dependencies between them (e.g., the items subpipeline returns a list, the total subpipeline returns a count, and the facets subpipeline returns buckets); 2. Shared $match: All sub-pipelines share the filtering results from the $match stage preceding $facet, avoiding duplicate filtering; 3. Nested $facet (not recommended): $facet nested within $facet, which results in complex logic and doubles memory consumption. Pattern 2 is the best practice—apply common filtering upfront (such as by category, keyword, or price range), and have sub-pipelines perform only their respective statistical logic.
📝 Exercises
- Basic Question (⭐): Create a text index (title + description) for the
productscollection and perform a $text search. - Basic Problem (⭐): Use $bucket to count the number of items by price range.
- Advanced Problem (⭐⭐): Implement a complete search API (text search + bucket-based statistics + category-based statistics).
- Advanced Exercise (⭐⭐): Implement a nearby store search (2dsphere + $nearSphere).
- Challenge Question (⭐⭐⭐): Integrated Search System (Text + Geography + Faceted Statistics).
Recommendations for Implementing the Challenge: The comprehensive search system is the most complex challenge in this course—it requires combining four capabilities: $text (text search), $facet (multi-dimensional statistics), $bucket (price bucketing), and $geoNear (distance sorting). We recommend a step-by-step implementation: 1. First, implement $text search and the results list; 2. Then add $facet for multidimensional statistics; 3. Next, add $bucket for price bucketing; 4. finally, add $geoNear proximity search. Proceed to the next step only after each step has been verified. Note that $text and $geoNear cannot be used within the same $match—$text must be the first condition in $match, and $geoNear must be the first stage in the pipeline. Solution: Use $facet to run the two searches in parallel, or perform $text first, followed by $geoNear.
Key Points for Deploying a Comprehensive Search System: The educational version of the challenge is still a few steps away from production—1. Input validation: All query parameters must be validated (q length 1–200, category enumeration values, price range 0–999999, coordinate format validation); 2. Default Sorting: Sort by sales volume or rating (not random order) when no keywords are provided; sort by relevance when keywords are provided; 3. Result Caching: Cache results for the same query parameters for 5 minutes (key = hash(params)) to reduce database load; 4. Timeout Protection: maxTimeMS: 5000 limits aggregation execution time; if a timeout occurs, partial results are returned along with a prompt stating "Results may be incomplete"; 5. Slow Query Monitoring: Search queries with execution times exceeding 1 second are logged to optimize indexes and pipelines. These points represent the key gaps between the search system’s transition from demo to production.



