Construire un index d'identifiants de documents

C++Beginner
Pratiquer maintenant

Introduction

Vous vous souvenez d'un mot d'une note, mais plus de son répertoire. Un outil de recherche utile doit retrouver toutes les notes correspondantes, distinguer les fichiers portant le même nom et afficher des lignes réelles que vous pouvez consulter dans l'original.

Construisez cette relation avec les tables associatives et les ensembles de C++. Le chargement, l'analyse des mots, les requêtes et l'affichage des lignes sont préparés ; vous implémenterez l'index et l'essaierez avec des notes d'exercice modifiables. Les prérequis sont la navigation élémentaire dans un terminal, les chaînes, vecteurs, fonctions, flux de fichiers et exceptions C++. Les tables associatives et ensembles sont enseignés ici. Les 50 minutes sont une estimation de l'auteur.

Associer un mot à toutes les notes

Dans cette étape, utilisez un exemple fonctionnel, puis construisez l'index des mots vers les documents pour votre exécutable. Utilisez l'onglet Terminal tout au long du cours. Il ouvre un shell UTF-8 ; choisissez cet onglet nommé plutôt qu'un terminal dans WebIDE. Le répertoire du shell est conservé pendant cette connexion ouverte. Si vous ouvrez une autre connexion, répétez le cd indiqué.

cd change le répertoire de travail. Commencez dans le projet préparé :

cd /home/labex/project/note-search

notes/reference contient les originaux protégés ; notes/practice contient une copie accessible en écriture. find énumère les chemins récursivement ; -type f limite la liste aux fichiers ordinaires. L'ordre de la liste est sans importance.

find notes -type f

Les fichiers notes/reference/archive/notes.txt et notes/reference/field/notes.txt existent tous les deux. Leur dernier composant, ou nom de base, est identique, mais leurs chemins complets identifient des documents distincts. ./ exécute un programme du répertoire courant. Le sample complet reçoit un répertoire de notes et une requête :

./sample notes cat

Il renvoie trois groupes de documents, dont ces deux :

File: reference/archive/notes.txt
  1: CAT, cat!
  3: cat blue together

File: reference/field/notes.txt
  1: cat; red

Le troisième groupe est practice/旅行 notes.txt. Chaque identifiant est relatif à la racine notes fournie et omet donc notes/. Chaque document apparaît une seule fois, même si le mot se répète. Les nombres sont les vrais numéros des lignes originales, à partir de 1. Seules les lignes pertinentes apparaissent ; l'absence d'un numéro ne signifie pas que la ligne a été supprimée. cat affiche un fichier texte pour comparer :

cat notes/reference/archive/notes.txt

La ligne 2, Blue ledger — 東京, existe, mais ne contient pas cat. Les chemins UTF-8 et le texte des lignes sont conservés tels quels.

Notre règle de mots est limitée et précise : un jeton est une séquence maximale de lettres ASCII A-Z ou a-z, dont les majuscules ASCII sont converties en minuscules. Les chiffres, traits de soulignement, signes de ponctuation, espaces et chaque octet non ASCII séparent les jetons. CAT-blue_42 devient cat et blue ; cat diffère de catalog ; 猫cat狗 contient cat ; café produit caf, et non cafe. Conserver du texte Unicode ne revient pas à comprendre les mots de toutes les langues. Nous n'implémentons pas de segmentation multilingue.

Lisez l'interface préparée et votre code de départ :

cat search.hpp
cat engine.cpp

Document contient id, le chemin relatif complet, et lines, un vecteur de chaînes contenant les lignes originales. Le chargement préparé lit récursivement les fichiers .txt ordinaires et trie leurs identifiants. Les notes sont du texte UTF-8 avec des fins de ligne LF. Les chemins peuvent contenir des espaces et de l'Unicode ; les tabulations ou retours à la ligne dans les chemins, liens symboliques, données NUL et fichiers binaires arbitraires sont hors format. Une entrée illisible produit une erreur plutôt que des résultats partiels.

Index est un alias de std::map<std::string, std::set<std::string>>. Une table associative relie chaque clé de mot distincte à une valeur : ici, un ensemble d'identifiants de documents. Un ensemble conserve chaque identifiant une seule fois. Ainsi, trois occurrences de cat dans un document produisent une seule relation. Cette recherche inversée, des mots vers les documents, s'appelle un index inversé.

Votre code de départ renvoie un index vide. provided.inc contient la fonction de requête déjà opérationnelle dans cette unité. #include insère un fichier à la compilation. Conservez search.hpp, provided.inc, support.cpp, main.cpp, sample et tous les chemins et contenus de notes/reference/. Modifiez engine.cpp ; les fonctions auxiliaires et algorithmes équivalents sont autorisés. Seules les données de notes/practice/ peuvent être modifiées, ajoutées ou supprimées. Leur ensemble initial de fichiers n'est pas une exigence d'évaluation.

bash exécute le script d'autovérification préparé. index sélectionne le comportement enseigné ici. Cette première exécution échoue volontairement avec FAIL exact whole-word postings retain complete document identities et un code de sortie non nul. Cela signifie que votre index est incomplet, pas que l'environnement est défectueux :

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

Parcourez chaque document, chaque ligne et chaque mot analysé. Une boucle for fondée sur une plage visite chaque élément ; const auto& fournit une référence en lecture seule plutôt qu'une copie. index[word] accède à l'ensemble du mot et en crée un vide si la clé est nouvelle. .insert(doc.id) ajoute l'identifiant complet et ignore les relations dupliquées. Gardez la table hors de toutes les boucles pour accumuler les relations de tous les documents.

La commande suivante ne remplace que engine.cpp. Un here-document envoie plusieurs lignes à cat ; > les écrit dans le fichier indiqué en remplaçant son contenu. Le délimiteur entre apostrophes 'CPP' empêche le shell de développer le texte C++. Terminez par CPP seul sur une ligne. Conservez les deux inclusions pour disposer des déclarations et de la requête préparée.

cat > engine.cpp <<'CPP'
#include "search.hpp"
#include "provided.inc"
Index buildIndex(const std::vector<Document>& documents) {
    Index index;
    for (const auto& doc : documents) {
        for (const auto& line : doc.lines) {
            for (const auto& word : words(line)) {
                index[word].insert(doc.id);
            }
        }
    }
    return index;
}
CPP

words(line) est l'analyseur ASCII préparé. N'extrayez pas seulement le nom de base, ne vous arrêtez pas à la première correspondance et ne stockez pas un nombre d'occurrences à la place de l'ensemble de documents.

Compilez votre livrable. g++ est le compilateur ; -std=c++17 sélectionne la version du langage, -Wall -Wextra active des avertissements utiles et -o findnotes nomme l'exécutable produit. Les trois entrées .cpp sont votre moteur, le chargement et l'analyse préparés, et l'interface de commande. Les fichiers inclus sont lus automatiquement.

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

Une compilation réussie n'affiche généralement rien. En cas d'erreur, lisez le premier nom de fichier et numéro de ligne, corrigez engine.cpp ou répétez son remplacement complet, puis recompilez. Une compilation échouée peut laisser un ancien exécutable. Modifier le code ne le met pas à jour. L'autovérification teste à la fois vos fonctions originales sur des données isolées et votre findnotes existant ; elle ne reconstruit pas cet exécutable pour vous.

./findnotes notes cat

Vous devez obtenir les trois identifiants et les mêmes lignes originales que l'exemple. Un autre ordre reproductible des documents est autorisé ; les numéros de ligne de chaque document doivent croître et chaque ligne apparaître une fois. Cette étape évalue l'index et les requêtes d'un mot. La combinaison de mots différents est préparée ici et deviendra votre tâche au prochain laboratoire.

Reconstruire à partir des notes d'exercice actuelles

Dans cette étape, modifiez, ajoutez et supprimez des données d'exercice tout en conservant les originaux. Restez dans Terminal, dans /home/labex/project/note-search. Chaque invocation charge les fichiers actuels et construit un nouvel index en mémoire. Il n'y a ni fichier d'index sauvegardé, ni démon d'actualisation, ni commande supplémentaire de reconstruction.

Comparez d'abord les limites des mots entiers :

./findnotes notes catalog

Vous devez obtenir reference/catalog.txt et practice/旅行 notes.txt, avec des lignes contenant catalog. Ce ne sont pas des correspondances supplémentaires de cat. Un mot inconnu constitue aussi une recherche normale :

./findnotes notes absent

Le programme affiche No matching documents. et se termine avec succès. Cela diffère d'une entrée mal formée, rejetée par l'interface préparée. Le prochain laboratoire étudie cette distinction.

La copie d'exercice est déjà accessible en écriture ; les originaux de référence sont en lecture seule. Les apostrophes gardent un chemin contenant des espaces dans un seul argument et préservent les caractères Unicode. printf écrit du texte ; \n crée un retour à la ligne. > remplace tout le fichier d'exercice indiqué. Remplacez son contenu pour retirer cat de cette copie :

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

Seules les deux notes de référence correspondent maintenant. Le fichier d'exercice existe toujours ; son contenu actuel ne correspond plus. Ajoutez une autre note d'exercice autorisée :

printf 'CAT beside the window\nA new observation\n' > 'notes/practice/new notes.txt'
./findnotes notes cat

Trois documents correspondent à nouveau, dont practice/new notes.txt à la ligne 1. Vous n'avez pas recompilé : changer les données demande seulement une nouvelle exécution. Modifier le code C++ nécessite une compilation.

rm supprime un fichier. Son unique opérande entre apostrophes est ici la nouvelle note d'exercice, pas un original ni un répertoire entier :

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

Cet identifiant disparaît ; les deux résultats de référence restent. Ne supprimez jamais d'originaux et n'altérez pas l'analyseur pour forcer un résultat. Si vous saisissez mal un chemin, lisez No such file or directory, énumérez les chemins avec find notes -type f et utilisez le bon. Si vous visez un original en lecture seule, laissez-le intact et utilisez un fichier d'exercice. Une erreur de chemin ordinaire ne nécessite pas de réinitialiser l'environnement.

Relancez l'autovérification. Elle lit vos fichiers actuels, puis crée des données temporaires pour d'autres entrées. Les modifications, ajouts et suppressions de ces données ne touchent jamais vos notes d'exercice. Les sondes isolées d'entrées invalides peuvent afficher des erreurs attendues à côté des lignes PASS ; une exécution réussie se termine par CLI CHECKS PASSED.

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

Le bouton de vérification doit également réussir. Répéter une vérification ne réécrit pas votre code, ne remplace pas l'exécutable et ne réinitialise pas les notes. Si elle échoue après une modification du code, reconstruisez le vrai findnotes et réessayez. Les tests temporaires des fonctions ne suffisent pas à prouver que votre exécutable est à jour.

Résumé

Vous avez créé un index inversé dont les valeurs sont des ensembles d'identifiants complets de documents. Les mots répétés ne dupliquent pas les documents, les noms de base identiques restent distincts et les vrais numéros de ligne ramènent au texte original. Une nouvelle exécution reconstruit l'index de façon transparente après les modifications, ajouts et suppressions autorisés. Implémentez ensuite l'intersection exigeant plusieurs mots dans un même document.