Simple equality filters — { published: true } — cover only the most basic query needs. Real-world MERN applications require richer queries: posts with more than 100 views, posts created this week, posts tagged with either “react” or “mongodb”, posts whose titles match a search term, users who have not verified their email yet. MongoDB’s query operator system — comparison, logical, element, array, and text operators — gives you the tools to express any query you can imagine. This lesson covers the complete toolkit and shows how to combine operators into the complex filters your MERN Blog API needs.
Query Operator Categories
| Category | Operators | Used For |
|---|---|---|
| Comparison | $eq $ne $gt $gte $lt $lte $in $nin |
Numeric ranges, exact matches, set membership |
| Logical | $and $or $nor $not |
Combining multiple conditions |
| Array | $all $elemMatch $size |
Array field queries |
| Element | $exists $type |
Field existence and type checking |
| Evaluation | $regex $text $where |
Pattern matching, full-text search |
find(), findOne(), or countDocuments(). There is no Mongoose-specific query syntax for filters. Post.find({ viewCount: { $gt: 100 } }) in Mongoose produces exactly the same MongoDB query as db.posts.find({ viewCount: { $gt: 100 } }) in mongosh.const filter = {}; if (req.query.tag) filter.tags = req.query.tag; if (req.query.search) filter.$text = { $search: req.query.search };$where operator allows you to write arbitrary JavaScript inside a MongoDB query — it is convenient but extremely slow (no index use, full collection scan) and a security risk if any user input reaches it. Never use $where in a MERN application. Use standard comparison operators or $regex instead.Comparison Operators
// ── $eq — equal to (same as field: value shorthand) ───────────────────────────
Post.find({ published: { $eq: true } });
Post.find({ published: true }); // shorthand — identical
// ── $ne — not equal ────────────────────────────────────────────────────────────
Post.find({ status: { $ne: 'archived' } }); // all non-archived posts
// ── $gt / $gte / $lt / $lte — numeric comparisons ────────────────────────────
Post.find({ viewCount: { $gt: 100 } }); // more than 100 views
Post.find({ viewCount: { $gte: 100 } }); // 100 or more views
Post.find({ viewCount: { $lt: 10 } }); // fewer than 10 views
Post.find({ viewCount: { $gte: 10, $lte: 100 } }); // between 10 and 100
// ── $gt / $lt with Date ────────────────────────────────────────────────────────
const last7Days = new Date(Date.now() - 7 * 24 * 60 * 60 * 1000);
Post.find({ createdAt: { $gte: last7Days } }); // posts from the last week
// ── $in — value is in a list ──────────────────────────────────────────────────
Post.find({ status: { $in: ['draft', 'published'] } }); // either status
Post.find({ _id: { $in: [id1, id2, id3] } }); // any of these IDs
// ── $nin — value is NOT in a list ─────────────────────────────────────────────
Post.find({ status: { $nin: ['archived', 'deleted'] } }); // exclude these
Logical Operators
// ── $and — all conditions must match ──────────────────────────────────────────
// Note: multiple conditions at the same level are AND by default
Post.find({ published: true, featured: true });
// Equivalent explicit $and:
Post.find({ $and: [{ published: true }, { featured: true }] });
// Use explicit $and when applying multiple operators to the same field:
Post.find({ $and: [
{ viewCount: { $gte: 100 } },
{ viewCount: { $lte: 1000 } },
{ createdAt: { $gte: new Date('2025-01-01') } },
] });
// ── $or — at least one condition must match ───────────────────────────────────
Post.find({ $or: [
{ tags: 'mern' },
{ tags: 'react' },
{ featured: true },
] }); // posts tagged mern OR react OR featured
// ── $nor — none of the conditions match ──────────────────────────────────────
Post.find({ $nor: [
{ published: false },
{ deletedAt: { $ne: null } },
] }); // documents that are NOT unpublished AND NOT deleted
// ── $not — negates an operator expression ─────────────────────────────────────
Post.find({ viewCount: { $not: { $gt: 1000 } } });
// Same as: Post.find({ viewCount: { $lte: 1000 } })
Array Operators
// Single element in array — MongoDB searches inside arrays automatically
Post.find({ tags: 'mern' }); // posts where tags array contains 'mern'
// ── $all — array contains ALL specified values ─────────────────────────────────
Post.find({ tags: { $all: ['mern', 'javascript', 'beginner'] } });
// posts tagged with ALL three tags
// ── $in — array contains ANY of the specified values ──────────────────────────
Post.find({ tags: { $in: ['mern', 'react', 'mongodb'] } });
// posts tagged with ANY of these
// ── $size — array has exactly N elements ──────────────────────────────────────
Post.find({ tags: { $size: 0 } }); // posts with no tags
Post.find({ tags: { $size: 3 } }); // posts with exactly 3 tags
// ── $elemMatch — at least one array element matches ALL conditions ─────────────
// Useful for arrays of objects
User.find({
addresses: {
$elemMatch: { city: 'London', primary: true },
},
}); // users who have a primary address in London
Element Operators
// ── $exists — field exists (or doesn't exist) ─────────────────────────────────
Post.find({ publishedAt: { $exists: true } }); // has a publishedAt field
Post.find({ excerpt: { $exists: false } }); // no excerpt field
// ── $type — field is of a specific BSON type ──────────────────────────────────
Post.find({ viewCount: { $type: 'number' } }); // viewCount is a number
Post.find({ tags: { $type: 'array' } }); // tags is an array
// Type names: 'string', 'number', 'bool', 'date', 'null', 'array', 'objectId'
Evaluation Operators — Text Search and Regex
// ── $regex — regular expression pattern matching ───────────────────────────────
Post.find({ title: { $regex: /mern/i } }); // case-insensitive contains
Post.find({ title: { $regex: '^Getting', $options: 'i' } }); // starts with 'Getting'
Post.find({ email: { $regex: '@gmail\.com$' } }); // ends with @gmail.com
// ── $text — full-text search (requires a text index) ──────────────────────────
// First, create a text index:
// postSchema.index({ title: 'text', body: 'text' })
Post.find({ $text: { $search: 'mern stack tutorial' } })
.sort({ score: { $meta: 'textScore' } }); // sort by relevance score
// $text with language-aware stemming:
Post.find({ $text: { $search: 'running', $language: 'en' } });
// Matches: 'run', 'running', 'runner' — stemming is applied
Building a Dynamic Filter — Blog Post Search
// server/src/controllers/postController.js
const buildPostFilter = (query) => {
const filter = { published: true }; // base filter — always applied
// Tag filter
if (query.tag) {
filter.tags = query.tag;
}
// Author filter
if (query.author) {
filter.author = query.author;
}
// Text search
if (query.search) {
filter.$text = { $search: query.search };
}
// Date range filter
if (query.from || query.to) {
filter.createdAt = {};
if (query.from) filter.createdAt.$gte = new Date(query.from);
if (query.to) filter.createdAt.$lte = new Date(query.to);
}
// View count range
if (query.minViews) {
filter.viewCount = { $gte: parseInt(query.minViews, 10) };
}
return filter;
};
// Usage in controller:
const filter = buildPostFilter(req.query);
const posts = await Post.find(filter).sort({ createdAt: -1 }).limit(10);
Common Mistakes
Mistake 1 — Using $regex for full-text search on large collections
❌ Wrong — regex queries on large text fields without a text index are very slow:
Post.find({ body: { $regex: req.query.search, $options: 'i' } });
// Full collection scan on every search — milliseconds become seconds at scale
✅ Correct — use a text index and the $text operator for search functionality:
// Schema: postSchema.index({ title: 'text', body: 'text' })
Post.find({ $text: { $search: req.query.search } }); // uses the text index ✓
Mistake 2 — Forgetting that $in with ObjectIds needs proper types
❌ Wrong — passing string IDs to $in for an ObjectId field:
const ids = ['64a1f2b3...', '64a1f2b4...'];
Post.find({ _id: { $in: ids } }); // may return 0 results if schema expects ObjectId
✅ Correct — convert strings to ObjectIds, or let Mongoose handle it via proper schema types:
const { Types } = require('mongoose');
const ids = rawIds.map(id => new Types.ObjectId(id));
Post.find({ _id: { $in: ids } }); // ✓
Mistake 3 — Applying $or incorrectly for date range queries on the same field
❌ Wrong — using $or for a date range (should be $and or combined conditions):
Post.find({ $or: [
{ createdAt: { $gte: startDate } },
{ createdAt: { $lte: endDate } },
] }); // returns posts BEFORE endDate OR AFTER startDate — not a range!
✅ Correct — combine range conditions on the same field in one object:
Post.find({ createdAt: { $gte: startDate, $lte: endDate } }); // ✓ proper range
Quick Reference
| Goal | Operator / Pattern |
|---|---|
| Greater than | { field: { $gt: value } } |
| Less than or equal | { field: { $lte: value } } |
| In a set | { field: { $in: [a, b, c] } } |
| Not in a set | { field: { $nin: [a, b] } } |
| Either condition | { $or: [{ cond1 }, { cond2 }] } |
| Array contains value | { tags: 'mern' } |
| Array contains all | { tags: { $all: ['a','b'] } } |
| Array contains any | { tags: { $in: ['a','b'] } } |
| Field exists | { field: { $exists: true } } |
| Text search | { $text: { $search: 'query' } } |
| Regex match | { field: { $regex: /pattern/i } } |
| Date range | { createdAt: { $gte: from, $lte: to } } |