Перейти к содержанию

Подсветка совпадений без исполнения HTML

Карточка с одним заголовком не всегда объясняет, почему документ появился в поиске. Термин может находиться в середине длинной статьи. Добавим подсветку и короткие фрагменты основного текста. Результат урока — передача выделенных участков и их отображение в браузере как текста с элементами mark, без исполнения содержимого документа как HTML.

Работаем с query.py и web/index.html итогового проекта. Браузер и поисковые запросы при написании не запускались. Ожидаемые фрагменты иллюстрируют форму ответа; конкретное предложение выбирает движок по содержимому и запросу. Документация highlight определяет параметры этой операции.

Фрагмент относится к исходному полю

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

Для body оставляем _source, откуда highlighter может получить содержимое. Другие способы хранения позиций и смещений требуют дополнительных настроек и места. В небольшой библиотеке не добавляем их заранее ради неподтверждённого ускорения. Сначала нужен работоспособный договор фрагмента; способ получения смещений можно обсуждать при известных размерах корпуса.

Фрагмент полного запроса build_query выглядит так:

'highlight': {
    'pre_tags': ['[[pw-hit]]'],
    'post_tags': ['[[/pw-hit]]'],
    'fields': {'body': {
        'fragment_size': 180,
        'number_of_fragments': 2
    }}
}

Вместо HTML-тега используем собственные текстовые маркеры. Это не способ очистки опасного HTML; это способ передать небольшую разметку совпадений отдельно от исполнения. Ограничиваем число фрагментов, чтобы одна карточка не стала второй полной статьёй. Размер фрагмента является настройкой работы highlighter, а не гарантией одинаковой длины любого возвращённого предложения.

Предположим, документ содержит «Браузер проверяет HTTP-заголовок перед повторной загрузкой». Ожидаемая иллюстрация фрагмента при запросе HTTP:

Браузер проверяет [[pw-hit]]HTTP[[/pw-hit]]-заголовок перед повторной загрузкой.

Из этого текста интерфейс создаёт обычные текстовые узлы и mark вокруг выделенной части. Дефис и продолжение предложения остаются текстом. Важно, что пользователю показываем предложение, а не внутренний стем или значение score. Так карточка объясняет совпадение средствами самого документа.

Почему вставка innerHTML меняет доверие

В библиотеке может находиться пример <script>, HTML-атрибут или фрагмент с угловыми скобками. Для урока по веб-разработке такое содержимое нормально. Но вставка полученного фрагмента через innerHTML заставляет браузер интерпретировать его как разметку страницы поиска. Исходник статьи становится участником интерфейса с другой границей доверия.

Поэтому функция snippet разбивает текст по известным маркерам и назначает каждому участку textContent. Полный файл присутствует в архиве; ниже центральный фрагмент, внутри которого переменная active показывает состояние выделения, а part содержит очередную часть:

const node = active
  ? document.createElement('mark')
  : document.createElement('span');
node.textContent = part;
p.append(node);

Ни одна строка документа не превращается в имя элемента или атрибут события. Даже если исходник содержит собственный маркер [[pw-hit]], он может повлиять лишь на визуальную границу выделения, а не создать исполняемый код. Для строгого оформления маркеры можно дополнительно экранировать или использовать структурированные сегменты, но безопасность здесь обеспечивается текстовой вставкой.

Проверка адреса карточки имеет отдельную роль. title отображается как текст, а url используется в ссылке только после сопоставления с известной локальной формой. Без этого безопасный заголовок всё равно мог бы вести по неподходящей схеме URL. Две проверки защищают разные действия браузера и не заменяют друг друга.

Совпадение не всегда даёт фрагмент

Документ может попасть в выдачу благодаря заголовку, тогда в body не будет подходящего выделения. Функция public_response использует начало тела как запасной текст. Это ожидаемая ситуация: подсветка определена только для body, а поиск выполняется в нескольких полях. Отсутствие highlight.body не доказывает неисправность поискового запроса.

Запасной фрагмент тоже вставляется через textContent. Нельзя считать начало документа безопасным только потому, что его выбирало приложение, а не highlighter. Оно остаётся содержимым исходного файла и может включать примеры разметки. Одно правило отображения проще поддерживать, чем два режима доверия для почти одинаковых строк.

Подсветка также не является точным доказательством всех логических условий запроса. Сложные комбинации условий и несколько полей могут дать выделение отдельных слов без наглядного показа всей причины ранга. Поэтому не подписываем фрагмент как исчерпывающее объяснение score. Его цель — помочь читателю понять контекст и решить, открывать ли статью.

Особенно осторожно относитесь к запросам с языковым анализом. Поиск по форме «запросы» может выделить исходное «запроса». Для человека это полезная связь, но она не означает буквальное наличие введённой строки. Если продукт обещает буквальный поиск по цитате, такой режим следует определить отдельно и проверить его анализатор, поле и условие фразы.

Ограничение работы над длинным текстом

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

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

При будущем просмотре проверьте три содержательных случая: совпадение в body, совпадение только в title и текст с угловыми скобками. В первом ожидается выделение, во втором запасной фрагмент, в третьем скобки остаются видимыми символами. Эти наблюдения относятся к результату интерфейса; автор их не выполнял и не выдаёт за готовые скриншоты.

Запрос только по technical-подполю может дать иной набор совпадений, чем подсветка основного body. Поиск и highlighter используют связанные, но не тождественные представления. Если приложение обещает выделить каждое основание совпадения, потребуется согласовать поля и дополнительные настройки подсветки. Наш договор скромнее: показывается доступный фрагмент body либо его начало. Поэтому отсутствие mark не подписывается как ошибка или доказательство нерелевантной карточки.

После урока поисковая карточка показывает контекст совпадения и сохраняет текстовую природу исходника. Добавьте в собственный документ пример HTML и предскажите, какие DOM-элементы создаст snippet: только span и mark. В следующем уроке разберём управление порядком через фразу, веса полей и контрольные запросы.

Оглавление серии