Au cours du mois dernier, j'ai travaillé activement sur des projets de validation de principe liés à CouchDB, explorant ses fonctionnalités et préparant les tâches futures. Au cours de cette période, j'ai parcouru la documentation CouchDB plusieurs fois pour m'assurer de comprendre comment tout fonctionne. En lisant la documentation, je suis tombé sur une déclaration selon laquelle, malgré la livraison de CouchDB avec un serveur de requêtes par défaut écrit en JavaScript, la création d'une implémentation personnalisée est relativement simple et des solutions personnalisées existent déjà dans la nature.
J'ai fait quelques recherches rapides et trouvé des implémentations écrites en Python, Ruby ou Clojure. Comme l’implémentation entière ne m’a pas semblé trop longue, j’ai décidé d’expérimenter CouchDB en essayant d’écrire mon propre serveur de requêtes personnalisé. Pour ce faire, j'ai choisi Go comme langage. Je n'ai pas eu beaucoup d'expérience avec ce langage auparavant, à l'exception de l'utilisation de modèles Go dans les graphiques Helm, mais je voulais essayer quelque chose de nouveau et j'ai pensé que ce projet serait une excellente opportunité pour cela.
Avant de commencer à travailler, j'ai revisité la documentation de CouchDB une fois de plus pour comprendre comment fonctionne réellement le serveur de requêtes. Selon la documentation, la présentation générale du serveur de requêtes est assez simple :
Le serveur de requête est un processus externe qui communique avec CouchDB via le protocole JSON sur une interface stdio et gère tous les appels de fonctions de conception […].
La structure des commandes envoyées par CouchDB au serveur de requêtes peut être exprimée sous la forme [
Donc, en gros, ce que je devais faire était d'écrire une application capable d'analyser ce type de JSON à partir de STDIO, d'effectuer les opérations attendues et de renvoyer les réponses comme spécifié dans la documentation. Il y avait beaucoup de conversion de type impliquée pour gérer un large éventail de commandes dans le code Go. Des détails spécifiques sur chaque commande peuvent être trouvés dans la section Protocole du serveur de requête de la documentation.
Un problème auquel j'ai été confronté ici était que le serveur de requêtes devait être capable d'interpréter et d'exécuter du code arbitraire fourni dans les documents de conception. Sachant que Go est un langage compilé, je m'attendais à être bloqué à ce stade. Heureusement, j'ai rapidement trouvé le package Yeagi, capable d'interpréter facilement le code Go. Il permet de créer un bac à sable et de contrôler l'accès aux packages pouvant être importés dans le code interprété. Dans mon cas, j'ai décidé d'exposer uniquement mon package appelé couchgo, mais d'autres packages standards peuvent également être facilement ajoutés.
Grâce à mon travail, une application appelée CouchGO! émergé. Bien qu'il suive le protocole Query Server, il ne s'agit pas d'une réimplémentation individuelle de la version JavaScript car il a ses propres approches pour gérer les fonctions des documents de conception.
Par exemple, dans CouchGO !, il n'y a pas de fonction d'assistance comme émettre. Pour émettre des valeurs, vous les renvoyez simplement depuis la fonction map. De plus, chaque fonction du document de conception suit le même modèle : elle n'a qu'un seul argument, qui est un objet contenant des propriétés spécifiques à la fonction, et est censée renvoyer une seule valeur en conséquence. Il n'est pas nécessaire que cette valeur soit une primitive ; selon la fonction, il peut s'agir d'un objet, d'une carte, voire d'une erreur.
Pour commencer à travailler avec CouchGO !, il vous suffit de télécharger le binaire exécutable depuis mon référentiel GitHub, de le placer quelque part dans l'instance CouchDB et d'ajouter une variable d'environnement qui permet à CouchDB de démarrer CouchGO ! processus.
Par exemple, si vous placez l'exécutable couchgo dans le répertoire /opt/couchdb/bin, vous ajouterez la variable d'environnement suivante pour lui permettre de fonctionner.
export COUCHDB_QUERY_SERVER_GO="/opt/couchdb/bin/couchgo"
Pour comprendre rapidement comment écrire des fonctions avec CouchGO !, explorons l'interface de fonction suivante :
func Func(args couchgo.FuncInput) couchgo.FuncOutput { ... }
Chaque fonction de CouchGO ! suivra ce modèle, où Func est remplacé par le nom de fonction approprié. Actuellement, CouchGO! prend en charge les types de fonctions suivants :
Examinons un exemple de document de conception qui spécifie une vue avec des fonctions de mappage et de réduction, ainsi qu'une fonction validate_doc_update. De plus, nous devons préciser que nous utilisons Go comme langage.
{ "_id": "_design/ddoc-go", "views": { "view": { "map": "func Map(args couchgo.MapInput) couchgo.MapOutput {\n\tout := couchgo.MapOutput{}\n\tout = append(out, [2]interface{}{args.Doc[\"_id\"], 1})\n\tout = append(out, [2]interface{}{args.Doc[\"_id\"], 2})\n\tout = append(out, [2]interface{}{args.Doc[\"_id\"], 3})\n\t\n\treturn out\n}", "reduce": "func Reduce(args couchgo.ReduceInput) couchgo.ReduceOutput {\n\tout := 0.0\n\n\tfor _, value := range args.Values {\n\t\tout += value.(float64)\n\t}\n\n\treturn out\n}" } }, "validate_doc_update": "func Validate(args couchgo.ValidateInput) couchgo.ValidateOutput {\n\tif args.NewDoc[\"type\"] == \"post\" {\n\t\tif args.NewDoc[\"title\"] == nil || args.NewDoc[\"content\"] == nil {\n\t\t\treturn couchgo.ForbiddenError{Message: \"Title and content are required\"}\n\t\t}\n\n\t\treturn nil\n\t}\n\n\tif args.NewDoc[\"type\"] == \"comment\" {\n\t\tif args.NewDoc[\"post\"] == nil || args.NewDoc[\"author\"] == nil || args.NewDoc[\"content\"] == nil {\n\t\t\treturn couchgo.ForbiddenError{Message: \"Post, author, and content are required\"}\n\t\t}\n\n\t\treturn nil\n\t}\n\n\tif args.NewDoc[\"type\"] == \"user\" {\n\t\tif args.NewDoc[\"username\"] == nil || args.NewDoc[\"email\"] == nil {\n\t\t\treturn couchgo.ForbiddenError{Message: \"Username and email are required\"}\n\t\t}\n\n\t\treturn nil\n\t}\n\n\treturn couchgo.ForbiddenError{Message: \"Invalid document type\"}\n}", "language": "go" }
Maintenant, décomposons chaque fonction en commençant par la fonction map :
func Map(args couchgo.MapInput) couchgo.MapOutput { out := couchgo.MapOutput{} out = append(out, [2]interface{}{args.Doc["_id"], 1}) out = append(out, [2]interface{}{args.Doc["_id"], 2}) out = append(out, [2]interface{}{args.Doc["_id"], 3}) return out }
In CouchGO!, there is no emit function; instead, you return a slice of key-value tuples where both key and value can be of any type. The document object isn't directly passed to the function as in JavaScript; rather, it's wrapped in an object. The document itself is simply a hashmap of various values.
Next, let’s examine the reduce function:
func Reduce(args couchgo.ReduceInput) couchgo.ReduceOutput { out := 0.0 for _, value := range args.Values { out += value.(float64) } return out }
Similar to JavaScript, the reduce function in CouchGO! takes keys, values, and a rereduce parameter, all wrapped into a single object. This function should return a single value of any type that represents the result of the reduction operation.
Finally, let’s look at the Validate function, which corresponds to the validate_doc_update property:
func Validate(args couchgo.ValidateInput) couchgo.ValidateOutput { if args.NewDoc["type"] == "post" { if args.NewDoc["title"] == nil || args.NewDoc["content"] == nil { return couchgo.ForbiddenError{Message: "Title and content are required"} } return nil } if args.NewDoc["type"] == "comment" { if args.NewDoc["post"] == nil || args.NewDoc["author"] == nil || args.NewDoc["content"] == nil { return couchgo.ForbiddenError{Message: "Post, author, and content are required"} } return nil } return nil }
In this function, we receive parameters such as the new document, old document, user context, and security object, all wrapped into one object passed as a function argument. Here, we’re expected to validate if the document can be updated and return an error if not. Similar to the JavaScript version, we can return two types of errors: ForbiddenError or UnauthorizedError. If the document can be updated, we should return nil.
For more detailed examples, they can be found in my GitHub repository. One important thing to note is that the function names are not arbitrary; they should always match the type of function they represent, such as Map, Reduce, Filter, etc.
Even though writing my own Query Server was a really fun experience, it wouldn’t make much sense if I didn’t compare it with existing solutions. So, I prepared a few simple tests in a Docker container to check how much faster CouchGO! can:
I seeded the database with the expected number of documents and measured response times or differentiated timestamp logs from the Docker container using dedicated shell scripts. The details of the implementation can be found in my GitHub repository. The results are presented in the table below.
Test | CouchGO! | CouchJS | Boost |
---|---|---|---|
Indexing | 141.713s | 421.529s | 2.97x |
Reducing | 7672ms | 15642ms | 2.04x |
Filtering | 28.928s | 80.594s | 2.79x |
Updating | 7.742s | 9.661s | 1.25x |
As you can see, the boost over the JavaScript implementation is significant: almost three times faster in the case of indexing, more than twice as fast for reduce and filter functions. The boost is relatively small for update functions, but still faster than JavaScript.
As the author of the documentation promised, writing a custom Query Server wasn’t that hard when following the Query Server Protocol. Even though CouchGO! lacks a few deprecated functions in general, it provides a significant boost over the JavaScript version even at this early stage of development. I believe there is still plenty of room for improvements.
If you need all the code from this article in one place, you can find it in my GitHub repository.
Thank you for reading this article. I would love to hear your thoughts about this solution. Would you use it with your CouchDB instance, or maybe you already use some custom-made Query Server? I would appreciate hearing about it in the comments.
Don’t forget to check out my other articles for more tips, insights, and other parts of this series as they are created. Happy hacking!
Ce qui précède est le contenu détaillé de. pour plus d'informations, suivez d'autres articles connexes sur le site Web de PHP en chinois!