API Search pour les anciens services groupés

L'API Search fournit un modèle d'indexation de documents contenant des données structurées. Vous pouvez effectuer une recherche dans un index, et organiser et présenter les résultats obtenus. L'API est compatible avec la correspondance de texte intégral sur les champs de type chaîne. Les documents et les index sont enregistrés dans des espaces de stockage persistants distincts optimisés pour les opérations de recherche. L'API Search peut indexer un nombre illimité de documents. App Engine Datastore peut être plus approprié pour les applications qui ont besoin de récupérer de gros volumes de résultats. Pour afficher le contenu du package search, consultez la documentation de référence sur le package search.

Aperçu

L'API Search repose sur quatre concepts principaux : les documents, les index, les requêtes et les résultats.

Documents

Un document est un objet doté d'un identifiant unique et d'une liste de champs contenant des données utilisateur. Chaque champ possède un nom et un type. Il existe plusieurs types de champs, identifiés par les types de valeurs qu'ils contiennent :

  • Champ Atom : chaîne de caractères indivisible
  • Champ texte : chaîne de texte brut dans laquelle une recherche mot à mot peut être effectuée
  • Champ HTML : chaîne contenant des balises de mise en forme HTML. Seul le texte à l'extérieur peut faire l'objet d'une recherche.
  • Champ numérique : nombre à virgule flottante
  • Champ de type temps : valeur time.Time, stockée avec une précision à la milliseconde
  • Champ de type point géographique : objet de données avec des coordonnées de latitude et de longitude

La taille maximale d'un document est de 1 Mo.

Index

Un index stocke les documents pour qu'il soit possible de les récupérer. Un seul document peut être récupéré par son ID, un ensemble de documents par les ID consécutifs ou la totalité des documents par leur index. Vous pouvez également rechercher dans un index des documents répondant à des critères donnés de champs et de valeurs, spécifiés en tant que chaîne de requête. Vous pouvez gérer des groupes de documents en les plaçant dans des index distincts.

Il n'y a pas de limite au nombre de documents dans un index, ni au nombre d'index que vous pouvez utiliser. La taille totale de tous les documents d'un index est limitée à 10 Go par défaut. Les utilisateurs disposant du rôle Administrateur App Engine peuvent envoyer une demande à partir de la page App Engine Search de la console Google Cloud pour augmenter la taille jusqu'à 200 Go.

Requêtes

Pour rechercher dans un index, construisez une requête comportant une chaîne de requête et éventuellement des options supplémentaires. Une chaîne de requête spécifie des conditions pour les valeurs d'un ou de plusieurs champs de document. Lorsque vous recherchez dans un index, vous ne récupérez que les documents de l'index dont les champs correspondent à la requête.

La requête la plus simple, parfois appelée "recherche globale", est une chaîne contenant uniquement des valeurs de champ. La recherche suivante utilise une chaîne de caractères pour rechercher les documents contenant les mots "rose" et "water" :

index.Search(ctx, "rose water", nil)

Celle-ci recherche les documents contenant des champs de type date correspondant au 4 juillet 1776 ou des champs texte contenant la chaîne "1776-07-04" :

index.Search(ctx, "1776-07-04", nil)

Une chaîne de requête peut également être plus spécifique. Elle peut contenir un ou plusieurs termes, chacun nommant un champ et une contrainte sur la valeur du champ. La forme exacte d'un terme dépend du type du champ. Par exemple, en supposant qu'il existe un champ texte appelé "Product" et un champ numérique appelé "Price", voici une chaîne de requête comportant deux termes :

// search for documents with pianos that cost less than $5000
index.Search(ctx, "Product = piano AND Price < 5000", nil)

Les options de requête, comme leur nom l'indique, ne sont pas obligatoires. Elles offrent un certain nombre de fonctionnalités :

  • Contrôler le nombre de documents affichés dans les résultats de la recherche
  • Spécifier les champs de document à inclure dans les résultats. La valeur par défaut consiste à inclure tous les champs du document d'origine. Vous pouvez spécifier de n'inclure dans les résultats qu'un sous-ensemble de champs, le document d'origine n'étant pas affecté.
  • Trier les résultats
  • Créer des "champs calculés" pour les documents utilisant des expressions FieldExpressions et des champs de texte abrégés à l'aide d'extraits.
  • Permettre la pagination des résultats de la recherche en n'affichant qu'une partie des documents correspondant à chaque requête (à l'aide de décalages et de curseurs).

Nous vous recommandons de consigner les chaînes de requête dans votre application si vous souhaitez conserver un enregistrement des requêtes exécutées.

Résultats de recherche

Un appel Search renvoie une valeur Iterator, qui peut être utilisée pour afficher l'ensemble des documents correspondants.

Supports de formation supplémentaires

En plus de cette documentation, vous pouvez consulter le cours en deux parties concernant l'API Search de Google Developer's Academy. (Bien que ce cours utilise l'API Python, la discussion supplémentaire sur les concepts de recherche peut s'avérer utile.)

Documents et champs

Les documents sont représentés par des structures Go comprenant une liste de champs. Ils peuvent également être représentés par tout type mettant en œuvre l'interface FieldLoadSaver.

Identifiant du document

Chaque document présent dans un index doit posséder un identifiant de document unique, ou docID. Ce dernier peut être utilisé pour extraire un document d'un index sans effectuer de recherche. Par défaut, l'API Search génère automatiquement un docID lorsqu'un document est créé. Vous pouvez également spécifier vous-même le docID lorsque vous créez un document. Un docID ne doit contenir que des caractères ASCII visibles et imprimables (codes ASCII 33 à 126 inclus) et ne doit pas dépasser 500 caractères. Un identifiant de document ne peut pas commencer par un point d'exclamation ("!"), et ne peut pas commencer ni se terminer par un double trait de soulignement ("__").

Bien qu'il soit pratique de créer des identifiants de documents uniques lisibles et significatifs, vous ne pouvez pas inclure le docID dans une recherche. Considérez ce scénario : vous disposez d'un index avec des documents qui représentent des pièces, en utilisant le numéro de série de la pièce comme docID. Il sera très facile de récupérer le document correspondant à une seule pièce, mais il sera impossible de rechercher une plage de numéros de série avec d'autres valeurs de champ, telles que la date d'achat. Le problème est résolu avec l'enregistrement du numéro de série dans un champ Atom.

Champs du document

Un document contient des champs qui ont un nom, un type et une valeur unique de ce type. Deux champs ou plus peuvent avoir le même nom, mais des types différents. Par exemple, vous pouvez définir deux champs avec le nom "âge" : l'un de type texte (avec la valeur "vingt-deux"), l'autre de type numérique (avec la valeur "22").

Noms des champs

Les noms de champ sont sensibles à la casse et ne peuvent contenir que des caractères ASCII. Ils doivent commencer par une lettre et peuvent contenir des lettres, des chiffres ou des traits de soulignement. Un nom de champ ne peut pas dépasser 500 caractères.

Champs à valeurs multiples

Un champ ne peut contenir qu'une seule valeur, qui doit correspondre au type du champ. Les noms de champs ne doivent pas nécessairement être uniques. Un document peut avoir plusieurs champs portant le même nom et le même type, ce qui permet de représenter un champ avec plusieurs valeurs. Cependant, les champs de date et de numéro portant le même nom ne peuvent pas être répétés. Un document peut également contenir plusieurs champs portant le même nom mais avec des types différents.

Types de champ

Trois types de champs permettent de stocker des chaînes de caractères. Ils sont collectivement appelés champs de type chaîne :

  • Champ texte : chaîne d'une longueur maximale de 1024**2 caractères.
  • Champ HTML : chaîne au format HTML d'une longueur maximale de 1 024**2 caractères
  • Champ Atom : chaîne d'une longueur maximale de 500 caractères

Il existe également trois types de champs qui stockent des données non textuelles :

  • Champ numérique : valeur à virgule flottante avec deux décimales, comprise entre -2 147 483 647 et 2 147 483 647
  • Champ de type temps : valeur time.Time, stockée avec une précision à la milliseconde
  • Champ de point géographique : point sur Terre décrit par ses coordonnées de latitude et de longitude

Les champs de type chaîne sont représentés par le type string intégré de Go, et les types HTML et Atom du package search. Les champs numériques sont représentés par le type float64 intégré de Go, les champs de type temps utilisent le type time.Time et les champs de point géographique utilisent le type GeoPoint du package appengine.

Traitement spécial des champs de types chaîne et temps

Lorsqu'un document comportant des champs de types temps, texte ou HTML est ajouté à un index, il est soumis à un traitement spécial. Il est utile de comprendre ce qui se passe "sous le capot" afin d'utiliser efficacement l'API Search.

Tokenisation des champs de chaîne

Lorsqu'un champ HTML ou texte est indexé, son contenu est tokenisé. La chaîne est divisée en jetons à chaque espace ou caractère spécial (signe de ponctuation, signe de hachage, barre oblique inverse, etc.). L'index inclut une entrée pour chaque jeton. Cela vous permet de rechercher des mots-clés et des expressions ne représentant qu'une partie de la valeur d'un champ. Par exemple, une recherche sur "dark" correspondra à un document dont un champ de texte contient la chaîne "it was a dark and stormy night", et une recherche sur "time" correspondra à un document dont un champ texte contient la chaîne "this is a real-time system".

Dans les champs HTML, le texte à l'intérieur des balises n'est pas tokenisé. Par conséquent, un document contenant un champ HTML avec la valeur it was a <strong>dark</strong> night correspond à une recherche sur le mot "night", mais pas sur le mot "strong". Si vous souhaitez rechercher du texte de balisage, stockez-le dans un champ de texte.

Les champs Atom ne sont pas tokenisés. Un document avec un champ Atom ayant la valeur "bad weather" correspond uniquement à une recherche sur l'intégralité de la chaîne "bad weather". Il ne correspondra pas à une recherche sur les termes "bad" ou "weather" utilisés seuls.

Règles de tokenisation
  • Le trait de soulignement (_) et l'esperluette (&) ne divisent pas les mots en jetons.

  • Les espaces blancs suivants divisent toujours les mots en jetons : espace, retour chariot, saut de ligne, tabulation horizontale, tabulation verticale, saut de page et valeur nulle.

  • Les caractères suivants sont considérés comme signes de ponctuation et divisent les mots en jetons :

    !)%()
    *,-|/
    []]^`
    :=>?@
    {}~$
  • Les caractères du tableau suivant divisent généralement les mots en jetons, mais ils peuvent être gérés différemment selon le contexte dans lequel ils apparaissent :

    Caractère Règle
    < Dans un champ HTML, le signe "inférieur à" indique le début d'une balise HTML qui est ignorée.
    + Une chaîne d'un ou de plusieurs signes "plus" est traitée comme une partie du mot si elle apparaît à la fin de celui-ci (C++).
    # Le signe dièse est considéré comme une partie du mot s'il est précédé de a, b, c, d, e, f, g, j ou x (a# et g# sont des notes de musique, j# et x# sont des langages de programmation, c# correspond aux deux). Si un terme est précédé de "#" (#google), il est traité comme un hashtag et le signe dièse devient une partie du mot.
    ' L'apostrophe est une lettre si elle précède la lettre "s" suivie d'une coupure entre deux mots, comme dans l'anglais "John's hat".
    . Si un point apparaît entre des chiffres, il fait partie d'un nombre (séparateur décimal). Ce point peut également faire partie d'un mot s'il est utilisé dans un acronyme (A.B.C).
    - Le tiret fait partie d'un mot s'il est utilisé dans un acronyme (I-B-M).
  • Tous les autres caractères sur 7 bits autres que les lettres et les chiffres ("A-Z", "a-z", "0-9") sont traités comme des signes de ponctuation et divisent les mots en jetons.

  • Tout le reste est analysé comme caractère UTF-8.

Acronymes

La tokenisation utilise des règles spéciales pour reconnaître les acronymes (chaînes comme "I.B.M", "a-b-c" ou "C I A"). Un acronyme est une chaîne de caractères alphabétiques uniques, tous séparés par le même caractère de séparation. Les caractères de séparation valides sont le point, le tiret ou n'importe quel nombre d'espaces. Le caractère de séparation est supprimé de la chaîne lorsqu'un acronyme est tokenisé. Ainsi, les exemples de chaînes mentionnés ci-dessus deviennent les jetons "ibm", "abc" et "cia". Le texte original reste dans le champ du document.

Lorsque vous travaillez avec des acronymes, notez les points suivants :

  • Un acronyme ne peut pas contenir plus de 21 lettres. Une chaîne d'acronyme valide comportant plus de 21 lettres sera divisée en une série d'acronymes de 21 lettres ou moins.
  • Si les lettres d'un acronyme sont séparées par des espaces, toutes les lettres doivent avoir la même casse. Les acronymes construits avec des points ou des tirets peuvent utiliser à la fois des lettres majuscules et minuscules.
  • Lorsque vous recherchez un acronyme, vous pouvez saisir la forme canonique de l'acronyme (la chaîne sans séparateurs) ou l'acronyme ponctué du tiret ou du point (mais pas les deux) entre ses lettres. Ainsi, le texte "I.B.M" peut être récupéré avec les termes de recherche "I-B-M", "I.B.M" ou "IBM".

Précision des champs de type temps

Lorsque vous créez un champ de type temps dans un document, vous devez le définir sur une valeur time.Time. Pour faciliter l'indexation et la recherche des champs de type temps, tout composant de date est ignoré et la date est convertie en nombre de jours à partir du 01/01/1970 UTC. Cela signifie que même si un champ de type temps peut contenir une valeur d'heure précise, une requête sur la date ne peut spécifier qu'une valeur de champ de type temps sous la forme yyyy-mm-dd. Cela signifie également que l'ordre des champs de type temps triés comportant la même date n'est pas bien défini. Alors que le type time.Time représente l'heure avec une précision de l'ordre de la nanoseconde, l'API Search la stocke avec une précision de l'ordre de la milliseconde.

Autres propriétés du document

Le rang d'un document est un entier positif qui détermine le classement par défaut des documents affichés à partir d'une recherche. Par défaut, le rang est défini au moment où le document est créé avec le nombre de secondes écoulées depuis le 1er janvier 2011. Vous pouvez définir explicitement le rang lorsque vous créez un document. Il est déconseillé d'attribuer le même rang à de nombreux documents et vous ne devez jamais attribuer plus de 10 000 documents au même rang. Si vous spécifiez des options de tri, vous pouvez utiliser le rang comme clé de tri. Notez que lorsque le rang est utilisé dans une expression de tri ou dans une expression de champ, il est référencé sous la forme _rank. Pour en savoir plus sur la définition du rang, consultez la documentation de référence sur DocumentMetadata.

La propriété "Language" de la structure Field spécifie le langage dans lequel ce champ est codé.

Lier un document à d'autres ressources

Vous pouvez utiliser le champ docID d'un document, ainsi que d'autres champs en tant que liens vers d'autres ressources de votre application. Par exemple, si vous utilisez Blobstore, vous pouvez associer le document à un blob spécifique en définissant le docID ou la valeur d'un champ Atom sur la BlobKey des données.

Créer un document

L'exemple de code suivant montre comment créer un objet de document. Le type User spécifie la structure du document et une valeur User est construite de la manière habituelle.

import (
	"fmt"
	"net/http"
	"time"

	"golang.org/x/net/context"

	"google.golang.org/appengine"
	"google.golang.org/appengine/search"
)

type User struct {
	Name      string
	Comment   search.HTML
	Visits    float64
	LastVisit time.Time
	Birthday  time.Time
}

func putHandler(w http.ResponseWriter, r *http.Request) {
	id := "PA6-5000"
	user := &User{
		Name:      "Joe Jackson",
		Comment:   "this is <em>marked up</em> text",
		Visits:    7,
		LastVisit: time.Now(),
		Birthday:  time.Date(1960, time.June, 19, 0, 0, 0, 0, nil),
	}
	// ...

Travailler avec un index

Placer des documents dans un index

Lorsque vous placez un document dans un index, il est copié dans le stockage persistant, et chacun de ses champs est indexé en fonction de son nom, de son type et du docID.

L'exemple de code suivant montre comment accéder à un index et y insérer un document.

// ...
ctx := appengine.NewContext(r)
index, err := search.Open("users")
if err != nil {
	http.Error(w, err.Error(), http.StatusInternalServerError)
	return
}
_, err = index.Put(ctx, id, user)
if err != nil {
	http.Error(w, err.Error(), http.StatusInternalServerError)
	return
}
fmt.Fprint(w, "OK")

Lorsque vous placez un document dans un index et que celui-ci contient déjà un document avec le même docID, le nouveau document remplace l'ancien. Aucun avertissement n'est donné. Vous pouvez appeler Index.Get avant de créer un document ou de l'ajouter à un index pour déterminer si un docID spécifique existe déjà.

La méthode Put renvoie une réponse docID. Si vous n'avez pas spécifié l'identifiant docID vous-même, consultez le résultat pour connaître l'identifiant docID qui été généré :

id, err = index.Put(ctx, "", user)
if err != nil {
	http.Error(w, err.Error(), http.StatusInternalServerError)
	return
}
fmt.Fprint(w, id)

Notez que la création d'une instance de type Index ne garantit pas l'existence d'un index persistant. Un index persistant est créé la première fois que vous y ajoutez un document avec la méthode put.

Mettre à jour des documents

Un document ne peut pas être modifié une fois que vous l'avez ajouté à un index. Vous ne pouvez pas ajouter ou supprimer de champs, ni modifier la valeur d'un champ. Toutefois, vous pouvez remplacer le document par un nouveau document ayant le même docID.

Récupérer des documents par docID

Utilisez la méthode Index.Get pour extraire un document d'un index au moyen de son docID :

func getHandler(w http.ResponseWriter, r *http.Request) {
	ctx := appengine.NewContext(r)

	index, err := search.Open("users")
	if err != nil {
		http.Error(w, err.Error(), http.StatusInternalServerError)
		return
	}

	id := "PA6-5000"
	var user User
	if err := index.Get(ctx, id, &user); err != nil {
		http.Error(w, err.Error(), http.StatusInternalServerError)
		return
	}
	fmt.Fprint(w, "Retrieved document: ", user)
}

Rechercher des documents par leur contenu

Pour récupérer des documents dans un index, construisez une chaîne de requête et appelez Index.Search. Search affiche un itérateur qui renvoie les documents correspondants par ordre de rang décroissant.

func searchHandler(w http.ResponseWriter, r *http.Request) {
	ctx := appengine.NewContext(r)

	index, err := search.Open("myIndex")
	if err != nil {
		http.Error(w, err.Error(), http.StatusInternalServerError)
		return
	}
	for t := index.Search(ctx, "Product: piano AND Price < 5000", nil); ; {
		var doc Doc
		id, err := t.Next(&doc)
		if err == search.Done {
			break
		}
		if err != nil {
			fmt.Fprintf(w, "Search error: %v\n", err)
			break
		}
		fmt.Fprintf(w, "%s -> %#v\n", id, doc)
	}
}

Supprimer un index

Chaque index est constitué des documents qui y sont indexés et d'un schéma d'index. Pour supprimer un index, supprimez tous ses documents, puis supprimez le schéma d'index.

Vous pouvez supprimer un document d'un index en spécifiant l'identifiant docID du document à supprimer dans la méthode Index.Delete.

func deleteHandler(w http.ResponseWriter, r *http.Request) {
	ctx := appengine.NewContext(r)

	index, err := search.Open("users")
	if err != nil {
		http.Error(w, err.Error(), http.StatusInternalServerError)
		return
	}

	id := "PA6-5000"
	err = index.Delete(ctx, id)
	if err != nil {
		http.Error(w, err.Error(), http.StatusInternalServerError)
		return
	}
	fmt.Fprint(w, "Deleted document: ", id)
}

Cohérence à terme

Lorsque vous placez, mettez à jour ou supprimez un document d'un index, la modification se propage sur plusieurs centres de données. Cela se produit généralement rapidement, mais le temps nécessaire varie. L'API Search garantit la cohérence à terme. Cela signifie que dans certains cas, une recherche ou une récupération d'un ou de plusieurs documents peut afficher des résultats qui ne reflètent pas les modifications les plus récentes.

Schémas d'index

Chaque index dispose d'un schéma qui montre tous les noms et types de champs qui apparaissent dans les documents qu'il contient. Vous ne pouvez pas définir un schéma vous-même. Les schémas sont maintenus dynamiquement et mis à jour à mesure que des documents sont ajoutés à un index. Un schéma simple peut être semblable à ceci, au format JSON :

{'comment': ['TEXT'], 'date': ['DATE'], 'author': ['TEXT'], 'count': ['NUMBER']}

Chaque clé du dictionnaire est le nom d'un champ de document. La valeur de la clé est la liste des types de champs utilisés avec ce nom de champ. Si vous avez utilisé le même nom de champ avec différents types de champs, le schéma indiquera plusieurs types de champ pour un nom de champ, comme ci-dessous :

{'ambiguous-integer': ['TEXT', 'NUMBER', 'ATOM']}

Une fois qu'un champ apparaît dans un schéma, il ne peut plus être supprimé. Il n'est pas possible de supprimer un champ, même si l'index ne contient plus de document portant ce nom de champ particulier.

Un schéma ne définit pas une "classe" au sens de la programmation par objets. En ce qui concerne l'API Search, chaque document est unique et les index peuvent contenir différentes sortes de documents. Si vous souhaitez traiter des collections d'objets avec la même liste de champs que les instances d'une classe, vous devez appliquer une abstraction dans votre code. Par exemple, vous pouvez vous assurer que tous les documents contenant le même ensemble de champs sont conservés dans leur propre index. Le schéma d'index peut être considéré comme la définition de la classe et chaque document de l'index comme une instance de la classe.

Afficher les index dans Google Cloud Console

Dans la console Google Cloud, vous pouvez afficher des informations sur les index de votre application et les documents qu'ils contiennent. Cliquer sur un nom d'index permet de consulter les documents qu'il contient. Tous les champs de schéma définis pour l'index s'affichent. Pour chaque document comportant un champ avec ce nom, la valeur du champ s'affiche. Vous pouvez également émettre des requêtes sur les données d'index directement à partir de la console.

Quotas de l'API Search

L'API Search dispose de plusieurs quotas gratuits :

Ressource ou appel d'API Quota gratuit
Stockage total (documents et index) 0,25 Go
Requêtes 1 000 requêtes par jour
Ajout de documents aux index 0,01 Go par jour

Pour assurer la fiabilité du service, l'API Search impose les limites suivantes. Celles-ci s'appliquent aux applications gratuites et payantes :

Ressource Quota de sécurité
Utilisation maximale des requêtes 100 minutes cumulées de temps d'exécution des requêtes par minute
Nombre maximal de documents ajoutés ou supprimés 15 000 par minute
Taille maximale par index (nombre illimité d'index autorisés) 10 Go

L'utilisation de l'API est comptabilisée de différentes manières en fonction du type d'appel :

  • Index.Search : chaque appel d'API est comptabilisé comme une requête. Le temps d'exécution équivaut à la latence de l'appel.
  • Index.Put : lorsque vous ajoutez des documents à des index, la taille de chaque document et le nombre de documents sont comptabilisés dans le quota d'indexation.
  • Tous les autres appels de l'API Search sont comptabilisés en fonction du nombre d'opérations impliquées :
    • Index.Get : une opération est comptabilisée pour chaque document réellement renvoyé, ou une seule opération si rien n'est renvoyé.
    • Index.Delete : une opération est comptabilisée pour chaque document de la requête, ou une seule opération si la requête est vide.

Le quota sur le débit de la requête est imposé de sorte qu'un utilisateur ne puisse pas monopoliser le service de recherche. Comme les requêtes peuvent s'exécuter simultanément, chaque application est autorisée à exécuter des requêtes qui consomment jusqu'à 100 minutes de temps d'exécution par minute. Si vous exécutez de nombreuses requêtes courtes, vous n'atteindrez probablement pas cette limite. Une fois que vous avez dépassé le quota, les requêtes suivantes échouent jusqu'à la période suivante, lorsque votre quota est restauré. Le quota n'est pas strictement imposé par tranches d'une minute. Une variante de l'algorithme leaky bucket (algorithme du seau percé) permet de contrôler la bande passante utilisée par la recherche par incréments de cinq secondes.

Vous trouverez plus d'informations sur les quotas à la page Quotas. Lorsqu'une application tente de dépasser ces montants, une erreur de quota insuffisant s'affiche.

Notez que, bien que ces limites soient appliquées à la minute, la console affiche les totaux quotidiens pour chacune d'entre elles. Les clients ayant souscrit une formule d'assistance Silver, Gold ou Platinum peuvent demander une augmentation des limites de débit en contactant leur conseiller de l'équipe d'assistance.

Tarifs de l'API Search

Les frais suivants sont appliqués à l'utilisation au-delà des quotas gratuits :

Ressource Coût
Stockage total (documents et index) 0,18 $ par Go par mois
Requêtes 0,50 $ pour 10 000 requêtes
Indexation de documents dans l'index de recherche 2,00 $ par Go

Pour en savoir plus sur les tarifs, consultez la page Tarifs.