334 lines
14 KiB
Markdown
334 lines
14 KiB
Markdown
# Devoir à distance 3A Web du 05/04/20
|
|
## Présentation
|
|
|
|
Le but du devoir est de construire un moteur de recherche en texte intégral
|
|
_très_ basique pour rechercher du contenu sur disque, en stockant ses données
|
|
dans Redis.
|
|
|
|
Les tâches qui le composent sont les suivantes:
|
|
|
|
1. créer un programme en ligne de commande acceptant une commande d'indexation,
|
|
une de déboguage, et une de recherche
|
|
2. créer une fonction pouvant enregistrer une valeur dans un "sorted set" Redis
|
|
3. créer une fonction pouvant extraire un résultat d'un "sorted set" Redis
|
|
4. créer un test d'intégration combinant les deux précédentes
|
|
5. créer une fonction lisant un fichier texte mot par mot
|
|
6. créer une fonction enregistrant les statistiques de mots d'un fichier au moyen
|
|
des fonctions de 2. et 5.
|
|
7. créer une fonction parcourant une sous-arborescence disque pour en examiner
|
|
tous les fichiers
|
|
8. créer une fonction identifiant si un fichier est reconnu par Go comme un
|
|
fichier texte
|
|
9. intégrer les deux précédentes pour que le parcours ne traite que les fichiers
|
|
reconnus comme texte
|
|
10. intégrer la fonction avec l'écriture des comptes de mots dans Redis
|
|
11. écrire une fonction implémentant la commande de déboguage, qui liste tout le
|
|
contenu de la base Redis après une indexation
|
|
12. écrire une fonction implémentant la commande de recherche, qui interroge
|
|
Redis pour ramener les meilleurs résultats pour une recherche
|
|
13. contextualiser l'affichage des résultats avec le texte avoisinant
|
|
|
|
|
|
## Ressources
|
|
|
|
- Redis:
|
|
- pour Linux, vous pouvez l'installer depuis les paquets de votre distribution,
|
|
par exemple `apt install redis`
|
|
- pour macOS, vous pouvez l'installer depuis Homebrew: `brew install redis`
|
|
- sur tous les systèmes, vous pouvez l'installer par Docker, en suivant les
|
|
instructions sur https://hub.docker.com/_/redis/
|
|
- Pilote Go pour Redis:
|
|
- utiliser Redigo https://pkg.go.dev/github.com/gomodule/redigo/redis?tab=overview
|
|
- API: https://godoc.org/github.com/gomodule/redigo/redis#hdr-Executing_Commands
|
|
- Vous pouvez chercher des exemples en ligne, l'un des miens est visible sur
|
|
https://github.com/fgm/drupal_redis_stats
|
|
- Vous utiliserez l'adresse et le port Redis par défaut: `redis://localhost:6379/0`.
|
|
- Pour vider la base Redis entre deux essais vous pouvez utiliser en ligne de commande:
|
|
`redis-cli flushall`
|
|
- Organisation du code:
|
|
- à la racine du projet: `go.mod`, `go.sum`, `main.go`, `README.md`
|
|
- dans `cmd/`: vos fichiers de commandes (root+3) et de test (1).
|
|
- dans `engine/`: vos fichiers réalisant les traitements décrits à chaque étape:
|
|
- `engine/disk.go`: fonctions liées à l'accès disque
|
|
- `engine/redis.go`: fonctions liées à Redis
|
|
- `engine/results.go`: fonctions liées à l'affichage des résultats
|
|
- `engine/text.go`: fonctions liées à la manipulation de texte
|
|
- `text_test.go`: tests des fonctions de manipulation de texte
|
|
- Codage : votre code doit être formaté au standard Go et passer
|
|
`golint -min_confidence 0.3 ./...` sans remonter de messages.
|
|
- Pensez à indiquer le temps total passé sur le devoir. Il devrait représenter
|
|
de 3 à 6 heures de travail, tests unitaires et d'intégration compris.
|
|
|
|
|
|
## 1. Commandes
|
|
|
|
Votre programme s'appelera `search`.
|
|
|
|
Utiliser Cobra comme vu en cours (`cobra init search`) pour créer 3 commandes:
|
|
|
|
- `search index <répertoire de départ>`
|
|
- Le but de cette commande sera de déclencher l'indexation à l'étape 10.
|
|
- un seul argument : le chemin d'un répertoire. La fonction de commande `IndexDir`
|
|
devra vérifier que le répertoire existe et est lisible, et sortir proprement
|
|
sinon.
|
|
- `search dump`
|
|
- Le but de cette commande, implémentée par une fonction `Dump`, est de lister
|
|
l'intégralité de la base de données Redis sur la sortie standard, au format YAML :
|
|
|
|
```yaml
|
|
<mot>:
|
|
<fichier>: <nombre d'occurrences>
|
|
...fichier suivant...
|
|
...mot suivant...
|
|
```
|
|
- `search query <mot>`
|
|
- Le but de cette commande sera de lister à l'étape 13 les résultats de
|
|
l'indexation obtenue à l'étape 12, dans une fonction `Query`
|
|
|
|
A l'étape 1, vos commande ont juste besoin d'exister et d'afficher leurs arguments:
|
|
|
|
- `index <répertoire>`:
|
|
- si le répertoire existe et est lisible, afficher `walking <répertoire>`
|
|
- sinon afficher une erreur et terminer
|
|
- `dump`:
|
|
- afficher `dumping index`
|
|
- `query <mot>`
|
|
- afficher `querying index for <mot>`
|
|
|
|
Attention, sur macOS par exemple `/etc` est un lien symbolique vers `/private/etc`,
|
|
qui est traité comme un répertoire par `Stat`, mais dont le parcours ne ramène que
|
|
le lien lui-même. Si vous voulez lire son contenu, spécifiez-le comme `/etc/`
|
|
(ou bien traitez spécialement le cas avec `Lstat()` mais ce n'est pas utile ici).
|
|
|
|
Pensez, une fois la structure de commande Cobra créée, à activer le système de
|
|
modules par la commande `go mod init search`.
|
|
|
|
|
|
## 2. Écriture dans Redis
|
|
|
|
L'indexation est réalisée dans un "sorted set" Redis, avec `ZADD`:
|
|
|
|
https://redis.io/commands/zadd
|
|
|
|
- Ajoutez à votre commande d'indexation le fait d'ouvrir la connexion à Redis,
|
|
en appelant une fonction `func Dial() (redis.Conn, error)` placée dans le
|
|
fichier de votre commande racine, et de sortir avec un message d'erreur en cas
|
|
d'échec de connexion.
|
|
- Créez une fonction ajoutant une entrée dans Redis:
|
|
|
|
```go
|
|
func AddFile(c redis.Conn, key, file string, score int) error {
|
|
// Utiliser Redigo pour écrire une entrée avec la commande Redis ZADD
|
|
}
|
|
```
|
|
- Votre commande `index` doit invoquer cette fonction et vérifier le succès de
|
|
la commande
|
|
```go
|
|
err = AddFile(c, "test", "demo", 1)
|
|
```
|
|
En cas d'échec, afficher l'erreur et sortir.
|
|
|
|
|
|
## 3. Lecture dans Redis
|
|
|
|
La lecture s'opère depuis un "sorted set" Redis, avec `ZREVRANGE`.
|
|
|
|
https://redis.io/commands/zrevrange
|
|
|
|
- Ajoutez à votre commande de dump le fait d'ouvrir la connexion à Redis avec
|
|
votre fonction `Dial`
|
|
- Créez une fonction lisant les valeurs d'une entrée dans Redis. Pour convertir
|
|
le résultat de l'appel Redigo, pensez à utiliser un "helper":
|
|
https://godoc.org/github.com/gomodule/redigo/redis#hdr-Reply_Helpers
|
|
|
|
```go
|
|
func Get(c redis.Conn, key string) ([]string, error) {
|
|
// Utilisez Redigo pour lire toutes les valeurs de la clef, et les
|
|
// placer dans une tranche de chaînes. Renvoyez une erreur si nécessaire.
|
|
}
|
|
```
|
|
- La fonction `Dump` de votre commande `dump` doit invoquer cette fonction et
|
|
vérifier le succès de la commande, puis afficher le résultat:
|
|
```go
|
|
files, err = Get(c, "test")
|
|
// ...contrôlez le succès
|
|
for _, file := range files {
|
|
fmt.Println(file)
|
|
}
|
|
```
|
|
|
|
|
|
## 4. Test d'intégration
|
|
|
|
_Cette étape est recommandée et notée, mais pas indispensable pour la suite_
|
|
|
|
Créez une fonction de test `TestReadWrite(t *testing.T)` qui réalise les opérations
|
|
suivantes :
|
|
|
|
- ouverture de la connexion Redis, échec en cas d'erreur
|
|
- effacement total de Redis avec la commande `FLUSHALL` via Redigo
|
|
https://redis.io/commands/flushall
|
|
- écriture d'une valeur avec la fonction `AddFile` du 2.
|
|
- lecture d'une valeur avec la fonction `Get` du 3.
|
|
- vérification : le résultat doit contenir une seule chaîne, qui correspond à celle
|
|
passée à `addHit`.
|
|
|
|
|
|
## 5. Lecture de texte
|
|
|
|
- Créez une fonction de lecture par mots :
|
|
```go
|
|
func Scan(r io.Reader) ScanHits // ScanHits est un type défini pour map[string]int
|
|
```
|
|
- La fonction doit parcourir le lecteur reçu en argument pour découper le texte
|
|
en mots avec un `bufio.Scanner` avec le callback `bufio.ScanWords`.
|
|
- Au fil de la lecture, elle remplit une carte comptant pour chaque mot trouvé le
|
|
nombre de ses occurrences dans le fichier.
|
|
- En l'absence de résultats, la fonction renvoie une carte vide non-nil.
|
|
- Créez une fonction de test unitaire `func TestScan( t *testing.T)` qui vérifie
|
|
sur au moins trois textes d'exemple, dont un au moins sera la chaîne vide,
|
|
que la fonction est correcte.
|
|
- Pour créer un `Reader` à passer à `Scan`, utilisez `strings.NewReader` sur
|
|
vos chaînes de test.
|
|
|
|
|
|
## 6. Indexation d'un fichier
|
|
|
|
- Créez une fonction `IndexFile(c redis.Conn, file string) error`
|
|
- La fonction ouvre le fichier spécifié et le parcourt avec `Scan` du 5.
|
|
- En cas d'échec, elle renvoie une erreur.
|
|
- En cas de succès, elle enregistre les résultats dans Redis avec une boucle
|
|
sur la fonction `AddHit` du 2.
|
|
|
|
|
|
## 7. Parcours d'arborescence disque
|
|
|
|
- Modifiez la fonction de commande `IndexDir` pour qu'elle parcoure le répertoire
|
|
indiqué et ses sous-répertoires.
|
|
- Pour cela, utilisez la fonction `filepath.Walk`
|
|
- Pour créer votre fonction de rappel, créez une fonction `ScanFile` avec comme signature:
|
|
```go
|
|
func ScanFile(c redis.Conn, path string, info os.FileInfo, err error) error
|
|
```
|
|
|
|
- Puisque cette fonction n'est pas une `WalkFunction` du fait de l'argument
|
|
supplémentaire `redis.Conn`, utilisez une fonction anonyme comme fonction de
|
|
rappel, qui aura accès à la connexion Redis pour pouvoir invoquer `ScanFile`
|
|
- Pour cette étape,
|
|
- votre fonction vérifie si les fichiers sur lesquels elle
|
|
est invoquée sont bien des fichiers ordinaires (et pas des symlinks, des
|
|
répertoires, des fichiers périphériques, etc) avec le paramètre de type
|
|
`os.FileInfo` (regardez les constantes `os.ModeXXX`)
|
|
- elle saute silencieusement tout ce qui n'est pas un simple fichier
|
|
- elle affiche le chemin absolu des simples fichiers (un par ligne)
|
|
- Ignorez les erreurs renvoyées par `filepath.Walk`.
|
|
|
|
|
|
## 8. Vérification de contenu
|
|
|
|
Pour cet exemple, vous allez utiliser la fonction standard `http.DetectContentType`
|
|
pour déterminer quel est le type d'un fichier.
|
|
|
|
- Créez une fonction `IsText(r io.Reader) bool` qui renvoie "vrai" si le contenu
|
|
du lecteur qui lui est passé est identifié par la fonction standard comme
|
|
étant un type MIME de la forme `text/<quelquechose>`, par exemple `text/plain`
|
|
ou `text/html`.
|
|
- Vous n'avez besoin que des 512 premiers octets du contenu du lecteur, donc
|
|
utilisez un `LimitedReader`
|
|
- _Attention_ : cette fonction standard identifie les sources JSON, Go, et PHP comme
|
|
`text/plain`, ce n'est pas une erreur de votre part si vous obtenez ces résultats,
|
|
le résultat dans ces cas doit bien être "vrai".
|
|
- Créez une fonction de test unitaire `TestIsText(t *testing.T)` à laquelle vous
|
|
passerez des fichiers de différents types. Afin de pouvoir disposer de fichiers
|
|
binaires à tester, vous utiliserez le mécanisme `testdata`
|
|
- tel que décrit sur https://dave.cheney.net/2016/05/10/test-fixtures-in-go
|
|
- utilisé par exemple dans `http_test.TestServeFile`
|
|
https://golang.org/src/net/http/fs_test.go#L70
|
|
- pour ne pas avoir de problème de chemins, lancez vos tests depuis la racine
|
|
du projet, par `go test -v ./...`
|
|
|
|
## 9. Parcours d'arborescence filtré
|
|
|
|
- Modifiez votre fonction `ScanFile` pour qu'elle utilise `IsText` afin de ne
|
|
plus lister que les fichiers "texte" au sens de `IsText`.
|
|
- Ignorez les fichiers illisibles, par exemple par manque de permissions.
|
|
|
|
|
|
## 10. Analyse de fichiers texte
|
|
|
|
- Complétez la fonction `ScanFile` pour qu'elle invoque la fonction `IndexFile`
|
|
au lieu de simplement afficher le nom des fichiers.
|
|
|
|
À ce stade, votre indexeur est normalement complet.
|
|
|
|
|
|
## 11. Fonction de dump.
|
|
|
|
- Modifier la fonction `Dump` pour qu'elle ne liste plus simplement une clef
|
|
avec `ZREVRANGE`, mais qu'elle parcoure l'ensemble des clefs avec `SCAN` pour
|
|
parcourir l'ensemble de la base Redis.
|
|
- Afficher l'ensemble de la base au même format que précédemment, mais en triant
|
|
par ordre alphabétique des clefs. Attention, cela nécessite trois étapes:
|
|
- dans un premier temps, construisez en mémoire une tranche de chaînes contenant
|
|
toutes les clefs ramenées par `SCAN`
|
|
- dans un second temps, triez la tranche avec `sort.Strings`
|
|
- dans un troisième temps, bouclez sur la tranche triée pour invoquer votre
|
|
fonction `Get` sur chaque clef, et affichez chaque résultat sérialisé, donnant
|
|
ainsi la sérialisation de l'ensemble.
|
|
|
|
Le code de https://github.com/fgm/drupal_redis_stats/blob/master/stats/stats.go#L95
|
|
peut vous être utile pour l'utilisation de `SCAN`, qui n'est pas complètement
|
|
intuitive, mais ce n'est qu'un exemple.
|
|
|
|
|
|
## 12. Requêtage
|
|
|
|
- Ajoutez à votre commande `query` le fait d'ouvrir la connexion à Redis,
|
|
et de sortir avec un message d'erreur en cas d'échec de connexion. Réutilisez
|
|
la fonction que vous aurez créée pour cela à l'étape 2.
|
|
- Créez une fonction `ShowResults(w io.Writer, key string, hits []string)`
|
|
qui affiche le mot recherché et les noms de fichiers qui lui sont passés, un
|
|
par ligne.
|
|
- Modifier la fonction `Query` pour que
|
|
- elle extraie de Redis les 3 meilleurs résultats pour le mot recherché,
|
|
sous la forme d'une tranche de chaînes triée du score le plus élevé au score
|
|
le plus bas. Utilisez pour cela les paramètres additionels de `ZREVRANGE`.
|
|
Certains mots auront moins de 3 résultats. Incluez leurs scores dans l'affichage.
|
|
- elle invoque `ShowResults` pour l'affichage des résultats en lui passant
|
|
`os.Stdout` comme `io.Writer`.
|
|
|
|
Exemple de résultat après avoir indexé `/etc/`:
|
|
|
|
```bash
|
|
$ search query your
|
|
Querying index for "your":
|
|
- /etc/authorization.deprecated (8 hits)
|
|
- /etc/apache2/original/httpd.conf (6 hits)
|
|
- /etc/apache2/httpd.conf~previous (6 hits)
|
|
$
|
|
```
|
|
|
|
## 13. Contextualisation
|
|
|
|
- Modifier la fonction `ShowResults` pour qu'elle contextualise les résultats en
|
|
affichant pour chaque fichier la première ligne contenant le mot recherché.
|
|
Pour cela, pour chaque fichier, elle doit
|
|
- relire le fichier ligne par ligne avec un `bufio.Scanner` en mode `ScanLines`
|
|
- à la première ligne correspondant au mot recherché, afficher
|
|
- le nom du fichier
|
|
- un caractère ":" et un espace
|
|
- la ligne en cours
|
|
- refermer le fichier et passer au suivant.
|
|
- Pour aligner les colonnes, il est conseillé d'utiliser le paquet `tabwriter`
|
|
|
|
Exemple de résultat après avoir indexé `/etc/`:
|
|
|
|
```
|
|
$ Querying index for "file":
|
|
File |Count |First match
|
|
/etc/apache2/magic |15 |# Magic data for mod_mime_magic Apache module (originally for file(1) command)
|
|
/etc/apache2/original/httpd.conf |8 |# This is the main Apache HTTP server configuration file. It contains the
|
|
/etc/apache2/httpd.conf~previous |8 |# This is the main Apache HTTP server configuration file. It contains the
|
|
$
|
|
```
|