Найдите все документы, подходящие запросу

C++Beginner
Практиковаться сейчас

Введение

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

Эта независимая работа предоставляет правильный индекс слов и документов. Вы реализуете пересечение для запроса, пересоберёте реальную CLI-программу, отличите нормальный пустой ответ от неверного ввода и поищете после правок учебных заметок. Используются map, set, ASCII-токены и терминальные и файловые операции предыдущей работы. 55 минут — оценка автора.

Оставьте только документы со всеми словами запроса

На этом шаге реализуйте AND на уровне документа: заметка подходит, только если содержит каждое слово запроса, даже на разных строках. Эта работа запускается независимо. Правильный индекс уже есть и не зависит от предыдущего окружения. Пользуйтесь вкладкой Terminal, не терминалом WebIDE. Она открывает оболочку UTF-8 без изменения файлов. cd меняет каталог этой оболочки; повторите его в новом соединении.

cd /home/labex/project/note-search

Готовый пример принимает каталог и запрос. Одинарные кавычки удерживают два слова одним аргументом оболочки. Программа разбирает его по тому же правилу ASCII, что и заметки:

./sample notes 'cat blue'

Ожидаются practice/旅行 notes.txt и reference/archive/notes.txt. Группа архивной заметки содержит:

File: reference/archive/notes.txt
  1: CAT, cat!
  2: Blue ledger — 東京
  3: cat blue together

Каждая строка хотя бы с одним из слов запроса выводится один раз с настоящим номером, начиная с 1. Условие AND относится ко всему документу, не к одной строке. Заметка с cat только на строке 1 и blue только на строке 2 тоже обязана подойти. Повторы или несколько слов на одной строке не дублируют эту строку. Фрагменты сохраняют исходные байты Unicode.

cat показывает файл. Прочитайте интерфейс и подготовленную реализацию индекса:

cat search.hpp
cat provided.inc

Index — map от слова к множеству полных относительных идентификаторов документов. Множество документов слова также называют множеством его документных ссылок. provided.inc даёт правильный buildIndex в этой части. engine.cpp содержит незавершённый findAll:

cat engine.cpp

Сохраните search.hpp, provided.inc, support.cpp, main.cpp, sample и все пути и содержимое notes/reference/. Меняйте engine.cpp; вспомогательные функции и эквивалентные алгоритмы разрешены. Изменяемые данные — только notes/practice/, его начальный состав не обязателен. Загрузчик рекурсивно читает обычные .txt в UTF-8 с LF. Допустимы пробелы и Unicode в путях; табуляции и переводы строк в путях, символические ссылки, NUL-байты и произвольные двоичные файлы вне формата. Ошибки чтения не дают частичных результатов.

Готовый анализатор words() выделяет максимальные последовательности букв ASCII и переводит A-Z в a-z. Цифры, подчёркивания, пунктуация, пробельные символы и каждый байт вне ASCII разделяют токены. cat — не catalog; CAT-blue_42 даёт cat и blue; 猫cat狗 даёт cat; café даёт caf, не cafe. Многоязычная сегментация не заявляется. findAll получает уже нормализованные слова ASCII в нижнем регистре из анализатора, возможно с повторами. Для пустого списка слов он должен безопасно вернуть пустой вектор. Отдельно CLI отвергает сырой запрос без слов ASCII со статусом 2 и без результата в stdout; неизвестное слово или допустимый запрос без совпадений — нормальные 0 и No matching documents.

bash запускает самопроверку; query выбирает полное поведение запроса. Начальный код намеренно не проходит её, обычно с FAIL all documents, not only the first. Ненулевой статус здесь означает незавершённую работу:

bash /opt/labex-note-search/check.sh query

Для AND начните с копии множества документов первого слова. Затем проверяйте множества остальных слов по одному: удаляйте кандидата, как только его нет хотя бы в одном из них. Это пересечение множеств. Если слово неизвестно, ни один документ не может содержать все слова. Не объединяйте все ссылки простым добавлением: получится OR, принимающий заметки только с одним из требуемых слов.

Проверьте terms.empty() до обращения к первому элементу через terms.front(). index.find(word) ищет ключ map без вставки; index.end() означает его отсутствие. Не разыменовывайте отсутствующий элемент. Для найденного элемента first->second или posting->second использует -> для доступа через итератор; second — его значение, множество идентификаторов. Скопируйте его в found, чтобы запрос не менял индекс.

Цикл использует итератор — позицию в множестве. found.begin() — первая позиция; found.end() — позиция после последнего элемента. *it — текущий идентификатор. .count(*it) равно 0, если его нет в другом множестве документов. Удаление делает прежнюю позицию недействительной, поэтому erase(it) возвращает следующий допустимый итератор. Иначе ++it просто продвигается дальше. После удаления не увеличивайте итератор повторно.

Замените engine.cpp следующей реализацией. Как и раньше, here-document записывает весь файл; > заменяет его, а разделитель в кавычках 'CPP' предотвращает раскрытие оболочкой. CPP должен завершать команду отдельной строкой. Включения сохраняют интерфейс и подготовленный правильный индекс.

cat > engine.cpp <<'CPP'
#include "search.hpp"
#include "provided.inc"
std::vector<std::string> findAll(const Index& index,
                                 const std::vector<std::string>& terms) {
    if (terms.empty()) return {};
    auto first = index.find(terms.front());
    if (first == index.end()) return {};
    std::set<std::string> found = first->second;
    for (const auto& term : terms) {
        auto posting = index.find(term);
        if (posting == index.end()) return {};
        for (auto it = found.begin(); it != found.end();) {
            if (posting->second.count(*it) == 0) it = found.erase(it);
            else ++it;
        }
    }
    return std::vector<std::string>(found.begin(), found.end());
}
CPP

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

Скомпилируйте текущий исходник. g++ компилирует три файла .cpp; -std=c++17 выбирает C++17, -Wall -Wextra включает предупреждения, а -o задаёт имя программы. Включаемые файлы автоматически компилируются через engine.cpp:

g++ -std=c++17 -Wall -Wextra engine.cpp support.cpp main.cpp -o findnotes

Успех обычно ничего не выводит. При неудаче прочитайте первое сообщение о файле и строке, исправьте engine.cpp или повторите замену и соберите снова. После неудачной компиляции может остаться старый бинарный файл; проверяются функции и реально существующий findnotes, без его пересборки. Отсутствующая или устаревшая программа не является завершённым результатом.

./findnotes notes 'cat blue'

Сравните полные группы и строки-доказательства с примером. Подготовленный вывод использует возвращённые идентификаторы для выбора настоящих строк оригинала; изменение исходных заметок или печать фиксированного успеха не реализует поиск.

Отличите пустой результат от неверного запроса

На этом шаге проверьте повторяющиеся слова, границы токенов, ошибки и перестроение по текущим файлам. Оставайтесь в Terminal в каталоге проекта. Готовая функция не должна менять вход const Index& или дублировать документы при повторе слова:

./findnotes notes 'cat cat blue'

Группы и строки такие же, как для cat blue. Теперь добавьте неизвестное слово:

./findnotes notes 'cat absent'

Ожидается No matching documents.. Документа только с cat недостаточно. Запрос допустим, но ответа нет, поэтому код завершения равен 0. Сразу после команды переменная оболочки $? хранит её код; двойные кавычки раскрывают его одним аргументом. printf показывает значение, а \n завершает строку вывода:

printf 'Exit status: %s\n' "$?"

Выводится Exit status: 0. Проверьте приведение регистра и разделители:

./findnotes notes 'CAT-blue_42'

Это означает то же, что cat blue. Цифры и подчёркивания не входят в токены. Поиск целого слова не является поиском подстроки:

./findnotes notes catalog

Подходят только reference/catalog.txt и учебная заметка с catalog. Это не означает cat.

Пустая строка записывается двумя соседними одинарными кавычками, ''. Это переданный, но пустой аргумент оболочки. Следующая команда — намеренная ошибка: она печатает Query needs at least one ASCII word в stderr и не выводит результат в stdout:

./findnotes notes ''

Сразу прочитайте её статус:

printf 'Exit status: %s\n' "$?"

Выводится 2. Запрос только из цифр, разделителей и не-ASCII-символов неверен по той же причине:

./findnotes notes '42_東京'

Это тоже намеренная ошибка со статусом 2. Она не означает поломку хранения Unicode: в запросе просто нет поддерживаемого ASCII-слова. Для восстановления передайте запрос со словом, не меняя оригинал или анализатор.

Опечатка в имени каталога — другая ошибка. Следующая команда намеренно указывает несуществующий каталог:

./findnotes missing-notes cat

Она печатает Cannot read notes directory и завершается с 2 без частичного результата. При необходимости посмотрите реальные пути через find notes -type f, затем восстановите работу с правильным корнем:

./findnotes notes cat

Обычные результаты возвращаются. То же правило «полное чтение либо ошибка» действует, если не читается любой исходный файл. А пустой, но читаемый каталог заметок допустим и даёт отсутствие совпадений.

Теперь измените подготовленную учебную копию, доступную для записи. Одинарные кавычки сохраняют имя с пробелами и Unicode. printf пишет текст, \n создаёт LF-переводы строк, > заменяет этот учебный файл. Оригиналы в reference остаются нетронутыми:

printf 'cat green\n東京 — preserved\n' > 'notes/practice/旅行 notes.txt'
./findnotes notes 'cat blue'

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

Добавьте заметку со словами запроса на разных строках:

printf 'BLUE here\ncat elsewhere 東京\n' > 'notes/practice/new notes.txt'
./findnotes notes 'cat blue'

Теперь подходит practice/new notes.txt, показывая строки 1 и 2 вместе с архивным результатом. Это прямое подтверждение документного AND между строками. rm удаляет только указанный учебный файл:

rm 'notes/practice/new notes.txt'
./findnotes notes 'cat blue'

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

Выполните самопроверку. Она читает текущий проект и пробует другие входы на временных данных; она не чинит бинарный файл, не редактирует реальные заметки и не сбрасывает прогресс. Пробы неверного ввода могут печатать ожидаемые ошибки рядом с PASS; успех заканчивается CLI CHECKS PASSED:

bash /opt/labex-note-search/check.sh query

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

Резюме

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