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

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

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

Урок 15 из 25 ~18 мин +20 XP

Inline-режим: бот в поле поиска

Inline-режим выносит бота из чата в поле ввода: пользователь печатает @имябота запрос в любом диалоге, а InlineQuery и AnswerInlineQuery на Go превращают это в подсказки-карточки, которые можно вставить одним нажатием. Разберём апдейт InlineQuery, статьи AnswerInlineQuery, кэш результатов и включение режима через BotFather.

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

InlineQuery: апдейт без сообщения#

Бот-словарь терминалов Go живёт в чате, и чтобы посмотреть значение слова, пользователь должен открыть диалог, написать команду, прочитать ответ — четыре шага ради справки. Inline-режим сжимает их до двух: в любом чате, даже в группе с друзьями, человек набирает @имябота и первые буквы термина, а над полем ввода появляются карточки-подсказки; нажатие вставляет готовый текст в сообщение. Это тот же механизм, которым пользуются @gif и @pic при поиске картинок, и он меняет роль бота: из собеседника он становится службой подсказок, доступной из поля ввода каждого чата. Механика на Go держится на двух объектах: входящий InlineQuery и ответ AnswerInlineQuery со списком результатов. Ниже — включение режима, ветка в цикле обновлений, статьи с кэшированием и честный разбор того, почему бот с работающим кодом может молчать.

Пользователь начал набирать @имябота — Telegram присылает в цикл обновлений объект InlineQuery, у которого Message равен nil: это не сообщение в чат, а запрос подсказок. Рабочие поля четыре. ID — идентификатор запроса, на него надо ответить. From — кто ищет: для персонализации и лимитов. Query — сам поисковый запрос, обрезанный по текущей позиции курсора. Offset — строка пагинации для длинных списков. Первым делом режим надо включить: в BotFather команда /setinline, выбор бота и подсказка-плейсхолдер для поля ввода — без этого апдейты InlineQuery не приходят вовсе, и код будет безошибочно молчать. Ответ обязателен на каждый запрос, включая пустой Query: пользователь стёр буквы — бот должен вернуть пустой список, иначе клиент держит старые подсказки. Ветка в цикле ставится рядом с проверкой CallbackQuery из девятого урока: update.InlineQuery != nil — своя ветка со своим обработчиком.

Ветка inline в цикле обновлений

GO
for update := range updates {
        if update.InlineQuery != nil {
                handleInline(bot, update.InlineQuery)
                continue
        }
        if update.CallbackQuery != nil {
                handleCallback(bot, update.CallbackQuery)
                continue
        }
        if update.Message == nil {
                continue
        }
        handleMessage(bot, update.Message)
}

func handleInline(bot *tgbotapi.BotAPI, iq *tgbotapi.InlineQuery) {
        log.Printf("inline: %q от %d", iq.Query, iq.From.ID)
}

Порядок проверок одинаковый: спец-апдейты первыми, сообщение последним. InlineQuery проверяется раньше CallbackQuery — порядок между ними не важен, важно, что оба раньше Message, потому что у них update.Message равен nil. Обработчик пока только логирует запрос и автора: ответ на iq.ID добавим следующим шагом.

AnswerInlineQuery и статьи#

Ответ строится из списка результатов, а простейший результат — статья: NewInlineQueryResultArticle(id, title, text) создаёт карточку с заголовком и текстом, который вставится в поле ввода при выборе. Первый аргумент — короткий идентификатор результата: он нужен, чтобы потом узнать выбранный вариант в ChosenInlineResult, и обычно собирается как индекс или ключ записи. Одним ответом уходит до пятидесяти результатов — для подсказок хватает трёх-пяти, длинные списки режутся по Offset. Отправляет ответ bot.AnswerInlineQuery с InlineQueryConfig, где InlineQueryID равен iq.ID, а Results — срез интерфейсов. Тонкость: Results имеет тип []interface{}, и каждый результат придётся приводить к этому типу при добавлении — особенность сигнатуры библиотеки v5. Ответ должен уйти быстро: клиент показывает подсказки только живому ответу, а Telegram хранит результаты в кэше и не ждёт вечности.

Ответ тремя подсказками

GO
func handleInline(bot *tgbotapi.BotAPI, iq *tgbotapi.InlineQuery) {
        if iq.Query == "" {
                return
        }

        r1 := tgbotapi.NewInlineQueryResultArticle(
                "1", "Карта: map", "map[string]int — словарь ключ-значение")
        r2 := tgbotapi.NewInlineQueryResultArticle(
                "2", "Срез: slice", "[]int — динамический массив")
        r3 := tgbotapi.NewInlineQueryResultArticle(
                "3", "Канал: chan", "chan int — обмен между горутинами")

        cfg := tgbotapi.InlineQueryConfig{
                InlineQueryID: iq.ID,
                Results:       []interface{}{r1, r2, r3},
        }
        bot.AnswerInlineQuery(cfg)
}

На пустой запрос бот не отвечает — старые подсказки у клиента и так исчезают. Три статьи: у каждой свой идентификатор, заголовок и текст для вставки. Срез Results объявлен как []interface{} — статьи добавляются в него без проблем, а ответ уходит по InlineQueryConfig с ID исходного запроса.

CacheTime и IsPersonal#

У InlineQueryConfig есть поля, которые управляют тем, как Telegram кэширует и переиспользует ответы. CacheTime — срок жизни ответа в секундах: по умолчанию Telegram держит результаты запроса пять минут и не дёргает бота повторно на тот же запрос от того же пользователя. Для словаря терминов это подарок: статичные подсказки отдаются мгновенно, нагрузка на бота падает в разы. Для живых данных — курса валют, остатков на складе, свободных слотов — кэш вредит, и CacheTime ставят в единицу или ноль. IsPersonal делает результаты приватными: без него один ответ может быть показан участникам чата, где искали — неважно для терминов, критично для личных документов и цен. Offset и его разбор вместе с ChosenInlineResult закрывают продвинутые сценарии: пагинация и статистика выбора — но и без них бот-словарь уже полноценный.

Кейс: поиск по словарю терминов#

Свяжем механику с реальным сценарием. Словарь — срез структур Term с полями Name и Meaning, штук сорок-пятьдесят для учебного проекта. Обработчик фильтрует его по iq.Query: strings.Contains(strings.ToLower(t.Name), strings.ToLower(iq.Query)) — регистр приводится с двух сторон, иначе «КАРТА» не найдёт карту. На первые три совпадения создаются статьи: идентификатор — индекс в словаре, заголовок — термин, вставляемый текст — значение. Кэш CacheTime 300 не мешает: словарь меняется с релизами, а не с секундами. Логика поиска — чистый Go над строками и срезами, и она полноценно тестируется без Telegram: unit-тесты на фильтр ловят и регистр, и пустой запрос, ещё до первого запуска бота. Когда словарь вырастет за сотню записей, фильтр по памяти переедет в Redis с поиском по множествам — механика inline при этом не изменится.

Поиск по словарю и ответ

GO
type Term struct {
        Name    string
        Meaning string
}

var terms = []Term{
        {"Карта", "map[K]V — словарь ключ-значение"},
        {"Срез", "[]T — динамический массив"},
        {"Канал", "chan T — обмен между горутинами"},
}

func handleInline(bot *tgbotapi.BotAPI, iq *tgbotapi.InlineQuery) {
        q := strings.ToLower(iq.Query)
        results := make([]interface{}, 0, 3)

        for i, t := range terms {
                if strings.Contains(strings.ToLower(t.Name), q) {
                        r := tgbotapi.NewInlineQueryResultArticle(
                                fmt.Sprintf("term-%d", i),
                                t.Name, t.Meaning)
                        results = append(results, r)
                }
                if len(results) == 3 {
                        break
                }
        }

        cfg := tgbotapi.InlineQueryConfig{
                InlineQueryID: iq.ID,
                Results:       results,
                CacheTime:     300,
        }
        bot.AnswerInlineQuery(cfg)
}

Фильтр нечувствителен к регистру с двух сторон: и запрос, и термин приводятся к нижнему. Идентификатор результата собирается из префикса и индекса — по нему ChosenInlineResult потом опознаёт выбранную запись. Лимит в три результата останавливает проход раньше полного перебора словаря, а CacheTime 300 включает штатное кэширование.

Вариация: форматированные результаты#

Статья из конструктора вставляет плоский текст, а для подсказок с разметкой есть второй конструктор — NewInlineQueryResultArticleHTML: вставляемый текст разбирается как HTML, и в чат уходит сообщение с жирным, курсивом и кодом из тринадцатого урока. Пример словаря выигрывает сразу: значение термина с кодом в теге code выглядит в чате как настоящий фрагмент документации, а не как строка с квадратными скобками. У каждого результата есть ещё два полезных поля: Description — серая строка под заголовком, куда уходит усечённое значение термина, и ThumbnailURL — миниатюра слева от карточки. Один нюанс безопасности наследуется из урока про форматирование: в разметку результата попадает пользовательский запрос? экранируйте его тем же html.EscapeString, иначе скобка из поиска сломает вставку у получателя сообщения.

Типичная ошибка: тяжёлый поиск на каждую букву#

Клиент шлёт InlineQuery на каждое изменение поля: пользователь набрал слово из восьми букв — бот получил восемь запросов, включая одиночную m. Учебный фильтр по сорока терминам это переживёт усмехаясь, но словарь на тысячи записей с запросом в базу на каждую букву положит и Redis, и лимиты API. Лечение многослойное. Сначала честный порог: запросы короче двух букв отвечают пустым списком — клиент всё равно показывает мусор, который хуже отсутствия подсказок. Второй слой — CacheTime: уже включённое кэширование съедает повторные запросы той же строки. Третий — счётчик: тяжёлый поиск оборачивается таймером, который не даёт чаще одного запроса в секунду на пользователя, механика та же, что в rate limiter. Для учебного словаря хватает первых двух слоёв, но привычка считать запросы на каждую букву экономит потом ночные инциденты с настоящей базой.

Поля InlineQuery#

ПолеТипЧто содержит
IDstringИдентификатор запроса — адрес ответа AnswerInlineQuery
From*UserКто ищет: ID, имя, username
QuerystringПоисковый запрос после @имябота
OffsetstringМетка пагинации для длинных списков

ChosenInlineResult: что выбрал пользователь#

AnswerInlineQuery отдаёт подсказки, но не говорит, чем всё кончилось: выбрал ли человек вариант или стёр запрос. Обратная связь приходит отдельным апдейтом — ChosenInlineResult с полями From, ResultID и Query. Для его получения в BotFather включается /setinlinefeedback, и в цикле появляется третья ветка спец-апдейтов. Зачем это боту-словарю: статистика — какие термины ищут, какие подсказки реально вставляют; на этих числах живут и сортировка результатов, и решение о новых статьях словаря. Формат простой: по ResultID — а мы клали туда term-с-индексом — наращиваем счётчик в Redis, и через неделю видно топ терминов. Без включённого фидбека бот тоже работает, просто вслепую: подсказки уходят в никуда, и качество поиска растёт только усилиями автора. Это тот же принцип, что и AnswerCallbackQuery из девятого урока: телеметрия нажатий — половина ценности интерактивных режимов.

Проверка: где работает inline

Где пользователь видит результаты inline-режима бота?

Проверка: тишина в ответ на запросы

Обработчик InlineQuery корректен, но подсказки не приходят. Первое, что стоит проверить?

Сколько подсказок найдёт фильтр

Прочитайте код и выберите, что он выведет:

terms := []string{"map", "slice", "make", "maps"}
q := "ma"
found := 0

for _, t := range terms {
        if strings.Contains(t, q) {
                found++
        }
}

fmt.Println(found)

Соберите ветку inline-ответа

Впишите проверку апдейта, конструктор статьи и метод отправки ответа.

if update. != nil { iq := update.InlineQuery r := tgbotapi.("1", "Карта", "map[K]V") cfg := tgbotapi.InlineQueryConfig{ InlineQueryID: iq.ID, Results: []interface{}{r}, } bot.(cfg) }

Шесть строк inline-ответа

Расставьте ответ на запрос: от чтения апдейта до отправки результата.

Строки

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

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

Карточки: inline-режим

Термин

InlineQuery

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

Апдейт запроса подсказок: ID, From, Query, Offset. Приходит, когда пользователь набирает @имябота в поле ввода любого чата.

1 / 6

Бот появился в поле поиска любого чата — следующий шаг по расширению присутствия: Mini Apps открывают внутри Telegram полноценный веб-интерфейс, а рассылки из четырнадцатой части остаются главным способом вернуться к пользователю, который подсказками только пользуется. Механику нажатий и callback-ответов напомнит урок про inline-клавиатуры, кэширование результатов поиска — тема урока про кэширование в go-redis. Полный маршрут курса — на его странице.

Песочница урока

Редактируйте код и запускайте его прямо здесь — интерпретатор работает в браузере, ничего устанавливать не нужно. За каждый запуск — +2 XP.

Вывод

Запустите код, чтобы увидеть вывод.

Коротко: что запомнить

  • Inline-режим отвечает на @имябота в поле ввода любого чата: бот становится службой подсказок.
  • InlineQuery — апдейт без Message: ветка update.InlineQuery != nil ставится перед веткой сообщений.
  • Ответ AnswerInlineQuery обязателен на каждый запрос, включая пустой, и несёт до 50 результатов-статей.
  • Режим включается /setinline в BotFather: без него апдейты не приходят при полностью рабочем коде.
  • CacheTime кэширует ответы по умолчанию на пять минут: статичным словарям на пользу, живым данным во вред.
  • IsPersonal прячет результаты от участников чата — обязательно для личных данных.
  • ChosenInlineResult с включённым /setinlinefeedback рассказывает, какие подсказки реально выбирают.

Домашнее задание

Включите inline-режим у локального бота через /setinline и соберите словарь из пяти терминов Go: фильтр по Query с ToLower, до трёх статей в ответе, CacheTime 300. Проверьте в любом чате: ввод @имябота с частью термина показывает подсказки, нажатие вставляет значение, пустой запрос оставляет поле чистым. Затем включите /setinlinefeedback и посмотрите в логах, какой ResultID выбирают.

Личные заметкиПусто
Хранятся только в вашем браузере (Telegram · Inline-режим: бот в поле поиска)

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

Можно ли менять подсказки в зависимости от пользователя?

Да: From даёт ID, и для каждого пользователя формируется свой список — плюс IsPersonal прячет результаты от других участников чата. Для общих словарей персонализация обычно лишняя, а для личных документов обязательна.

Какие типы результатов бывают, кроме статей?

Bot API поддерживает фото, видео, GIF, аудио, документы и голосовые результаты — в библиотеке у каждого свой конструктор вида NewInlineQueryResultPhoto. Статья остаётся самым простым типом и покрывает большинство текстовых сценариев.

Почему у Results тип []interface{}, а не []InlineQueryResultArticle?

Ответ может содержать результаты разных типов — статьи, фото, документы, — и сигнатура библиотеки принимает их одним срезом интерфейсов. Статьи кладутся туда без проблем, при смешанных списках просто добавляются конструкторы разных типов.

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

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

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

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

Мониторинг Go с Prometheus: метрики, Grafana, алерты

Курс по метрикам в Go: Counter, Gauge, Histogram, лейблы, PromQL, Grafana-дашборды, алерты и бизнес-метрики. 25 уроков с примерами client_golang.

  • Понимать четыре типа метрик и выбирать правильный
  • Экспортировать метрики из Go через promhttp
  • Инструментировать HTTP-запросы и бизнес-события
  • Писать PromQL-запросы: rate, sum, p99