SpazioCodice

Mises à jour atomiques d'Apache Solr

Mises à jour atomiques d’Apache Solr : une approche polymorphique

Dans cet article, nous décrivons une approche pour résoudre le problème d’une application nécessitant à la fois des mises à jour complètes et atomiques, en utilisant l’un des concepts puissants de la programmation orientée objet : le polymorphisme.

En programmation orientée objet, le polymorphisme fait référence à la capacité d’une variable, d’une méthode ou d’un objet à prendre plusieurs formes.

Bien que le contexte de l’exemple ait été abstrait afin de fournir une perspective de haut niveau, une application pratique de l’approche décrite a été implémentée dans Alfresco Search Services. Alfresco Search Services offre des capacités de recherche pour Alfresco Content Services en exploitant Apache Solr. Il est utilisé tant par les versions Entreprise que Communautaire d’Alfresco Content Services.

Contexte

Le code existant crée des instances de SolrInputDocument à partir d’un modèle de données entrant. Une fois créés, les documents sont envoyés à Solr pour l’indexation.
Chaque document représente l’état complet d’un objet de domaine : cela signifie que la première fois qu’il est envoyé, il sera inséré ; la fois suivante, si le même document (c’est-à-dire un document avec le même identifiant) est envoyé, il remplace le document existant.

C’est une partie essentielle du système, et la logique est assez complexe : une instance de SolrInputDocument est créée à plusieurs endroits et transmise à de nombreuses méthodes qui l’enrichissent avec un ensemble spécifique d’attributs. Quelque chose comme ceci :

				
					public void indexScenario1(DomainObject o) {
        SolrInputDocument doc = new SolrInputDocument();
        ...
        addAttributeSetA(doc, Domain);
        addAttributeSetB(doc, Domain);
        if (something)
            addAttributeSetC(doc, Domain);
        else
            addAttributeSetD(doc, Domain);
        ...
				
			

Défi

Avec notre contribution, la partie du système qui crée les instances de modèle de domaine a été légèrement modifiée : l’amélioration principale réside dans la capacité supplémentaire de travailler avec des objets “delta”. En d’autres termes, le code appelant est capable de fournir à ce composant d’indexation des objets de domaine “complets” ou “partiels” (c’est-à-dire des objets de domaine contenant uniquement les éléments qui ont été mis à jour).

Contraintes

Jusqu’à présent, vous pensez que c’est un cas idéal pour utiliser des mises à jour atomiques ! Et c’est définitivement vrai : les objets de domaine contenant uniquement les parties modifiées peuvent être transformés en des instances de SolrInputDocument partiels, puis envoyés à Solr pour l’indexation.

Cependant, une première contrainte doit être abordée : les objets partiels ne seront pas un scénario exclusif, nous devrons toujours traiter les objets complets.

Deuxième contrainte : comme mentionné ci-dessus, le composant d’indexation représente une partie centrale/critique du système, donc même un changement minimal comporte un certain niveau de risque, les modifications du code doivent donc être minimisées.

Selon notre expérience, cela nécessite une approche “moins vous changez, mieux c’est”, et la bonne vieille programmation orientée objet est définitivement excellente pour cela !

Qu'est-ce que les mises à jour atomiques ?

Les mises à jour atomiques sont un moyen d’exécuter des commandes d’indexation côté client en utilisant une sémantique “mise à jour”, en appliquant/indiquant un document représentant un état partiel d’un objet de domaine.

Donc, en pratique, en utilisant les mises à jour atomiques, un client peut envoyer uniquement un document “partiel” qui contient uniquement les mises à jour devant être appliquées à un document existant (c’est-à-dire déjà indexé).

Voyons un exemple. Après l’indexation du document suivant :

				
					{
   "id": 1
   "title": "Design Patterns: Elements of Reusable Object-Oriented Software",
   "author": [
      "Erich Gamma",
      "Richard Helm",
      "Ralph Jonson"
   ]
}
				
			

Vous vous rendez compte qu’il manque un “h” dans “Ralph Johnson” (aaaarrgh ! Confondre le nom d’un tel Guru : inacceptable !) ; de plus, vous avez oublié John Vlissides… quelle catastrophe !

Alors, vous pouvez faire l’une des deux choses suivantes.

La méthode habituelle consiste à recréer le document entier sans l’erreur et à le renvoyer à Solr :

				
					{
  "id":1
  "title":"Design Patterns: Elements of Reusable Object-Oriented Software",
  "author":[
      "Erich Gamma",
      "Richard Helm",
      "Ralph Johnson",
      "John Vlissides"
  ]
}
				
			

Ce nouveau document remplace complètement celui indexé (note : on suppose implicitement que le champ uniqueId est “id”).

L’autre méthode permet d’envoyer uniquement ce que vous souhaitez changer sur un document existant. Dans ce cas, nous enverrions à Solr un document comme celui-ci :

				
					{
  "id": 1
  "author": {
     "remove": "Ralph Jonson",
     "add": ["Ralph Johnson", "John Vlissides"]
  }
}
				
			

Il ciblera le document indexé avec id=1 et

  • supprime la mauvaise valeur (“Ralph Jonson”)
  • ajoute la valeur correcte pour l’auteur (“Ralph Johnson”)
  • ajoute l’autre auteur manquant

Comme vous pouvez le voir, la valeur d’un champ devant être mis à jour n’est plus une valeur littérale (par exemple une chaîne de caractères, un entier) ou une liste de valeurs ; à la place, nous avons une carteles clés sont les commandes de mise à jour à appliquer (par exemple, supprimer, ajouter, définir) et les valeurs sont une ou plusieurs valeurs littérales à utiliser pour la mise à jour.

Plus d’informations sur la sémantique complète des mises à jour atomiques peuvent être trouvées dans le Guide de Référence Apache Solr[2] : ici, il est important de se rappeler que du côté de Solr, il n’y a pas de “vraie” mise à jour partielle en coulisses : la version ancienne du document est récupérée et elle est fusionnée avec l’état partiel ; après cela, le nouveau document “complet” résultant est réindexé.

Cela reste néanmoins très bénéfique, car cela réduit considérablement la quantité de données que vous pouvez transférer vers Solr lorsque vous devez mettre à jour des documents.

En Java, plus précisément avec SolrJ, la classe SolrInputDocument représente les données que nous envoyons à Solr pour l’indexation. C’est essentiellement une carte, donc nous pouvons ajouter, définir ou supprimer des champs et des valeurs.

Nous sommes intéressés par les trois méthodes suivantes :

				
					// If a field with that name doesn’t exist it adds a new entry with the
// corresponding value, otherwise the value is collected together with
// the existing value(s)
// This is typically used on multivalued fields (i.e. calling twice this
// method on the same field, will collect 2 xvalues for that field)
addField(String name, Object value)

// Sets/Replaces a field value
setField(String name, Object value)

// Remove a field from the document
removeField(String name, Object value)
				
			

La même classe est également utilisée pour représenter un document partiel. Vous pouvez le faire en définissant une carte comme valeur dans la méthode setField ou addField. La carte peut contenir un ou plusieurs modificateurs :

  • “add” : ajoute les valeurs spécifiées à un champ à valeurs multiples.
  • “remove” : supprime toutes les occurrences des valeurs spécifiées dans un champ à valeurs multiples.
  • “set” : définit ou remplace les valeurs du champ par les valeurs spécifiées, ou supprime les valeurs si « null » ou une liste vide est spécifiée comme nouvelle valeur.

Notez qu’il existe deux modificateurs supplémentaires (inc, removeregex), mais nous ne nous y intéressons pas dans ce contexte.

L'idée

Rappelons les contraintes que nous avons définies ci-dessus :

  • Le code existant effectue toujours des mises à jour complètes des documents.
  • Un changement a été apporté au niveau du code appelant : les objets de domaine entrants seront soit complets, soit partiels, en fonction du cas d’utilisation.
  • L’instance de document Solr est valorisée dans plusieurs méthodes. Une instance de SolrInputDocument est créée puis passée à plusieurs méthodes qui l’enrichissent avec un ensemble spécifique d’attributs.
  • Nous avons besoin de mises à jour partielles, mais elles ne seront pas le scénario exclusif : dans certains cas, nous aurons encore des mises à jour complètes.

L’implémentation en Java du mécanisme de mise à jour partielle décrit jusqu’ici nécessite que les méthodes addField, setField ou removeField soient conscientes de leur contexte d’exécution (mise à jour partielle ou complète).

Cela parce que, dans le cas d’une mise à jour complète, ajouter un nouvel auteur serait simplement :

				
					doc.addField(“author”, “Ralph Johnson”);
				
			

Tandis que dans une mise à jour partielle, il est nécessaire de prendre en compte la différence entre le tout premier ajout :

				
					// can't use List::of, we need a mutable list
var authors = new ArrayList<String>();
authors.add("Ralph Johnson");

doc.addField("author", Map.of("add", authors);
				
			

et les ajouts suivants :

				
					var fieldModifier = (Map<String, Object>)doc.getFieldValue("author");
var authors = (List<String>) fieldModifier.get("add");
authors.add("John Vlissides");
				
			

La logique ci-dessus (qui pourrait être écrite de manière bien plus propre) doit être appliquée pour chaque champ, à chaque appel de add/set/remove ! Existe-t-il une meilleure manière de gérer cela ? Oui, bien sûr : en créant une sous-classe de SolrInputDocument :

				
					public class PartialSolrInputDocument extends SolrInputDocument {
     static Function<String, List<Object>> LAZY_EMPTY_MUTABLE_LIST = 
                key -> new ArrayList<>();

     @Override
     @SuppressWarnings("unchecked")
     public void addField(String name, Object value) {
         Map<String, List<Object>> fieldModifier =
                 (Map<String, List<Object>>)computeIfAbsent(name, k -> {
                     remove(name);
                     setField(name, newFieldModifier("add"));

                     return getField(name);
                 }).getValue();

        ofNullable(value)
             .ifPresent(v -> 
                      fieldModifier.computeIfAbsent(
                                fieldModifier
                                  .keySet()
                                  .iterator()
                                  .next(),
                                LAZY_EMPTY_MUTABLE_LIST).add(v));
     }

     @Override
     public SolrInputField removeField(String name) {
        setField(name, newFieldModifier("set"));
        return getField(name);
     }

     private Map<String, List<String>> newFieldModifier(String op) {
        return new HashMap<>()
        {{
           put(op, null);
        }};
     }
}
				
			

La logique de cette classe peut être résumée comme suit :

  • setField : elle maintient la sémantique originale : appeler cette méthode remplacera toute valeur existante.
  • removeField : un removeField sur un document partiel signifie « Hé, je veux supprimer toute valeur existante du document indexé ». Cette sémantique est implémentée dans les mises à jour atomiques en utilisant un modificateur “set” avec une valeur nulle.
  • addField : la logique ici change en fonction du fait qu’un appel removeField a eu lieu ou non (sur un champ donné).
    • Si un removeField a eu lieu pour le champ X, il est associé à un modificateur “set” et une valeur nulle. Ensuite, “addField” est appelé, la ou les valeurs ajoutées peuplent la liste associée à ce modificateur “set”. En d’autres termes, cela signifie « Solr, prends cette définition de champ et utilise-la pour remplacer les valeurs existantes dans le document indexé ».
    • Sinon (si un removeField n’a PAS eu lieu pour le champ X) : addField collecte un ensemble de valeurs dans le modificateur “add”. En d’autres termes, les valeurs collectées sont ajoutées aux valeurs existantes dans le document indexé.

En utilisant cette approche, la question : “Devons-nous utiliser une mise à jour complète ou partielle ?” est résolue au moment de la construction de l’objet.

Voyons un exemple. Voici une méthode existante qui utilise une instance de SolrInputDocument et ajoute un nouveau nom au champ multi-valeurs “author” :

				
					public void addAuthor(SolrInputDocument doc, String authorName) {
    doc.addField(“author”, authorName)
}
				
			

Maintenant, en supposant que la méthode qui crée l’instance de SolrInputDocument soit consciente du contexte (mise à jour complète ou partielle) :

				
					// In case of full document update
SolrInputDocument doc = new SolrInputDocument();
				
			

ou

				
					// in case of partial document update (i.e. atomic update)
SolrInputDocument doc = new PartialInputDocument();
				
			

Et ensuite, quelle que soit le choix précédent, la méthode suivante fonctionne correctement :

				
					// in case of partial document update (i.e. atomic update)
SolrInputDocument doc = new PartialInputDocument();
				
			

En fonction du type d’instance de SolrInputDocument passé, la méthode addField appropriée est appelée, et le document résultant déclenche une mise à jour complète ou partielle. La même chose est valable pour toutes les autres méthodes qui remplissent l’état du document.

Ce qui est important à souligner, c’est qu’aucun changement n’a été effectué sur la signature de ces méthodes, le polymorphisme gère la bonne implémentation en fonction du type.

À titre d’information, il est important de garder à l’esprit que SolrInputDocument (et donc la sous-classe PartialSolrinputDocument) est un bon candidat pour être une classe fragile. Cela signifie que ce qui est décrit ci-dessus n’est pas destiné à agir comme une solution universelle adaptée à tous les scénarios possibles.

Share this post

Laisser un commentaire

En savoir plus sur SpazioCodice

Abonnez-vous pour poursuivre la lecture et avoir accès à l’ensemble des archives.

Poursuivre la lecture