Модуль 2 · Язык фильтров

Справочник по MongoDB · модуль 2 · язык фильтров

2.1

2.1

Фильтр — это документ.
Точечная нотация

В модуле 1 фильтр встречался как «документ с условиями» и почти везде состоял из одной пары. Здесь разобрано, что происходит, когда пар несколько, почему совпадение проверяется по типу значения, и как добраться до поля, лежащего внутри другого документа.

Категория
Язык запросов
Уровень
Начальный
Связанные темы
Чтение и курсор, операторы сравнения, $match в агрегации
Зачем это знать
Писать условия отбора и понимать, почему запрос вернул пусто

Как запускать примеры этой главы

БазыГлава только читает данные — сбрасывать базы не нужно.
Архивmongodb-glava-2.1-python.zip, 60 КБmongodb-glava-2.1-ruby.zip, 60 КБmongodb-glava-2.1-go.zip, 65 КБmongodb-glava-2.1-cpp.zip, 63 КБ — все примеры главы готовыми программами, базы и стенд в Docker. В README.md — запуск в Docker, если Compass нет или он не подключается, и на установленном сервере с Compass, а также что должна напечатать каждая программа.
Файлlessons/ch2_1.py — пример вставляется в конец файлаlessons/ch2_1.rb — пример вставляется в конец файлаlessons/ch2_1.go — пример внутрь скобок в конце main, функции и типы — выше mainlessons/ch2_1.cpp — пример внутрь скобок в конце main, функции — выше main
Запускcd lessons, затем python ch2_1.pycd lessons, затем python3 ch2_1.py · в Docker: docker compose run --rm python lessons/ch2_1.pycd lessons, затем ruby ch2_1.rb · в Docker: docker compose run --rm ruby lessons/ch2_1.rbcd lessons, затем go run ch2_1.go · в Docker: docker compose run --rm go lessons/ch2_1.gocd lessons, затем g++ -std=c++17 ch2_1.cpp -o prog $(pkg-config --cflags --libs libmongocxx1) && ./prog · в Docker: docker compose run --rm cpp lessons/ch2_1.cpp
ПорядокКаждый пример вставляется вместо предыдущего и запускается один раз, по порядку, — так получены все выводы главы. Подробно — глава 1.0, §10.

§1База hh: четыре коллекции

Модуль 1 целиком прошёл на базе shop: товары, заказы, покупатели. Модуль 2 работает на базе hh — это данные сервиса по найму. Она подходит для языка фильтров тем, что документы в ней устроены сложнее: есть вложенные документы, массивы строк и массивы документов, а часть полей заполнена не у всех записей.

В базе четыре коллекции:

КоллекцияСколькоЧто внутри
resumes9Резюме соискателей: город, зарплата, навыки, образование, опыт
vacancies8Вакансии: название, компания, город, вилка зарплаты
companies8Компании: отрасль, город, численность
interviews60Собеседования: кандидат, дата, формат, этап, компания
Compass, база hh: список из четырёх коллекций, в столбце Documents — 8, 60, 9 и 8 документов
Compass · база hh: те же четыре коллекции и те же количества документов, что в таблице выше

Столбец Documents повторяет второй столбец таблицы, а Avg. document size показывает средний размер документа: у resumes он 538 байт против 145 у companies. Остальные столбцы — размер данных на диске и число индексов; для фильтров они не нужны.

В резюме полей больше, чем в документах остальных трёх коллекций. Ниже приведено одно резюме целиком — примеры главы обращаются к этим полям.

mongosh · оболочкаодно резюме
hh> db.resumes.findOne({ fio: "Пётр Ковалёв" })
Вывод в терминалрезультат запуска
{
  _id: ObjectId('6a9e46fe2be8a88bb5e50f30'),
  fio: 'Пётр Ковалёв',
  position: 'Junior Python-разработчик',
  city: 'Ярославль',
  salary: 70000,
  ready_to_move: false,
  updated: ISODate('2026-09-01T09:12:00.000Z'),
  skills: [ 'Python', 'SQL', 'Git', 'Docker' ],
  education: { level: 'СПО', place: 'Ярославский колледж связи', year: 2026 },
  experience: [
    {
      company: 'ИП Сорокин',
      role: 'стажёр',
      months: 6,
      stack: [ 'Python', 'PostgreSQL' ]
    }
  ],
  contacts: { email: 'kovalev.p@example.ru', tg: '@kovalev_p' }
}
Compass, коллекция hh.resumes: документ Петра Ковалёва раскрыт целиком, видны массив skills, вложенные документы education и contacts, массив документов experience
Compass · то же резюме, что вернул findOne: вложенные документы и массивы раскрыты треугольниками слева

Compass показывает тот же документ, что и mongosh, только в своей раскраске: имена полей слева, значения справа, строки зелёным, числа синим. Поле _id раскрыто в $oid, а updated — в $date: так Compass показывает типы BSON, у которых нет записи в JSON.

В этом документе три вида полей, и для каждого фильтр пишется по-своему:

Простое значениеfio, city, salary, ready_to_move — строка, число, логическое значение.§2–§4
Вложенный документeducation и contacts: внутри поля лежит ещё один документ со своими полями.§5
Массивskills — массив строк, experience — массив документов.§6

Вакансия устроена проще, но и в ней есть вложенный документ: зарплата задана вилкой.

mongosh · оболочкаодна вакансия
hh> db.vacancies.findOne()
Вывод в терминалрезультат запуска
{
  _id: 'v-001',
  title: 'Junior Python-разработчик',
  company: 'Тензор',
  city: 'Ярославль',
  salary: { from: 60000, to: 90000 },
  published: ISODate('2026-08-28T09:00:00.000Z'),
  is_open: true
}

Поле salary в этих двух коллекциях устроено по-разному. В резюме это число — сколько человек хочет получать. В вакансии это документ с полями from и to — вилка, которую предлагает работодатель. Одно имя поля, разные типы значения в разных коллекциях: сервер это допускает, и в §4 разобрано, к чему это приводит в запросах.

§2Фильтр — это документ

Первый аргумент find называется фильтром. Это обычный документ — такой же, какие лежат в коллекции. В Python это словарь, в Ruby — хеш, в Go — bson.D, в C++ — документ, собранный через make_document.

Правило чтения такое: поле в фильтре означает «поле документа», значение — «должно быть равно этому». Документ попадает в результат, если совпадение есть.

Python · PyMongoрезюме из одного города
for doc in resumes.find({"city": "Ярославль"},
                     {"fio": 1, "position": 1, "_id": 0}):
    print(doc)
C++ · mongocxxрезюме из одного города
mongocxx::options::find options;
options.projection(make_document(kvp("fio", 1), kvp("position", 1), kvp("_id", 0)));

for (const auto& doc : resumes.find(make_document(kvp("city", "Ярославль")), options)) {
    std::cout << bsoncxx::to_json(doc) << std::endl;
}
Go · mongo-driverрезюме из одного города
cursor, _ := resumes.Find(ctx,
    bson.D{{Key: "city", Value: "Ярославль"}},
    options.Find().SetProjection(bson.D{
        {Key: "fio", Value: 1}, {Key: "position", Value: 1}, {Key: "_id", Value: 0}}))

for cursor.Next(ctx) {
    var doc bson.M
    cursor.Decode(&doc)
    fmt.Println(doc)
}
Ruby · mongoрезюме из одного города
resumes.find(
  { "city" => "Ярославль" },
  projection: { "fio" => 1, "position" => 1, "_id" => 0 }
).each { |doc| puts doc.inspect }
Вывод в терминалрезультат запуска
{'fio': 'Анна Белова', 'position': 'Junior Python-разработчик'}
{'fio': 'Пётр Ковалёв', 'position': 'Junior Python-разработчик'}
{'fio': 'Игорь Самойлов', 'position': 'Backend-разработчик C#'}
{'fio': 'Ксения Лапина', 'position': 'Тестировщик'}
{'fio': 'Ольга Пирогова', 'position': 'Аналитик данных'}
Вывод в терминалрезультат запуска
{ "fio" : "Анна Белова", "position" : "Junior Python-разработчик" }
{ "fio" : "Пётр Ковалёв", "position" : "Junior Python-разработчик" }
{ "fio" : "Игорь Самойлов", "position" : "Backend-разработчик C#" }
{ "fio" : "Ксения Лапина", "position" : "Тестировщик" }
{ "fio" : "Ольга Пирогова", "position" : "Аналитик данных" }
Вывод в терминалрезультат запуска
{"fio":"Анна Белова","position":"Junior Python-разработчик"}
{"fio":"Пётр Ковалёв","position":"Junior Python-разработчик"}
{"fio":"Игорь Самойлов","position":"Backend-разработчик C#"}
{"fio":"Ксения Лапина","position":"Тестировщик"}
{"fio":"Ольга Пирогова","position":"Аналитик данных"}
Вывод в терминалрезультат запуска
{"fio"=>"Анна Белова", "position"=>"Junior Python-разработчик"}
{"fio"=>"Пётр Ковалёв", "position"=>"Junior Python-разработчик"}
{"fio"=>"Игорь Самойлов", "position"=>"Backend-разработчик C#"}
{"fio"=>"Ксения Лапина", "position"=>"Тестировщик"}
{"fio"=>"Ольга Пирогова", "position"=>"Аналитик данных"}

Пять резюме из девяти. Проекция здесь только для краткости вывода — она разобрана в главе 1.5 и на отбор не влияет.

Почему фильтр устроен документом

Если бы условие задавалось строкой, эту строку пришлось бы склеивать из кусков, и значение, пришедшее от пользователя, могло бы попасть в неё как часть условия. В документе имя поля и значение передаются отдельно, и значение остаётся значением. Документ к тому же собирают по частям в коде: условия добавляют в словарь по мере того, как они становятся известны.

§3Несколько условий: все должны выполниться

В фильтре может быть сколько угодно пар. Сервер проверяет все и возвращает документ, только если выполнены все условия сразу. Отдельного слова для этого не нужно: перечисление через запятую уже означает «и».

Python · PyMongoгород и должность
filtr = {"city": "Ярославль",
         "position": "Junior Python-разработчик"}

print("город:", resumes.count_documents({"city": "Ярославль"}))
print("город и должность:", resumes.count_documents(filtr))

for doc in resumes.find(filtr, {"fio": 1, "salary": 1, "_id": 0}):
    print(doc)
C++ · mongocxxгород и должность
auto filtr = make_document(kvp("city", "Ярославль"),
                           kvp("position", "Junior Python-разработчик"));

std::cout << "город: " << resumes.count_documents(make_document(kvp("city", "Ярославль"))) << std::endl;
std::cout << "город и должность: " << resumes.count_documents(filtr.view()) << std::endl;

mongocxx::options::find options;
options.projection(make_document(kvp("fio", 1), kvp("salary", 1), kvp("_id", 0)));
for (const auto& doc : resumes.find(filtr.view(), options)) {
    std::cout << bsoncxx::to_json(doc) << std::endl;
}
Go · mongo-driverгород и должность
filtr := bson.D{
    {Key: "city", Value: "Ярославль"},
    {Key: "position", Value: "Junior Python-разработчик"}}

gorod, _ := resumes.CountDocuments(ctx, bson.D{{Key: "city", Value: "Ярославль"}})
oba, _ := resumes.CountDocuments(ctx, filtr)
fmt.Println("город:", gorod)
fmt.Println("город и должность:", oba)

cursor, _ := resumes.Find(ctx, filtr,
    options.Find().SetProjection(bson.D{
        {Key: "fio", Value: 1}, {Key: "salary", Value: 1}, {Key: "_id", Value: 0}}))
for cursor.Next(ctx) {
    var doc bson.M
    cursor.Decode(&doc)
    fmt.Println(doc)
}
Ruby · mongoгород и должность
filtr = { "city" => "Ярославль",
          "position" => "Junior Python-разработчик" }

puts "город: " + resumes.count_documents({ "city" => "Ярославль" }).to_s
puts "город и должность: " + resumes.count_documents(filtr).to_s

resumes.find(filtr, projection: { "fio" => 1, "salary" => 1, "_id" => 0 })
       .each { |doc| puts doc.inspect }
Вывод в терминалрезультат запуска
город: 5
город и должность: 2
{'fio': 'Анна Белова', 'salary': 65000}
{'fio': 'Пётр Ковалёв', 'salary': 70000}
Вывод в терминалрезультат запуска
город: 5
город и должность: 2
{ "fio" : "Анна Белова", "salary" : 65000 }
{ "fio" : "Пётр Ковалёв", "salary" : 70000 }
Вывод в терминалрезультат запуска
город: 5
город и должность: 2
{"fio":"Анна Белова","salary":{"$numberInt":"65000"}}
{"fio":"Пётр Ковалёв","salary":{"$numberInt":"70000"}}
Вывод в терминалрезультат запуска
город: 5
город и должность: 2
{"fio"=>"Анна Белова", "salary"=>65000}
{"fio"=>"Пётр Ковалёв", "salary"=>70000}
Compass, коллекция hh.resumes: фильтр с полями city и position, счётчик 1 – 2 of 2, найдены Анна Белова и Пётр Ковалёв
Compass · фильтр из двух пар: счётчик 1 – 2 of 2 над списком — те же два резюме, что напечатала программа

Второе условие сократило выборку с пяти резюме до двух: в Ярославле пять соискателей, но на должность Junior Python-разработчика из них претендуют двое. Каждое добавленное условие сужает выборку или оставляет её прежней.

Порядок пар в фильтре ни на что не влияет: {"city": …, "position": …} и {"position": …, "city": …} — один и тот же запрос. Сервер сам решает, в каком порядке проверять условия.

§4Значение должно совпасть точно

Сравнение в фильтре идёт по значению и типу BSON. Число 70000 и строка "70000" — разные значения, и документ, где зарплата записана числом, по строке не найдётся.

Python · PyMongoтип и регистр значения
print("salary 70000 числом: ", resumes.count_documents({"salary": 70000}))
print("salary '70000' строкой:", resumes.count_documents({"salary": "70000"}))
print("is_open True:          ", vacancies.count_documents({"is_open": True}))
print("is_open 'true':        ", vacancies.count_documents({"is_open": "true"}))
print("город с маленькой:     ", resumes.count_documents({"city": "ярославль"}))
C++ · mongocxxтип и регистр значения
std::cout << "salary 70000 числом:  " << resumes.count_documents(make_document(kvp("salary", 70000))) << std::endl;
std::cout << "salary '70000' строкой:" << resumes.count_documents(make_document(kvp("salary", "70000"))) << std::endl;
std::cout << "is_open true:          " << vacancies.count_documents(make_document(kvp("is_open", true))) << std::endl;
std::cout << "is_open \"true\":        " << vacancies.count_documents(make_document(kvp("is_open", "true"))) << std::endl;
std::cout << "город с маленькой:     " << resumes.count_documents(make_document(kvp("city", "ярославль"))) << std::endl;
Go · mongo-driverтип и регистр значения
chislom, _ := resumes.CountDocuments(ctx, bson.D{{Key: "salary", Value: 70000}})
strokoy, _ := resumes.CountDocuments(ctx, bson.D{{Key: "salary", Value: "70000"}})
istina, _ := vacancies.CountDocuments(ctx, bson.D{{Key: "is_open", Value: true}})
tekstom, _ := vacancies.CountDocuments(ctx, bson.D{{Key: "is_open", Value: "true"}})
malenkoy, _ := resumes.CountDocuments(ctx, bson.D{{Key: "city", Value: "ярославль"}})

fmt.Println("salary 70000 числом:  ", chislom)
fmt.Println("salary '70000' строкой:", strokoy)
fmt.Println("is_open true:          ", istina)
fmt.Println("is_open 'true':        ", tekstom)
fmt.Println("город с маленькой:     ", malenkoy)
Ruby · mongoтип и регистр значения
puts "salary 70000 числом:   " + resumes.count_documents({ "salary" => 70000 }).to_s
puts "salary '70000' строкой:" + resumes.count_documents({ "salary" => "70000" }).to_s
puts "is_open true:          " + vacancies.count_documents({ "is_open" => true }).to_s
puts "is_open 'true':        " + vacancies.count_documents({ "is_open" => "true" }).to_s
puts "город с маленькой:     " + resumes.count_documents({ "city" => "ярославль" }).to_s
Вывод в терминалрезультат запуска
salary 70000 числом:  1
salary '70000' строкой: 0
is_open True:           8
is_open 'true':         0
город с маленькой:      0
Вывод в терминалрезультат запуска
salary 70000 числом:  1
salary '70000' строкой:0
is_open true:          8
is_open "true":        0
город с маленькой:     0
Вывод в терминалрезультат запуска
salary 70000 числом:   1
salary '70000' строкой: 0
is_open true:           8
is_open 'true':         0
город с маленькой:      0
Вывод в терминалрезультат запуска
salary 70000 числом:   1
salary '70000' строкой:0
is_open true:          8
is_open 'true':        0
город с маленькой:     0

Три нуля в выводе получены по трём разным причинам:

  • тип значения другой: зарплата хранится числом, а поиск идёт по строке;
  • логическое значение записано текстом: true и "true" в BSON — разные типы;
  • регистр не совпал: сравнение строк посимвольное, «ярославль» и «Ярославль» — разные строки.
Compass, коллекция hh.resumes: фильтр salary равно строке 70000, счётчик 0 – 0 of 0 и надпись No results
Compass · { salary: "70000" }: вкладка Documents подписана числом 9, а под фильтр не подошло ни одного

Так выглядит несовпадение по типу в Compass: запрос выполнен без ошибки, счётчик показывает 0 – 0 of 0, и вместо документов стоит надпись No results. Кавычки вокруг 70000 в поле фильтра — единственное отличие от запроса, который находит документ.

Последний случай в модуле 2 решается шаблоном с $options: "i" — глава 2.7. Первые два случая средствами запроса не исправить: если под одним именем поля в базе лежат значения разных типов, их приводят к одному виду при записи.

Откуда берутся строки вместо чисел

Чаще всего — из веб-формы. Браузер присылает все поля текстом, и если при записи значение не привести к числу, в базе окажется "70000". Запросы с числом в фильтре такое поле не найдут, а сортировка выстроит значения как строки: "100000" окажется раньше "70000", потому что «1» меньше «7». Тип значения проверяется оператором $type — глава 2.5.

§5Точечная нотация: поле внутри документа

Когда в поле лежит другой документ, до его полей добираются через точку: "salary.from" читается как «поле from внутри поля salary». Такая запись называется точечной нотацией. Всё, что стоит в кавычках, — одно имя поля; точка входит в него как обычный символ.

salary:
from: 60000 ← salary.from
to: 90000 ← salary.to
Python · PyMongoполе внутри документа
for doc in vacancies.find({"salary.from": 60000},
                       {"title": 1, "salary": 1, "_id": 0}):
    print(doc)

print("выпускники СПО:", resumes.count_documents({"education.level": "СПО"}))
print("собеседования в Москве:", interviews.count_documents({"company.city": "Москва"}))
C++ · mongocxxполе внутри документа
mongocxx::options::find options;
options.projection(make_document(kvp("title", 1), kvp("salary", 1), kvp("_id", 0)));
for (const auto& doc : vacancies.find(make_document(kvp("salary.from", 60000)), options)) {
    std::cout << bsoncxx::to_json(doc) << std::endl;
}

std::cout << "выпускники СПО: " << resumes.count_documents(make_document(kvp("education.level", "СПО"))) << std::endl;
std::cout << "собеседования в Москве: " << interviews.count_documents(make_document(kvp("company.city", "Москва"))) << std::endl;
Go · mongo-driverполе внутри документа
cursor, _ := vacancies.Find(ctx,
    bson.D{{Key: "salary.from", Value: 60000}},
    options.Find().SetProjection(bson.D{
        {Key: "title", Value: 1}, {Key: "salary", Value: 1}, {Key: "_id", Value: 0}}))
for cursor.Next(ctx) {
    var doc bson.M
    cursor.Decode(&doc)
    fmt.Println(doc)
}

spo, _ := resumes.CountDocuments(ctx, bson.D{{Key: "education.level", Value: "СПО"}})
msk, _ := interviews.CountDocuments(ctx, bson.D{{Key: "company.city", Value: "Москва"}})
fmt.Println("выпускники СПО:", spo)
fmt.Println("собеседования в Москве:", msk)
Ruby · mongoполе внутри документа
vacancies.find({ "salary.from" => 60000 },
               projection: { "title" => 1, "salary" => 1, "_id" => 0 })
         .each { |doc| puts doc.inspect }

puts "выпускники СПО: " + resumes.count_documents({ "education.level" => "СПО" }).to_s
puts "собеседования в Москве: " + interviews.count_documents({ "company.city" => "Москва" }).to_s
Вывод в терминалрезультат запуска
{'title': 'Junior Python-разработчик', 'salary': {'from': 60000, 'to': 90000}}
выпускники СПО: 4
собеседования в Москве: 27
Вывод в терминалрезультат запуска
{ "title" : "Junior Python-разработчик", "salary" : { "from" : 60000, "to" : 90000 } }
выпускники СПО: 4
собеседования в Москве: 27
Вывод в терминалрезультат запуска
{"title":"Junior Python-разработчик","salary":{"from":{"$numberInt":"60000"},"to":{"$numberInt":"90000"}}}
выпускники СПО: 4
собеседования в Москве: 27
Вывод в терминалрезультат запуска
{"title"=>"Junior Python-разработчик", "salary"=>{"from"=>60000, "to"=>90000}}
выпускники СПО: 4
собеседования в Москве: 27

Глубина не ограничена: если бы внутри company лежал документ address, до улицы добирались бы через "company.address.street". Точка работает и в проекции, и в сортировке — везде, где пишется имя поля.

Точечная нотация отличается от поиска по целому документу. Фильтр {"salary": {"from": 60000, "to": 90000}} задаёт другое условие: он требует, чтобы вложенный документ совпал целиком и в том же порядке полей. Добавится третье поле — совпадения не будет. Поэтому по вложенным полям ищут через точку.

§6Точка и массивы

Условие на поле-массив проверяется на каждом элементе по отдельности. Фильтр {"skills": "Python"} читается как «среди навыков есть Python», хотя написан так же, как обычное равенство.

Python · PyMongoмассив строк и массив документов
print("знают Python:", resumes.count_documents({"skills": "Python"}))

for doc in resumes.find({"experience.role": "стажёр"},
                     {"fio": 1, "_id": 0}):
    print(doc)
C++ · mongocxxмассив строк и массив документов
std::cout << "знают Python: " << resumes.count_documents(make_document(kvp("skills", "Python"))) << std::endl;

mongocxx::options::find options;
options.projection(make_document(kvp("fio", 1), kvp("_id", 0)));
for (const auto& doc : resumes.find(make_document(kvp("experience.role", "стажёр")), options)) {
    std::cout << bsoncxx::to_json(doc) << std::endl;
}
Go · mongo-driverмассив строк и массив документов
python, _ := resumes.CountDocuments(ctx, bson.D{{Key: "skills", Value: "Python"}})
fmt.Println("знают Python:", python)

cursor, _ := resumes.Find(ctx,
    bson.D{{Key: "experience.role", Value: "стажёр"}},
    options.Find().SetProjection(bson.D{{Key: "fio", Value: 1}, {Key: "_id", Value: 0}}))
for cursor.Next(ctx) {
    var doc bson.M
    cursor.Decode(&doc)
    fmt.Println(doc)
}
Ruby · mongoмассив строк и массив документов
puts "знают Python: " + resumes.count_documents({ "skills" => "Python" }).to_s

resumes.find({ "experience.role" => "стажёр" },
             projection: { "fio" => 1, "_id" => 0 })
       .each { |doc| puts doc.inspect }
Вывод в терминалрезультат запуска
знают Python: 6
{'fio': 'Пётр Ковалёв'}
{'fio': 'Алина Дроздова'}
Вывод в терминалрезультат запуска
знают Python: 6
{ "fio" : "Пётр Ковалёв" }
{ "fio" : "Алина Дроздова" }
Вывод в терминалрезультат запуска
знают Python: 6
{"fio":"Пётр Ковалёв"}
{"fio":"Алина Дроздова"}
Вывод в терминалрезультат запуска
знают Python: 6
{"fio"=>"Пётр Ковалёв"}
{"fio"=>"Алина Дроздова"}
Compass, коллекция hh.resumes: фильтр skills равно Python, счётчик 1 – 6 of 6, у первого резюме массив skills раскрыт и содержит Python и Git
Compass · { skills: "Python" }: у первого резюме массив раскрыт, и видно, что Python — один из его элементов

У Анны Беловой в skills лежат две строки, "Python" и "Git". Фильтр сравнивает "Python" с каждой из них, и документ попадает в результат по первому совпадению. Остальные пять резюме в списке свёрнуты, но отобраны по тому же правилу.

Шесть резюме из девяти содержат Python среди навыков. Второй запрос показывает то же правило на массиве документов: experience — список мест работы, и "experience.role" означает «поле role у любого элемента этого списка». Нашлись двое, у кого хотя бы одна запись об опыте помечена ролью «стажёр».

Это правило действует, пока на массив наложено одно условие. Разница появляется, когда условий на массив становится два: они могут выполниться разными элементами. Такой случай разбирается в главе 2.6 вместе с оператором $elemMatch.

§7Пустой фильтр

Фильтр без единой пары — {} — не задаёт ни одного условия, поэтому подходит любой документ. Это следствие правила «вернуть то, что удовлетворяет всем условиям»: когда условий нет, удовлетворяют все.

Python · PyMongoпустой фильтр
print("всего резюме:", resumes.count_documents({}))
print("всего собеседований:", interviews.count_documents({}))
C++ · mongocxxпустой фильтр
std::cout << "всего резюме: " << resumes.count_documents(make_document()) << std::endl;
std::cout << "всего собеседований: " << interviews.count_documents(make_document()) << std::endl;
Go · mongo-driverпустой фильтр
vsego, _ := resumes.CountDocuments(ctx, bson.D{})
sobes, _ := interviews.CountDocuments(ctx, bson.D{})
fmt.Println("всего резюме:", vsego)
fmt.Println("всего собеседований:", sobes)
Ruby · mongoпустой фильтр
puts "всего резюме: " + resumes.count_documents({}).to_s
puts "всего собеседований: " + interviews.count_documents({}).to_s
Вывод в терминалрезультат запуска
всего резюме: 9
всего собеседований: 60
Вывод в терминалрезультат запуска
всего резюме: 9
всего собеседований: 60
Вывод в терминалрезультат запуска
всего резюме: 9
всего собеседований: 60
Вывод в терминалрезультат запуска
всего резюме: 9
всего собеседований: 60
Compass, коллекция hh.interviews: поле фильтра пустое, счётчик 1 – 25 of 60
Compass · коллекция hh.interviews без фильтра: счётчик показывает 1 – 25 of 60

Поле фильтра здесь пустое — это тот же случай, что {} в коде. Список Compass выдаёт страницами по 25 документов; размер страницы задаётся выпадающим списком слева от счётчика, а остальные документы подгружаются стрелками справа.

На девяти резюме это незаметно. В коллекции собеседований пустой фильтр поднимает с диска все 60 документов, а в рабочей базе — все документы коллекции, сколько бы их ни было. Поэтому в программе пустой фильтр ставят осознанно: для подсчёта, для выгрузки целиком или вместе с ограничением из главы 1.6.

§8Фильтр в Compass

В Compass фильтр вводится в поле над списком документов. Пишется он там тем же документом, что и в коде. Кавычки вокруг имён полей Compass не требует, но и не запрещает.

Compass, коллекция hh.resumes: в поле фильтра education.level равно СПО, найдено 4 документа из 9
Compass · hh.resumes, фильтр { "education.level": "СПО" } — те же четыре резюме, что вернул запрос из §5

На кадре видны четыре места:

  • слева в дереве раскрыта база hh и её четыре коллекции — те же, что в таблице из §1;
  • вкладка Documents подписана числом 9: столько документов в коллекции всего;
  • в поле запроса стоит фильтр с точечной нотацией, и Compass принял его без изменений;
  • справа над списком — 1 – 4 of 4: под фильтр подошли четыре документа из девяти.

Документы в списке свёрнуты: вложенные документы и массивы показаны как {…} и […], их разворачивают треугольником слева. У Дарьи Нечаевой видно поле courses, которого нет у остальных; поиск по таким полям разобран в главе 2.5.

Фильтр удобно проверять в Compass до переноса в код: если запрос уже здесь возвращает не те документы, ошибку надо искать в самом фильтре.

§9Частые ошибки

Что написаноЧто происходитКак правильно
{"salary": "70000"}Пусто: в базе число, а ищем строку{"salary": 70000}
{"is_open": "true"}Пусто: в базе логическое значение{"is_open": True}
{"salary": {"from": 60000}}Пусто: сравнение с целым документом, а в базе у salary два поля{"salary.from": 60000}
{"education": {"level": "СПО"}}Пусто по той же причине: внутри education три поля{"education.level": "СПО"}
{"education.level": "спо"}Пусто: регистр не совпал{"education.level": "СПО"}
{"city": "Ярославль", "city": "Москва"}Одно условие вместо двух: в словаре остаётся последняя параНужен $in или $or — главы 2.3 и 2.4
Что написаноЧто происходитКак правильно
kvp("salary", "70000")Пусто: в базе число, а ищем строкуkvp("salary", 70000)
kvp("is_open", "true")Пусто: в базе логическое значениеkvp("is_open", true)
kvp("salary", make_document(kvp("from", 60000)))Пусто: сравнение с целым документомkvp("salary.from", 60000)
make_document(kvp("city", "Ярославль"), kvp("city", "Москва"))Пусто: оба поля уходят на сервер и соединяются по «и», а город у документа одинНужен $in или $or — главы 2.3 и 2.4
kvp("education.level", "спо")Пусто: регистр не совпалkvp("education.level", "СПО")
Фильтр объявлен внутри if, а используется снаружиОбращение к освобождённой памяти: make_document возвращает владеющее значениеДержать auto filtr = make_document(…) живым, пока идёт запрос
Что написаноЧто происходитКак правильно
bson.D{{Key: "salary", Value: "70000"}}Пусто: в базе число, а ищем строкуValue: 70000
bson.D{{Key: "is_open", Value: "true"}}Пусто: в базе логическое значениеValue: true
bson.D{{Key: "salary", Value: bson.D{{Key: "from", Value: 60000}}}}Пусто: сравнение с целым документомbson.D{{Key: "salary.from", Value: 60000}}
bson.M{"city": …, "position": …} там, где важен порядокbson.M — карта, порядок полей не сохраняетсяДля фильтра годится и bson.M; порядок важен только в индексах и сортировке — bson.D
Value: "спо"Пусто: регистр не совпалValue: "СПО"
Ошибка запроса присвоена _Пустой результат выглядит как «ничего не нашлось»Проверять второе значение, которое возвращает Find
Что написаноЧто происходитКак правильно
{ "salary" => "70000" }Пусто: в базе число, а ищем строку{ "salary" => 70000 }
{ "is_open" => "true" }Пусто: в базе логическое значение{ "is_open" => true }
{ "salary" => { "from" => 60000 } }Пусто: сравнение с целым документом{ "salary.from" => 60000 }
{ city: "Ярославль" }Работает, но ключ — символ; в документах поля строковыеПисать ключи строками: { "city" => "Ярославль" }
{ "education.level" => "спо" }Пусто: регистр не совпал{ "education.level" => "СПО" }
Результат find перебран дважды и ожидается тот жеЗапрос выполняется заново при каждом обходеСохранить в массив: .to_a

§10Проверьте себя

Чем {"city": "Ярославль", "position": "Тестировщик"} отличается от двух отдельных запросов?

Это один запрос с двумя условиями, и сервер вернёт документы, где выполнены оба сразу. Два отдельных запроса дали бы два набора, которые пришлось бы пересекать в программе. Перечисление пар в фильтре означает «и».

Почему {"salary": "70000"} ничего не находит, хотя такая зарплата в базе есть?

В базе значение записано числом, а в фильтре стоит строка. Сравнение идёт по значению и типу BSON, поэтому 70000 и "70000" не совпадают. Правильный фильтр — {"salary": 70000}.

Что вернёт {"salary": {"from": 60000}} на коллекции вакансий?

Ничего. Такой фильтр требует, чтобы в поле salary лежал документ ровно с одним полем from, а там два поля — from и to. Чтобы искать по вложенному полю, нужна точка: {"salary.from": 60000}.

Что означает {"skills": "Python"}, если skills — массив?

«Среди элементов массива есть строка Python». Условие проверяется на каждом элементе по отдельности; с массивом целиком оно не сравнивается. Такой фильтр нашёл шесть резюме из девяти.

Сколько документов вернёт find({}) и почему?

Все, сколько есть в коллекции. Пустой фильтр не задаёт ни одного условия, а вернуть нужно документы, удовлетворяющие всем условиям, — значит, подходят все.

§11Шпаргалка

Одно условие col.find({"city": "Ярославль"})
Несколько условий — «и» col.find({"city": "Ярославль", "salary": 70000})
Вложенное поле col.find({"salary.from": 60000})
Элемент массива col.find({"skills": "Python"})
Всё подряд col.find({})
Сколько подходит col.count_documents(filtr)
Одно условие col.find(make_document( kvp("city", "Ярославль")));
Несколько условий — «и» make_document(kvp("city", "Ярославль"), kvp("salary", 70000))
Вложенное поле kvp("salary.from", 60000)
Элемент массива kvp("skills", "Python")
Всё подряд col.find(make_document());
Сколько подходит col.count_documents(filtr.view());
Одно условие col.Find(ctx, bson.D{ {Key: "city", Value: "Ярославль"}})
Несколько условий — «и» bson.D{{Key: "city", Value: "Ярославль"}, {Key: "salary", Value: 70000}}
Вложенное поле {Key: "salary.from", Value: 60000}
Элемент массива {Key: "skills", Value: "Python"}
Всё подряд col.Find(ctx, bson.D{})
Сколько подходит col.CountDocuments(ctx, filtr)
Одно условие col.find({ "city" => "Ярославль" })
Несколько условий — «и» col.find({ "city" => "Ярославль", "salary" => 70000 })
Вложенное поле col.find({ "salary.from" => 60000 })
Элемент массива col.find({ "skills" => "Python" })
Всё подряд col.find({})
Сколько подходит col.count_documents(filtr)

Итог главы

Фильтр — документ той же формы, что и документы в коллекции: поле означает поле, значение означает «должно быть равно». Несколько пар в фильтре проверяются вместе и работают как «и», порядок пар роли не играет. Совпадение требуется точное — по типу BSON и по регистру строк. До поля внутри вложенного документа добираются через точку; сравнение с целым вложенным документом задаёт другое условие — совпадение всех его полей в том же порядке. Условие на массив проверяется на каждом элементе по отдельности. Дальше вместо точного равенства появятся операторы сравнения: больше, меньше, не равно.

Глава 2.1 · Справочник по MongoDB Автор: Макаров Максим Николаевич