Update README.md

pull/1/head
Raphaël 2020-04-06 18:50:00 +01:00 committed by GitHub
parent 1214f239a6
commit 2390ac5fb6
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
1 changed files with 333 additions and 1 deletions

334
README.md
View File

@ -1 +1,333 @@
# go-search
# 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
$
```