Содержание урока · 10 разделов
- Неявный AND и когда нужен явный $and
- $or: альтернативы и ловушка второго ключа
- Как собрать комбинации условий
- $regex: поиск по части строки
- Синтаксис паттернов: MongoDB против Go regexp
- Чек-лист сложного фильтра
- Комбинации на практике: три типовых запроса
- Песочница урока
- Коротко: что запомнить
- Вопросы и ответы
Фильтры восьмого урока работали с одним полем: статус, цена, попадание в список. Реальный поиск устроен сложнее. Панель администратора собирает запрос из трёх полей, любое из которых может быть пустым: статус, минимальная цена, подстрока в названии. Поиск по каталогу ищет фразу и в названии, и в описании — «или» по двум полям. Отчёт исключает архив и черновики, оставляя всё остальное. Это уже не одиночные условия, а их комбинации, и MongoDB описывает их тремя операторами: неявный AND соберёт условия по разным полям, $or объявит альтернативы, $and разрешит конфликты, а $regex добавит частичное совпадение строк. В Go всё это собирается из тех же bson.M и bson.D, но у композиции есть ловушка, которая не ловится ни компилятором, ни сервером: карта в Go хранит один ключ один раз, и второй $or молча затирает первый. У композиции цена небрежности выше, чем у любого одиночного условия: ошибка не ломает запрос — она меняет его смысл. Начнём с простого и дойдём до этой ловушки по шагам, а к концу соберём панельный фильтр, который не боится пустых полей и пользовательского ввода, — в виде именованной функции, которую не страшно читать через полгода.
Неявный AND и когда нужен явный $and#
Фильтр с несколькими ключами — уже конъюнкция: bson.M{"status": "active", "stock": bson.M{"$gt": 0}} находит документы, где статус активен и остаток положителен. Это неявный AND, и он покрывает большинство запросов, пока условия лежат на разных полях. Явный $and нужен в двух случаях. Первый: одно поле должно удовлетворить нескольким условиям, которые не помещаются в один операторный документ — два диапазона с границами «строго больше» и «не больше» записываются как {"$and": [{"qty": {"$gt": 5}}, {"qty": {"$lte": 20}}]}. Второй: нужно скомбинировать две $or-группы — об этом отдельный блок ниже. Записывается $and массивом условий: bson.D{{Key: "$and", Value: []bson.M{...}}}; порядок элементов массива сервер сохраняет, поэтому для сложных конъюнкций берут bson.D, а не карту — карта и здесь рискует перезаписью ключей. Внутри массива каждый элемент — обычный фильтр: равенства, операторы, вложенные $or — всё то же самое на один уровень глубже. Явный $and с единственным условием избыточен: такое условие спокойно живёт в одной карте рядом с другими, и оператор оправдан, только когда без него не собрать.
Динамический фильтр панели
GOfunc adminFilter(q AdminQuery) bson.D {
conds := bson.D{}
if q.Status != "" {
conds = append(conds, bson.E{Key: "status", Value: q.Status})
}
if q.MinPrice > 0 {
conds = append(conds, bson.E{Key: "price", Value: bson.M{"$gte": q.MinPrice}})
}
if q.Search != "" {
pattern := regexp.QuoteMeta(q.Search)
conds = append(conds, bson.E{Key: "name", Value: bson.M{
"$regex": pattern, "$options": "i",
}})
}
return conds // неявный AND: ключи не пересекаются
}Динамический фильтр собирается срезом bson.E: пустые поля панели просто не попадают в условие, и неявный AND собирает остальные в один запрос без лишних веток. Пользовательский ввод проходит regexp.QuoteMeta: точка или скобка в поиске превращаются в литералы, а не в части регэкспа, поэтому инъекция паттерна исключена ещё на сборке. Возвращённый срез пар передаётся прямо в Find как фильтр — сервер сам прочитает конъюнкцию.
$or: альтернативы и ловушка второго ключа#
$or принимает массив условий и находит документы, подходящие хотя бы под одно: поиск по названию или описанию записывается bson.M{"$or": []bson.M{{"name": ...}, {"description": ...}}}. В Go-коде mongo or filter чаще всего собирается именно срезом bson.M — каждый элемент самостоятельный фильтр со своими операторами. Ловушка подстерегает на уровне языка: задача «активные или отложенные, но не архивные» выглядит как два ключа $or в одной карте, но карта в Go хранит ключ один раз — второй $or перезапишет первый ещё при сборке литерала, без ошибки компиляции и без ошибки сервера. Половина условий исчезает молча, и фильтр находит не то, что задумано. Решение — упаковать обе группы в $and: bson.D{{Key: "$and", Value: []bson.M{{"$or": ...}, {"archived": ...}}}} — каждый элемент массива живёт своей жизнью. Вложенность ограничена только здравым смыслом: $or внутри $and внутри $or читается уже с трудом, и это сигнал вынести сборку фильтра в именованную функцию с комментариями. Рядом стоит зеркальный оператор $nor: он находит документы, не подходящие ни под одно условие из массива, — «не архив и не черновик» иногда читается через $nor из двух равенств, хотя чаще та же запись живёт в $nin со списком значений.
Как собрать комбинации условий#
| Приём | Запись в Go | Когда брать |
|---|---|---|
| Неявный AND | bson.M{"status": "active", "stock": 1} | условия по разным полям — самый частый случай |
| $and | bson.D{{Key: "$and", Value: []bson.M{...}}} | две $or-группы вместе или условия, не помещающиеся в одну карту |
| $or | bson.M{"$or": []bson.M{...}} | любое из условий — альтернативы одного смысла |
| $ne / $nin | bson.M{"archived": bson.M{"$ne": true}} | исключение значения или списка значений |
| $regex | bson.M{"name": bson.M{"$regex": "^A"}} | частичное совпадение строки, лучше с якорем |
$regex: поиск по части строки#
Оператор $regex запускает на сервере регэксп: bson.M{"name": bson.M{"$regex": "^клавиатура", "$options": "i"}} находит имена, начинающиеся на «клавиатура», в любом регистре. Паттерн — обычная строка, опции идут отдельным ключом $options: i снимает регистр, m включает якоря по строкам, x разрешает пробелы в шаблоне. Вторая форма записи — тип bson.Regex из пакета bson: bson.Regex{Pattern: "^клавиатура", Options: "i"} вставляется прямо как значение фильтра, и кодек соберёт BSON-тип регэкспа; в v1 этот тип назывался primitive.Regex. Для regex поиска mongodb-сервер компилирует паттерн в свой движок, и синтаксис там PCRE-подобный, а не Go-шный: look-ahead в нём есть, а именованные классы пишутся по-другому — паттерн пишется для сервера, а не для пакета regexp. Цена операции зависит от якоря: паттерн с префиксом ^буквы может опираться на индекс и перебирать только его начало; паттерн без якоря заставляет сервер проверять каждое значение каждого документа коллекции. Опции парсятся однобуквенные: i снимает регистр, m включает якоря по строкам, s делает точку совпадающей с переводом строки, x разрешает пробелы в шаблоне, u включает юникод-таблицы. Самая частая — i, и её же чаще всего добавляют без нужды: регистр важен для артикулов и кодов.
На каталоге в два миллиона товаров поиск без якоря — {$regex: q} — уложил базу: каждое нажатие клавиши запускало полный скан коллекции с компиляцией регэкспа, а опция i удваивала работу. Правило: $regex — для админских панелей и префиксного поиска по индексу; пользовательский поиск — отдельный механизм (текстовый индекс семнадцатого урока или внешний поисковик), а ввод пользователя — всегда через regexp.QuoteMeta. Мораль та же, что и с пустым фильтром удаления: драйвер выполнит любой запрос честно, цену посчитает только мониторинг.
Синтаксис паттернов: MongoDB против Go regexp#
Два регэксп-движка в одном проекте — постоянный источник путаницы, поэтому разложим грани. Паттерн в $regex исполняет сервер MongoDB: синтаксис близок к PCRE — есть look-ahead и look-behind, классы \d и \w, а флаги живут в $options. Пакет regexp в Go исполняет RE2: look-around в нём принципиально отсутствует, зато он линеен по времени и не подвержен катастрофическому бэктрекингу. Из Go в фильтр паттерн уезжает строкой, и его пишут под серверный движок; Go-regexp нужны только для проверки на клиенте, если она вообще нужна. Вторая граница — экранирование: пользовательский ввод почти всегда содержит спецсимволы, и regexp.QuoteMeta превращает их в литералы; без этого скобка или звёздочка в запросе меняют смысл паттерна. Третья граница — сами флаги: опции сервера однобуквенные, i для регистра — самое частое; вместе с якорем ^ префиксный поиск по индексу остаётся быстрым, всё остальное — платно. Паттерн — обычная строка, поэтому его удобно отлаживать в mongosh тем же запросом: db.coll.find({ name: { $regex: "...", $options: "i" } }) — увиденный там состав выборки совпадёт с выдачей драйвера байт в байт.
Комбинация: $and, две $or-группы, $ne
GO// «Активные ИЛИ отложенные» — и не архивные
filter := bson.M{
"$and": []bson.M{
{"$or": []bson.M{
{"status": "active"},
{"status": "deferred"},
}},
{"archived": bson.M{"$ne": true}},
},
}
count, err := coll.CountDocuments(ctx, filter)Две $or-группы в одной карте не уживутся: второй ключ перезапишет первый молча, ещё до отправки на сервер. Обёртка $and со срезом условий решает конфликт: каждый элемент среза — самостоятельное условие со своей группой альтернатив. $ne: true исключает архив; для списка исключений есть $nin со срезом значений — приём тот же, меняется только количество.
Чек-лист сложного фильтра#
- Условия по разным полям — неявный AND без всяких операторов.
- Две $or-группы — только через $and со срезом: второй ключ карты затрёт первый.
- Одно поле с несколькими операторами — один вложенный документ или $and.
- Пользовательский ввод в $regex — сначала regexp.QuoteMeta.
- Префиксный ^паттерн — под индекс; без якоря — полный скан.
- Комбинированный фильтр собирает именованная функция — литералы на месте вызова не читаются.
Проверка: два ключа $or
В bson.M записаны два ключа "$or" подряд. Что окажется в фильтре?
Проверка: какой $regex быстрый
Какой паттерн $regex сможет опираться на индекс по полю name?
Сколько имён найдёт фильтр?
Прочитайте код и выберите, что он выведет:
// в коллекции: alice, Bob, carol, Dave
cur, _ := coll.Find(ctx, bson.M{
"name": bson.M{"$regex": "^a", "$options": "i"},
})
var names []bson.M
cur.All(ctx, &names)
fmt.Println(len(names))Комбинации на практике: три типовых запроса#
Соберём три запроса, которые встречаются почти в каждом сервисе. Поиск по любому из полей: $or со срезом из name и description, каждый элемент — свой $regex с QuoteMeta-вводом и опцией i; итог — «фраза встречается где угодно из двух мест». Исключение неактуального: {"archived": {"$ne": true}} и {"status": {"$nin": ["draft", "deleted"]}} рядом в неявном AND — карта справится, ключи разные. Комбо с диапазоном: цена между границами через $gte и $lte в одном вложенном документе, статус через $in со списком допустимых — и всё это внутри $and с $or-группой для поиска. Общий приём у всех трёх — сборка через именованную функцию, возвращающую bson.D или bson.M: панель с пятью необязательными полями в виде одного литерала не читается, а функция с append по условию — читается и тестируется. Фильтры из этих примеров уходят в Find и Aggregate одинаково: синтаксис условий общий, агрегации лишь оборачивают его в этап $match. Отдельная мелочь, о которой стоит знать заранее: сравнить два поля документа операторами запроса нельзя — $gte сравнивает поле с константой. Для «остаток больше зарезервировано» есть $expr: {"$expr": {"$gt": ["$stock", "$reserved"]}} запускает агрегационное выражение прямо в фильтре — это мост к конвейерам пятнадцатого урока.
Тип регэкспа переехал из primitive в bson вместе со всем словарём примитивов: bson.Regex{Pattern, Options} — то, что в старых туториалах писалось как primitive.Regex. Обе формы — карта с $regex и $options и тип bson.Regex — собирают один BSON-тип; разница только в читаемости. Поля типа называются Pattern и Options, без доллара: доллар нужен только в строковой форме.
Соберите комбинацию условий
Восстановите фильтр: альтернативы статусов, исключение архива и сортировка по новизне.
Соберите функцию поиска
Расставьте строки метода по порядку: экранирование ввода, фильтр с регэкспом, запрос с опциями, декодирование результата.
Строки
Ваша программа
Нажимайте на строки слева, чтобы собрать программу
Карточки: сложные фильтры
Фильтры сложного уровня готовы: комбинации через $and и $or, частичное совпадение через $regex с экранированием, сборка динамических условий срезом пар. Они уезжают в тот же Find из седьмого урока вместе с опциями сортировки и лимитами — базовые условия восьмого урока остаются фундаментом, а итоговый проект соберёт поиск каталога именно из этих деталей. Двух места требуют индексы: префиксный regex и $match по статусу быстры только с ними — урок про CreateOne покажет, как измерить и построить. Перед отладкой печатайте собранный фильтр: bson.MarshalExtJSON из пятого урока покажет документ ровно в том виде, в каком его увидит сервер, — и затёртый $or ловится глазом за секунду. Проверять комбинации лучше на живых данных: песочница сайта — тематическая программа на чистом Go, поэтому соберите панельный фильтр локально и посмотрите в mongosh, какие документы он находит на каждом шаге сборки.
Песочница урока
Редактируйте код и запускайте его прямо здесь — интерпретатор работает в браузере, ничего устанавливать не нужно. За каждый запуск — +2 XP.
Запустите код, чтобы увидеть вывод.
Коротко: что запомнить
- Несколько ключей в одной карте — неявный AND; он покрывает условия по разным полям.
- $or принимает срез условий; два ключа $or в bson.M молча перезаписываются — комбинируйте через $and.
- $and со срезом bson.M нужен для двух $or-групп и условий, не помещающихся в одну карту.
- $regex исполняет сервер, синтаксис PCRE-подобный: префиксный ^паттерн опирается на индекс, остальное — скан.
- Пользовательский ввод перед $regex проходит regexp.QuoteMeta — спецсимволы становятся литералами.
- bson.Regex{Pattern, Options} — второй способ записать регэксп; в v1 тип назывался primitive.Regex.
- Для пользовательского поиска по всему тексту $regex не предназначен: текстовый индекс или внешний поисковик.
Домашнее задание
Соберите функцию поиска каталога: три необязательных параметра (статус, минимальная цена, подстрока названия), срез bson.E с append по условию, QuoteMeta для ввода и сортировка с лимитом из опций Find. Затем проверьте на живой базе: добавьте индекс по name и сравните в explain префиксный паттерн со сканом — разница в плане покажет цену якоря. Попутно выведите финальный фильтр через bson.MarshalExtJSON: увиденный документ — это ровно то, что уйдёт на сервер.
Личные заметкиПусто
Вопросы и ответы по уроку
Почему нельзя записать две $or-группы в одной bson.M?
Карта в Go хранит каждый ключ один раз: второй $or перезапишет первый при сборке литерала — без ошибки компиляции и без ошибки сервера. Решение: $and со срезом условий, где каждая группа живёт отдельным элементом массива. Поймать такое быстро: выведите фильтр через bson.MarshalExtJSON перед запросом — в распечатке видно, что от двух $or осталась одна.
Как искать по части слова без скана коллекции?
Честный ответ — почти никак: только префиксные паттерны с ^ опираются на индекс. Для поиска «в середине слова» MongoDB предлагает текстовый индекс по словам, а для тяжёлых требований — внешний поисковик. $regex без якоря на больших коллекциях — бюджет скана. Граница простая: если поиск нужен пользователям каталога на сотни тысяч документов — внешний поисковик или текстовый индекс; если админам для разовой чистки — $regex с якорем и QuoteMeta.
Чем паттерн $regex отличается от Go regexp?
Паттерн исполняет сервер MongoDB со своим PCRE-подобным движком: там есть look-ahead и look-behind, которых нет в Go RE2. Паттерн пишется под серверный синтаксис, а regexp.QuoteMeta из Go используется только для экранирования ввода перед подстановкой.
Термины урока
Ключевые понятия из этого урока — короткие определения в глоссарии. Наведите на термин, чтобы вспомнить, откройте карточку для подробностей и ссылки на практику.
Изучайте дальше
Следующая ступень учебного пути
Zerolog в Go: быстрое JSON-логирование
Курс zerolog для Go: JSON-события, уровни, поля, ConsoleWriter, контекстные поля, ротация, sampling, HTTP-логи и тесты. 25 уроков с примерами.
- Писать структурированные JSON-логи с нуля аллокаций
- Выбирать уровни и поля события осознанно
- Настраивать ConsoleWriter для dev и JSON для прода
- Строить сублоггеры и контекстные поля на запрос