Перейти к содержимому
ГоМастер

Поиск по урокам

Найдите урок, курс, термин или раздел

Урок 14 из 25 ~20 мин +50 XP

$and, $or, $regex: сложные фильтры

$and, $or и $regex в mongo из Go закрывают поиск по частичному совпадению и комбинации условий: «активные или отложенные», «имя начинается с А», «цена в диапазоне и не архив». Разберём неявный AND, явный $and через bson.D, ловушку дублирующегося ключа и цену регистронезависимого regex поиска mongodb.

Содержание урока · 10 разделов

Фильтры восьмого урока работали с одним полем: статус, цена, попадание в список. Реальный поиск устроен сложнее. Панель администратора собирает запрос из трёх полей, любое из которых может быть пустым: статус, минимальная цена, подстрока в названии. Поиск по каталогу ищет фразу и в названии, и в описании — «или» по двум полям. Отчёт исключает архив и черновики, оставляя всё остальное. Это уже не одиночные условия, а их комбинации, и 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 с единственным условием избыточен: такое условие спокойно живёт в одной карте рядом с другими, и оператор оправдан, только когда без него не собрать.

Динамический фильтр панели

GO
func 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Когда брать
Неявный ANDbson.M{"status": "active", "stock": 1}условия по разным полям — самый частый случай
$andbson.D{{Key: "$and", Value: []bson.M{...}}}две $or-группы вместе или условия, не помещающиеся в одну карту
$orbson.M{"$or": []bson.M{...}}любое из условий — альтернативы одного смысла
$ne / $ninbson.M{"archived": bson.M{"$ne": true}}исключение значения или списка значений
$regexbson.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, и её же чаще всего добавляют без нужды: регистр важен для артикулов и кодов.

Синтаксис паттернов: 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"]}} запускает агрегационное выражение прямо в фильтре — это мост к конвейерам пятнадцатого урока.

Соберите комбинацию условий

Восстановите фильтр: альтернативы статусов, исключение архива и сортировка по новизне.

filter := bson.M{ "$": []bson.M{ {"status": "active"}, {"status": "deferred"}, }, "archived": bson.M{"$": true}, } cursor, err := coll.Find(ctx, filter, options.Find(). Set(bson.D{{Key: "created_at", Value: -1}}))

Соберите функцию поиска

Расставьте строки метода по порядку: экранирование ввода, фильтр с регэкспом, запрос с опциями, декодирование результата.

Строки

Ваша программа

Нажимайте на строки слева, чтобы собрать программу

Карточки: сложные фильтры

Термин

$or

нажмите, чтобы перевернуть
Определение

Массив условий: bson.M{"$or": []bson.M{...}} — подходит хотя бы одно. Второй ключ $or в карте молча затирается: две группы комбинируют через $and.

1 / 6

Фильтры сложного уровня готовы: комбинации через $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: увиденный документ — это ровно то, что уйдёт на сервер.

Личные заметкиПусто
Хранятся только в вашем браузере (MongoDB · $and, $or, $regex: сложные фильтры)

Вопросы и ответы по уроку

Почему нельзя записать две $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 используется только для экранирования ввода перед подстановкой.

Термины урока

Ключевые понятия из этого урока — короткие определения в глоссарии. Наведите на термин, чтобы вспомнить, откройте карточку для подробностей и ссылки на практику.

Следующая ступень учебного пути

zerolog25 уроков~12 чпрактика25 уроков уже доступны

Zerolog в Go: быстрое JSON-логирование

Курс zerolog для Go: JSON-события, уровни, поля, ConsoleWriter, контекстные поля, ротация, sampling, HTTP-логи и тесты. 25 уроков с примерами.

  • Писать структурированные JSON-логи с нуля аллокаций
  • Выбирать уровни и поля события осознанно
  • Настраивать ConsoleWriter для dev и JSON для прода
  • Строить сублоггеры и контекстные поля на запрос