The most surprising thing about CouchDB’s Mango query syntax is that it’s not really a query language at all; it’s a JSON representation of a declarative filter that CouchDB interprets into an efficient lookup.
Let’s see this in action. Imagine you have a products database with documents like this:
{
"_id": "abc-123",
"type": "product",
"name": "Super Widget",
"price": 19.99,
"tags": ["electronics", "gadget"],
"stock": 50
}
You want to find all products that are either "electronics" or "gadgets" and have a price less than $20. In Mango, this looks like:
{
"selector": {
"$or": [
{"tags": "electronics"},
{"tags": "gadget"}
],
"price": {"$lt": 20.00}
}
}
When you POST this to your _find endpoint (e.g., POST /products/_find), CouchDB doesn’t parse this as a SQL-like query. Instead, it builds an index based on this selector. If you don’t have an index that can satisfy this query, CouchDB will suggest one, or you can create it manually:
curl -X POST \
http://localhost:5984/products/_index \
-H 'Content-Type: application/json' \
-d '{
"index": {
"fields": ["price", "tags"]
},
"ddoc": "product_finder"
}'
This creates a design document named _design/product_finder with an index on price and tags. CouchDB then uses this index to efficiently scan only the relevant documents, rather than a full table scan. The selector itself is the blueprint for how CouchDB should use its indexes.
The core of Mango is the selector object. It’s a JSON document that describes the criteria for matching documents. You can use logical operators like $and (implicit by default when you list multiple conditions), $or, and $nor, as well as comparison operators like $eq (implicit equality), $gt (greater than), $gte (greater than or equal to), $lt (less than), $lte (less than or equal to), $ne (not equal to), $exists, and $type.
For array fields, you can query for the presence of a specific element (like {"tags": "electronics"} which implicitly uses $eq) or for conditions that apply to any element in the array (using $elemMatch). For example, to find products where at least one tag is "electronics" and at least one tag is "gadget", you’d use:
{
"selector": {
"tags": {
"$elemMatch": {
"$and": [
{"$eq": "electronics"},
{"$eq": "gadget"}
]
}
}
}
}
This is different from the first example where {"tags": "electronics"} means "does the tags array contain the value 'electronics'?"
The $exists operator is particularly useful. {"stock": {"$exists": true}} will find all documents that have a stock field, regardless of its value. Conversely, {"stock": {"$exists": false}} finds documents without a stock field.
When you specify multiple top-level keys in your selector, CouchDB implicitly treats them as an $and. So, {"selector": {"price": {"$lt": 20.00}, "stock": {"$gt": 0}}} is equivalent to {"selector": {"$and": [{"price": {"$lt": 20.00}}, {"stock": {"$gt": 0}}]}}. This implicit $and is crucial for building efficient queries because CouchDB can often satisfy such queries by using a composite index.
The fields option in the _find request allows you to specify which fields to return, and sort lets you order the results. Combining these with the selector gives you a powerful, declarative way to retrieve precisely the data you need.
What most people don’t realize is how CouchDB translates {"field": value} into {"field": {"$eq": value}}. This implicit equality is convenient but can be a source of confusion when you start using other operators. If you have a selector like {"price": 19.99, "price": {"$lt": 20.00}}, it’s actually invalid JSON. However, if you had {"price": 19.99, "stock": 50}, CouchDB interprets this as {"price": {"$eq": 19.99}, "stock": {"$eq": 50}}. The implicit $eq is applied to each field independently before any explicit operators are considered.
The next step in mastering CouchDB queries is understanding how to leverage query functions within design documents for more complex logic that goes beyond what Mango selectors can express directly.