질의에 맞는 모든 문서 찾기

C++Beginner
지금 연습하기

소개

기억하는 두 단어로 검색하면 두 단어를 모두 포함한 메모로 범위가 좁혀져야 합니다. 단어는 서로 다른 줄에 있어도 되며, 같은 파일 이름의 두 메모도 서로 구별되어야 합니다.

이 독립 실습은 올바른 단어-문서 색인을 제공합니다. 질의 교집합을 구현하고, 실제 CLI 실행 파일을 다시 빌드하고, 정상적인 빈 결과와 잘못된 입력을 구별하며, 연습용 메모를 편집한 뒤 검색합니다. 이전 실습의 map, set, ASCII 토큰과 터미널·파일 작업을 바탕으로 합니다. 55분은 작성자의 예상 시간입니다.

모든 단어에 공통으로 속하는 문서만 남기기

이 단계에서는 문서 단위 AND를 구현합니다. 검색어가 서로 다른 줄에 있어도 메모에 모든 검색어가 있어야 일치합니다. 이 실습은 독립적으로 시작하며, 올바른 색인이 이미 제공되므로 이전 환경에 의존하지 않습니다. WebIDE 내부 터미널 대신 Terminal 탭을 사용하세요. 파일을 바꾸지 않고 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 판단은 한 줄이 아니라 문서 전체에 적용됩니다. 1번 줄에만 cat이 있고 2번 줄에만 blue가 있는 메모도 일치해야 합니다. 반복 출현이나 같은 줄의 두 검색어 때문에 줄이 중복되지 않습니다. 표시되는 원문 조각의 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/뿐이고 처음 파일 구성은 유지할 필요가 없습니다. 준비된 로더는 일반 UTF-8 LF .txt 파일을 재귀적으로 읽습니다. 경로의 공백과 Unicode는 지원하지만 경로의 탭·줄바꿈, 심볼릭 링크, NUL 바이트와 임의의 바이너리 파일은 입력 형식 밖입니다. 읽기에 실패하면 부분 검색 결과 없이 오류가 납니다.

준비된 분석기 words()는 ASCII 문자의 최대 연속 구간을 만들고 A-Z를 a-z로 바꿉니다. 숫자, 밑줄, 문장 부호, 공백과 모든 비ASCII 바이트가 토큰 경계입니다. cat은 catalog가 아니며, CAT-blue_42는 cat과 blue, 猫cat狗는 cat, café는 cafe가 아닌 caf를 만듭니다. 다국어 단어 분할을 제공한다는 뜻은 아닙니다. findAll은 이 분석기에서 이미 정규화된 소문자 ASCII 검색어를 받으며 중복이 있을 수 있습니다. 검색어 목록이 비었으면 안전하게 빈 벡터를 반환해야 합니다. 이와 별개로 CLI는 ASCII 단어가 없는 원시 질의를 종료 상태 2와 결과 stdout 없이 거부합니다. 알 수 없는 단어나 일치하지 않는 유효한 질의는 정상 종료 0과 No matching documents.입니다.

bash는 자체 검사를 실행하고 query는 완전한 질의 동작을 선택합니다. 시작 코드는 의도적으로 실패하며 보통 FAIL all documents, not only the first에서 멈춥니다. 지금의 0이 아닌 상태는 아직 할 일이 있다는 뜻입니다.

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

AND 질의는 첫 단어의 포스팅 집합 사본에서 시작합니다. 나머지 검색어 집합을 하나씩 확인하여, 후보 식별자가 그중 하나에라도 없으면 제거하세요. 이것이 집합의 교집합입니다. 알 수 없는 검색어가 있으면 모든 검색어를 포함한 문서는 없습니다. 모든 포스팅을 단순히 더해 합치지 마세요. 그러면 요청한 단어 하나만 포함해도 허용하는 OR가 됩니다.

terms.front()로 첫 요소에 접근하기 전에 terms.empty()를 확인해야 합니다. index.find(word)는 삽입 없이 map의 키를 찾으며 index.end()는 해당 키가 없다는 뜻입니다. 없는 항목을 역참조하지 마세요. 찾은 항목에 대해 first->second 또는 posting->second의 ->는 반복자를 통해 항목에 접근합니다. second는 항목의 값인 ID 집합입니다. 질의가 색인을 바꾸지 않도록 이 집합을 found에 복사하세요.

반복문은 집합 안의 위치인 반복자를 사용합니다. found.begin()은 첫 위치이며 found.end()는 마지막 다음 위치입니다. *it는 현재 ID입니다. 다른 포스팅 집합에 ID가 없으면 .count(*it)는 0입니다. 삭제는 이전 위치를 무효화하므로 erase(it)는 다음 유효한 반복자를 반환합니다. 그렇지 않으면 ++it로 정상적으로 이동합니다. 삭제 후 두 번 증가시키지 마세요.

다음 구현으로 engine.cpp를 교체하세요. 이전 실습처럼 here-document는 파일 전체를 쓰며 >는 교체하고 따옴표로 감싼 'CPP' 구분자는 셸 확장을 막습니다. 마지막에는 한 줄에 CPP만 써야 합니다. include는 인터페이스와 준비된 올바른 색인을 유지합니다.

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

마지막 생성자는 남은 집합 범위를 벡터에 복사합니다. 집합의 ID는 고유하므로 문서마다 한 번만 반환됩니다. 반복문에서 첫 검색어를 다시 확인해도 괜찮습니다. 집합과 자기 자신의 교집합은 변하지 않기 때문입니다. 같은 이유로 검색어 반복도 무해합니다. 빈 교집합은 자연스럽게 빈 벡터가 됩니다. 경로 내림차순을 포함한 재현 가능한 다른 그룹 순서도 허용됩니다. 각 그룹 안의 줄 번호는 계속 증가해야 합니다.

현재 소스를 컴파일하세요. 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'

전체 그룹과 근거가 되는 줄을 예제와 비교하세요. 준비된 표시 코드는 반환한 ID로 실제 원본 줄을 선택합니다. 원본 메모를 바꾸거나 고정된 성공 텍스트를 출력하는 것은 검색 구현이 아닙니다.

빈 결과와 잘못된 질의 구별하기

이 단계에서는 반복 단어, 토큰 경계, 오류와 현재 파일 기반 재구축을 시험합니다. 프로젝트 디렉터리의 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이라는 뜻이 아닙니다.

빈 문자열은 붙어 있는 작은따옴표 두 개 ''로 씁니다. 전달되었지만 비어 있는 셸 인수입니다. 다음 명령은 의도적인 실패이며 stderr에 Query needs at least one ASCII word를 출력하고 결과 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를 구현했습니다. 반복 검색어는 무해하고, 알 수 없는 단어는 정상적인 빈 결과를 내며, 잘못된 질의나 읽을 수 없는 입력은 명시적으로 실패합니다. 기존 실행 파일은 이제 현재 연습용 데이터를 읽고 관련 원본 줄을 각각 한 번 보여 줍니다. 챌린지에서는 다른 메모 모음에서 이 질의 동작과 색인 구성을 결합합니다.