Вайбкодинг – не для гуманитариев? Юрист сделал кривой поиск по PDF и просит помощи…
- суббота, 28 февраля 2026 г. в 00:00:13
Я столкнулся с простой (как мне изначально показалось – даже очень) задачкой. Мне в последнее время потребовалось часто проводить поиск в 4 словарях. Государство мне их дало в виде пяти PDF файлов, выложенных онлайн. Это нормативные словари русского языка, слова из которых можно использовать в публичном пространстве после 1 марта 2026 г.
Например, caсhe - можно использовать на русском как кэш, а не переводить как тайник или склад (не путать с cash как наличные), поскольку в Словарь иностранных слов это слово уже включено. И это слово нам еще пригодится далее по тексту)

Один раз в 10 лет я призываю силу Хабра на помощь. Прошлый раз мне сильно помогли в моей попытке сделать захабренный договор. Надеюсь сейчас тоже сработает.
Итак, открыл я все файлы сначала в браузере, потом в Adobe Acrobat и стал заводить поисковые запросы в каждом окне. Подустал уже после второго запроса и подумал, что должен же быть способ объединить их в один массив и выдавать результат сразу по всем словарям.
При этом мне нужно, чтобы в результатах выдачи по каждому совпадению показывался фрагмент текста с найденным словом, название словаря (оно же название файла) и номер страницы в файле, где найдено совпадение.
В идеале – быстрый переход по ссылке сразу на ту страницу, где найдено совпадение. Это нужно, чтобы доказывать проверяющим органам, что имеешь право юзать англицизм, вошедший в употребление, а точнее в один из 4 нормативных словарей, утвержденных Правительством РФ.
Я посмотрел кучу роликов в интернете как легко можно делать с помощью ИИ решения и подумал – справлюсь сам и навайбкодю. Ко второму дню начал подозревать, что в роликах про настройку агентов за 2 минуты немного обманывают.
Попытка привлечь 15-летнего сына, который ходит на олимпиады по программированию, не увенчалась успехом. Не дослушав задачу до конца, он сказал, что вообще ни разу не программист и мне следует привлечь профессионала. Но мы-то не ищем легких путей.
Сразу же решил делать решение доступным онлайн, чтобы команда и клиенты тоже могли пользоваться таким удобным поиском. Сайт у меня на Тильде, поэтому решение заключалось в разработке html-кода для вставки в блок T123.
В помощники я взял себе ChatGPT, Gemini и Claude. Все в лоб дали вот такой вариант (с небольшими вариациями) – ниже цитата из GPT
, собранном на Тильде, можно использовать несколько подходов. Вот шаги для реализации:
1. Загрузка PDF в облако
Загрузи свои PDF-документы на облачный сервис (например, Google Drive, Dropbox или любой другой сервис для хранения файлов). Обрати внимание, что лучше использовать ссылку с возможностью общего доступа, чтобы все могли видеть файлы.
2. Создание индекса для поиска
Чтобы пользователь мог искать по содержимому документов, можно использовать сервисы, которые позволяют индексировать PDF-файлы и строить поисковый функционал:
Google Custom Search:
Создаешь поисковую систему в Google, указывая ссылку на файлы, расположенные в облаке.
Получаешь код для встраивания на страницу Тильды.
Встраиваешь код на нужную страницу в Тильде через HTML-блок.
Searchable PDF: Есть сервисы и инструменты, которые могут конвертировать PDF-документы в индексируемый текст. Ты можешь использовать такие инструменты, чтобы создать индекс или текстовую базу данных для поиска по этим документам.
3. Добавление формы поиска на Тильде
Чтобы разместить поиск на странице Тильды, нужно добавить HTML/JavaScript код:
На Тильде выбери блок с HTML-кодом.
Вставь туда код для поиска, который ты получишь после настройки поисковой системы в Google или другого сервиса.
Чем меня не устроил этот вариант – после публикации не было никаких результатов поиска. На вопрос: "Где результаты?" – ИИ ответил, что нужно время на индексацию документов и это займет от нескольких часов до нескольких дней. Кстати, на второй день результаты стали появляться, но пока точно не все совпадения ищет. В этой части эксперимент продолжается.
Вторым решением, которое пока остается рабочим, было сделать собственный поиск по документам, размещенным онлайн.
Claude дал мне полный код, который заработал сразу после устранения пары ошибок.
<style> *, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; } #pdf-search-widget { width: 100%; max-width: 780px; margin: 0 auto; font-family: 'Georgia', serif; } .psw-header { margin-bottom: 28px; text-align: center; } .psw-header h2 { font-size: clamp(22px, 4vw, 34px); font-weight: normal; color: #1a1a1a; letter-spacing: -0.5px; } .psw-header p { margin-top: 6px; font-size: 14px; color: #888; font-family: 'Courier New', monospace; } .psw-search-wrap { position: relative; margin-bottom: 10px; } .psw-search-wrap input { width: 100%; padding: 16px 56px 16px 20px; font-family: 'Georgia', serif; font-size: 17px; border: 2px solid #1a1a1a; background: #fff; color: #1a1a1a; outline: none; transition: border-color 0.2s, box-shadow 0.2s; } .psw-search-wrap input::placeholder { color: #bbb; } .psw-search-wrap input:focus { border-color: #c8502a; box-shadow: 4px 4px 0 #c8502a; } .psw-search-icon { position: absolute; right: 18px; top: 50%; transform: translateY(-50%); color: #bbb; pointer-events: none; font-size: 20px; } .psw-clear-btn { position: absolute; right: 16px; top: 50%; transform: translateY(-50%); background: none; border: none; font-size: 22px; cursor: pointer; color: #999; line-height: 1; display: none; padding: 2px 4px; } .psw-clear-btn:hover { color: #c8502a; } .psw-status { font-family: 'Courier New', monospace; font-size: 12px; color: #999; margin-bottom: 18px; min-height: 18px; display: flex; align-items: center; gap: 8px; } .psw-dot { width: 8px; height: 8px; border-radius: 50%; background: #ddd; flex-shrink: 0; } .psw-dot.loading { background: #f0a830; animation: psw-pulse 0.9s ease-in-out infinite; } .psw-dot.ready { background: #4caf7d; } .psw-dot.error { background: #e05a5a; } @keyframes psw-pulse { 0%,100%{opacity:1} 50%{opacity:0.3} } .psw-progress-bar { height: 2px; background: #eee; margin-bottom: 18px; overflow: hidden; display: none; } .psw-progress-fill { height: 100%; background: linear-gradient(90deg, #c8502a, #f0a830); width: 0%; transition: width 0.3s ease; } .psw-results { display: flex; flex-direction: column; gap: 12px; } .psw-result-card { background: #fff; border: 1.5px solid #e0dcd4; padding: 18px 20px; cursor: pointer; transition: border-color 0.15s, transform 0.1s, box-shadow 0.15s; } .psw-result-card:hover { border-color: #c8502a; box-shadow: 4px 4px 0 rgba(200,80,42,0.13); transform: translateX(-2px); } .psw-card-top { display: flex; justify-content: space-between; align-items: flex-start; gap: 12px; margin-bottom: 8px; } .psw-doc-name { font-family: 'Courier New', monospace; font-size: 12px; font-weight: bold; color: #c8502a; text-transform: uppercase; letter-spacing: 0.5px; flex: 1; } .psw-page-badge { background: #1a1a1a; color: #f4f1eb; font-family: 'Courier New', monospace; font-size: 11px; padding: 2px 8px; white-space: nowrap; flex-shrink: 0; } .psw-snippet { font-family: 'Georgia', serif; font-size: 14px; color: #444; line-height: 1.65; } .psw-snippet mark { background: #fff0a0; color: #1a1a1a; padding: 0 2px; border-radius: 2px; } .psw-card-footer { margin-top: 12px; display: flex; align-items: center; gap: 12px; } .psw-dl-link { display: inline-flex; align-items: center; gap: 5px; font-family: 'Courier New', monospace; font-size: 12px; color: #1a1a1a; text-decoration: none; border-bottom: 1px solid #ccc; padding-bottom: 1px; transition: color 0.15s, border-color 0.15s; } .psw-dl-link:hover { color: #c8502a; border-color: #c8502a; } .psw-more-count { font-family: 'Courier New', monospace; font-size: 11px; color: #aaa; } .psw-empty { text-align: center; padding: 40px 20px; color: #bbb; font-family: 'Courier New', monospace; font-size: 13px; border: 1.5px dashed #ddd; } </style> <div id="pdf-search-widget"> <div class="psw-header"> <h2>Поиск по документам</h2> <p id="psw-doc-count-label">инициализация...</p> </div> <div class="psw-search-wrap"> <input type="text" id="psw-input" placeholder="Введите слово или фразу..." autocomplete="off" /> <span class="psw-search-icon" id="psw-icon">⌕</span> <button class="psw-clear-btn" id="psw-clear" title="Очистить">×</button> </div> <div class="psw-progress-bar" id="psw-progress-bar"> <div class="psw-progress-fill" id="psw-progress-fill"></div> </div> <div class="psw-status" id="psw-status"> <span class="psw-dot loading" id="psw-dot"></span> <span id="psw-status-text">Загружаем документы...</span> </div> <div class="psw-results" id="psw-results"></div> </div> <script src="https://cdnjs.cloudflare.com/ajax/libs/pdf.js/3.11.174/pdf.min.js"></script> <script> // ================================================ // НАСТРОЙКИ — замени на свои ссылки и названия // ================================================ var PDF_SOURCES = [ { name: 'Орфографический словарь', url: 'https://5b6cun312m.ucarecd.net/d0775608-a981-40e9-9699-f412433c40cc/orfograficheskij_slovar.pdf' }, { name: 'Орфоэпический словарь', url: 'https://5b6cun312m.ucarecd.net/9e5bbd55-48b3-467a-bbf2-328351ccf695/orfoepicheskij_slovar.pdf' }, { name: 'Словарь иностранных слов', url: 'https://5b6cun312m.ucarecd.net/2416bd08-c569-4f7f-a396-0e2617538988/slovar_inostr_slov.pdf' }, { name: 'Толковый словарь А-Н', url: 'https://5b6cun312m.ucarecd.net/6fb87d14-05f8-409a-983a-21b71808afb4/tolkovyj_slovar_chast1_AN.pdf' }, { name: 'Толковый словарь О-Я', url: 'https://5b6cun312m.ucarecd.net/8791e39d-ed5d-48be-a9d8-4122825c5e98/tolkovyj_slovar_chast2_OJa.pdf' }, ]; var MAX_RESULTS = 30; var SNIPPET_RADIUS = 120; // ================================================ pdfjsLib.GlobalWorkerOptions.workerSrc = 'https://cdnjs.cloudflare.com/ajax/libs/pdf.js/3.11.174/pdf.worker.min.js'; var input = document.getElementById('psw-input'); var resultsEl = document.getElementById('psw-results'); var statusText = document.getElementById('psw-status-text'); var statusDot = document.getElementById('psw-dot'); var clearBtn = document.getElementById('psw-clear'); var iconEl = document.getElementById('psw-icon'); var progressBar = document.getElementById('psw-progress-bar'); var progressFill = document.getElementById('psw-progress-fill'); var docLabel = document.getElementById('psw-doc-count-label'); var index = []; var ready = false; function setStatus(msg, state) { statusText.textContent = msg; statusDot.className = 'psw-dot ' + (state || 'loading'); } function escHtml(str) { return String(str).replace(/[&<>"']/g, function(c) { return {'&':'&','<':'<','>':'>','"':'"',"'":'''}[c]; }); } function highlight(text, query) { var esc = query.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); return escHtml(text).replace(new RegExp('(' + esc + ')', 'gi'), '<mark>$1</mark>'); } function getSnippet(text, query) { var idx = text.toLowerCase().indexOf(query.toLowerCase()); if (idx === -1) return escHtml(text.slice(0, SNIPPET_RADIUS * 2)) + '…'; var start = Math.max(0, idx - SNIPPET_RADIUS); var end = Math.min(text.length, idx + query.length + SNIPPET_RADIUS); var snippet = text.slice(start, end); if (start > 0) snippet = '…' + snippet; if (end < text.length) snippet = snippet + '…'; return highlight(snippet, query); } function loadAllPDFs() { progressBar.style.display = 'block'; var loaded = 0; function loadNext(i) { if (i >= PDF_SOURCES.length) { ready = true; progressBar.style.display = 'none'; var totalPages = index.filter(function(e){ return e.page > 0; }).length; setStatus('Проиндексировано ' + totalPages + ' стр. в ' + PDF_SOURCES.length + ' докум.', 'ready'); docLabel.textContent = PDF_SOURCES.length + ' документ' + (PDF_SOURCES.length === 1 ? '' : PDF_SOURCES.length < 5 ? 'а' : 'ов') + ' · ' + totalPages + ' страниц'; return; } var source = PDF_SOURCES[i]; setStatus('Загружаем «' + source.name + '»...', 'loading'); pdfjsLib.getDocument({ url: source.url, withCredentials: false }).promise .then(function(pdf) { var pageNum = 1; function nextPage() { if (pageNum > pdf.numPages) { loaded++; progressFill.style.width = (loaded / PDF_SOURCES.length * 100) + '%'; loadNext(i + 1); return; } pdf.getPage(pageNum).then(function(page) { page.getTextContent().then(function(content) { var text = content.items.map(function(it){ return it.str; }).join(' ').trim(); var textNoSpaces = text.replace(/\s+/g, ''); if (text.length > 10) { index.push({ docName: source.name, url: source.url, page: pageNum, text: text, textNoSpaces: textNoSpaces }); } pageNum++; nextPage(); }); }); } nextPage(); }) .catch(function(e) { console.warn('Ошибка загрузки ' + source.name + ':', e); index.push({ docName: source.name, url: source.url, page: 0, text: '[Не удалось загрузить]' }); loaded++; progressFill.style.width = (loaded / PDF_SOURCES.length * 100) + '%'; loadNext(i + 1); }); } loadNext(0); } function search(query) { resultsEl.innerHTML = ''; if (!query.trim()) return; if (!ready) { setStatus('Подождите, индексируем...', 'loading'); return; } var q = query.trim().toLowerCase(); var qNoSpaces = q.replace(/\s+/g, ''); var hits = index.filter(function(e){ return e.text.toLowerCase().indexOf(q) !== -1 || e.textNoSpaces.toLowerCase().indexOf(qNoSpaces) !== -1; }); if (hits.length === 0) { resultsEl.innerHTML = '<div class="psw-empty">Ничего не найдено по запросу «' + escHtml(query) + '»</div>'; setStatus('0 совпадений', 'ready'); return; } var grouped = {}; hits.forEach(function(h) { if (!grouped[h.docName]) grouped[h.docName] = []; grouped[h.docName].push(h); }); var cardCount = 0; var docNames = Object.keys(grouped); docNames.forEach(function(docName) { if (cardCount >= MAX_RESULTS) return; var docHits = grouped[docName]; var shown = docHits.slice(0, 3); var extra = docHits.length - shown.length; shown.forEach(function(hit, idx2) { if (cardCount >= MAX_RESULTS) return; cardCount++; var card = document.createElement('div'); card.className = 'psw-result-card'; var extraHtml = (extra > 0 && idx2 === shown.length - 1) ? '<span class="psw-more-count">+ ещё ' + extra + ' совпад. в этом документе</span>' : ''; card.innerHTML = '<div class="psw-card-top">' + '<span class="psw-doc-name">' + escHtml(docName) + '</span>' + (hit.page > 0 ? '<span class="psw-page-badge">стр. ' + hit.page + '</span>' : '') + '</div>' + '<div class="psw-snippet">' + getSnippet(hit.text, query.trim()) + '</div>' + '<div class="psw-card-footer">' + '<a class="psw-dl-link" href="' + escHtml(hit.url) + '" target="_blank" download>↓ Скачать PDF</a>' + extraHtml + '</div>'; (function(h) { card.addEventListener('click', function(e) { if (e.target.closest('.psw-dl-link')) return; var pageHash = h.page > 1 ? '#page=' + h.page : ''; window.open(h.url + pageHash, '_blank'); }); })(hit); resultsEl.appendChild(card); }); }); setStatus(hits.length + ' совпад. в ' + docNames.length + ' докум.', 'ready'); } var debounceTimer; input.addEventListener('input', function() { var val = input.value; clearBtn.style.display = val ? 'block' : 'none'; iconEl.style.display = val ? 'none' : 'block'; clearTimeout(debounceTimer); debounceTimer = setTimeout(function(){ search(val); }, 250); }); clearBtn.addEventListener('click', function() { input.value = ''; clearBtn.style.display = 'none'; iconEl.style.display = 'block'; resultsEl.innerHTML = ''; if (ready) setStatus('Готов к поиску', 'ready'); input.focus(); }); input.addEventListener('keydown', function(e) { if (e.key === 'Escape') clearBtn.click(); }); loadAllPDFs(); </script>
Но обнаружилась несколько проблем.
1. Чтобы запустить поиск первый раз нужно подождать около минуты. И так при каждой новой загрузке страницы, поскольку идет "инициализация" и загрузка файлов.

2. В файлах слова указаны с ударением и при поиске ударная гласная не воспринимается как обычная буква. В результатах поиска она как будто отделена пробелами. Соответственно полное слово не выдается в поиске.
Пример – ищем ОВЕРСАЙЗ

Видите Оверсайза?
- Нет.
- И я не вижу.
- А он есть.


ИИ говорит, вообще не проблема – перепиши код, пробелы будем учитывать при поиске. Вот тебе новый код на замену. Переписал, но что-то все равно не получается. Хочется, чтобы поиск был с учетом ударений и не исключал слова.
Спрашиваю у ИИ: а можно как-то побыстрее? Ну, говорит, можно и по-взрослому, но для этого нужен Python. Тогда все будет летать за секунды. Далее опять цитата
Браузер грузит не 163 МБ PDF, а 2-5 МБ чистого текста.
Как это работает: я пишу тебе Python-скрипт, ты запускаешь его один раз на своём компьютере, он вытаскивает весь текст из PDF и сохраняет в index.json. Этот файл кладёшь на Uploadcare, виджет грузит только его.
Плюсы: загрузка за 1-2 секунды вместо минуты, работает без кэша. Минус: при обновлении PDF нужно перегенерировать JSON заново.
И затем убойный вопрос: Python есть на твоём компьютере? Если да — за 5 минут настроим. Ага, спросил он у гуманитария. Тут настал предел моим компетенция и терпению.
Скажите, стоит мне дальше мучать ИИ и продолжать описывать свои мучения (подозреваю, что для специалистов может выглядеть смешно) или подросток, не верящий в силу интеллекта родителя и искусственных интеллектов был прав?
Если есть у вас желание помочь и дать дельный совет, буду рад увидеть в комментариях.
P.S. Пост буду дописывать.
P.P.S. Боюсь публиковать ссылку на поиск - забанят.