Queries & Operators
The query system is one of the most important parts of file-json-db. It lets you express filters in a MongoDB-like language. This page explains every operator in detail, including when to use it and real-world examples.
How a query works
When you call collection.find(query) or findOne(query),
the library walks every document (or uses an index when possible) and asks:
“Does this document match the given query object?”
The matching is performed by the internal function matchQuery(doc, query)
(you can see it in the source). It supports:
- Simple equality
- Comparison operators (
$eq,$gt, …) - Logical operators (
$and,$or,$nor) - Array and existence operators
- Regular expressions
- Nested field access via dot notation
Simple equality
The simplest query is a plain object whose keys are field names and whose values are the exact values you want:
// Find the user whose email is exactly this string
users.findOne({ email: 'alice@acme.dev' });
// Find all issues that are currently open
issues.find({ status: 'open' }).toArray();
// Empty query matches every document
const all = users.find({}).toArray();
Under the hood this is equivalent to { email: { $eq: 'alice@acme.dev' } }.
Nested fields (dot notation)
You can reach into nested objects with a dot:
// Document shape:
// { profile: { address: { city: 'Lagos', country: 'NG' } } }
users.find({ 'profile.address.city': 'Lagos' }).toArray();
users.findOne({ 'profile.address.country': 'NG' });
The helper getNestedValue(doc, 'profile.address.city') walks the path
safely and returns undefined if any intermediate key is missing.
Comparison operators
$eq – equal
Matches documents where the field is strictly equal to the given value (using JavaScript ===).
users.find({ age: { $eq: 25 } }).toArray();
// same as
users.find({ age: 25 }).toArray();
$ne – not equal
Matches documents where the field is not equal to the value.
// Everyone except the admin
users.find({ role: { $ne: 'admin' } }).toArray();
$gt / $gte – greater than / greater-or-equal
// Users older than 30
users.find({ age: { $gt: 30 } }).toArray();
// Orders with total >= 100
orders.find({ total: { $gte: 100 } }).toArray();
$lt / $lte – less than / less-or-equal
// Products cheaper than $20
products.find({ price: { $lt: 20 } }).toArray();
// Tasks due on or before today
tasks.find({ dueDate: { $lte: '2026-10-12' } }).toArray();
Combining comparisons on the same field
You can put several operators on one field:
// Age between 18 and 65 (inclusive)
users.find({
age: { $gte: 18, $lte: 65 }
}).toArray();
Array membership operators
$in – value is one of several
Matches if the field’s value appears in the provided array. Extremely useful for “status is open or in-progress”.
// Issues that are still active
issues.find({
status: { $in: ['open', 'in-progress', 'review'] }
}).toArray();
// Users with one of these roles
users.find({
role: { $in: ['admin', 'moderator'] }
}).toArray();
$nin – value is not in the list
// Everything except closed or cancelled
issues.find({
status: { $nin: ['closed', 'cancelled'] }
}).toArray();
Existence and type
$exists
Checks whether a field is present on the document (i.e. not undefined).
Note that a field set to null is still considered to exist.
// Users who have a phone number recorded
users.find({ phone: { $exists: true } }).toArray();
// Users who never set a phone number
users.find({ phone: { $exists: false } }).toArray();
$type
Matches the JavaScript-style type of the value.
Possible values: 'string', 'number', 'boolean',
'object', 'array', 'null'.
// Documents where age is stored as a number (not a string)
users.find({ age: { $type: 'number' } }).toArray();
// Tags field is an array
posts.find({ tags: { $type: 'array' } }).toArray();
Regular expressions – $regex
Matches a string field against a regular expression.
You can pass either a string or a real RegExp object.
Optional flags are supplied via $options.
// Usernames that start with "admin"
users.find({
username: { $regex: '^admin' }
}).toArray();
// Case-insensitive search for "error" in the title
issues.find({
title: { $regex: 'error', $options: 'i' }
}).toArray();
// Using a real RegExp
users.find({
email: { $regex: /@acme\.dev$/i }
}).toArray();
Array-specific operators
$size
Matches arrays that have exactly the given length.
// Posts that have exactly 3 tags
posts.find({ tags: { $size: 3 } }).toArray();
$elemMatch
Matches documents where at least one element of an array satisfies the given sub-query. Useful when the array contains objects.
// Orders that contain at least one item with quantity > 5
orders.find({
items: {
$elemMatch: { quantity: { $gt: 5 } }
}
}).toArray();
// Users who have a tag that is exactly "admin"
users.find({
tags: { $elemMatch: { $eq: 'admin' } }
}).toArray();
Logical operators
$and
All of the sub-queries must match. You rarely need to write $and
explicitly because top-level keys are already ANDed together, but it is useful
when you need multiple conditions on the same field or nested logic.
users.find({
$and: [
{ age: { $gte: 18 } },
{ age: { $lte: 65 } },
{ active: true }
]
}).toArray();
$or
At least one of the sub-queries must match.
// High-priority issues OR issues assigned to the current user
issues.find({
$or: [
{ priority: 'high' },
{ assigneeId: currentUserId }
]
}).toArray();
$nor
None of the sub-queries may match (logical NOR).
// Issues that are neither closed nor cancelled
issues.find({
$nor: [
{ status: 'closed' },
{ status: 'cancelled' }
]
}).toArray();
Real-world example: filtering a product catalog
const products = db.collection({
name: 'products',
autoId: true,
indexes: ['category', 'sku', 'brand']
});
// Complex storefront filter:
// - category is "electronics" or "computers"
// - price between 50 and 500
// - still in stock
// - brand is not "Generic"
// - name contains "wireless" (case-insensitive)
const results = products.find({
category: { $in: ['electronics', 'computers'] },
price: { $gte: 50, $lte: 500 },
stock: { $gt: 0 },
brand: { $ne: 'Generic' },
name: { $regex: 'wireless', $options: 'i' }
}).sort({ price: 1 }).limit(20).toArray();
Performance notes
- When the query is a simple equality on a field that has a secondary index and the query contains only that one field, the library uses the index and avoids a full scan.
-
Any other combination (multiple fields, range operators,
$or, etc.) currently falls back to a full scan of the relevant parts. - For large collections, keep the most selective filters on indexed fields and try to put them first when possible.