Dans AEM, les requêtes lentes sont classées dans 3 catégories principales, suivant le degré de gravité :
Requêtes sans index
Requêtes mal limitées
Requêtes avec jeu de résultats volumineux
Les 2 premières classifications de requêtes (sans index et mal limitées) sont lentes, car elles forcent le moteur de requête Oak à inspecter chacune des potentiel résultat (noeud de contenu ou entrée d’index) pour identifier celui qui appartient à réel jeu de résultats.
Le fait d’inspecter chaque résultat potentiel est désigné sous le nom de traversée.
Chaque résultat potentiel devant être inspecté, le coût lié à l’identification du jeu de résultats réel augmente de façon linéaire avec le nombre de résultats potentiels.
L’ajout de restrictions de requête et l’optimisation des index permettent de stocker les données d’index dans un format optimisé, ce qui se traduit par une récupération rapide des résultats. En outre, cela réduit la nécessité de recourir à une inspection linéaire des jeux de résultats potentiels, voire permet de s’en passer complètement.
Par défaut, dans AEM 6.3, lorsqu’une traversée de 100 000 est atteinte, la requête échoue et génère une exception. Cette limite n’existe pas par défaut dans AEM versions antérieures à AEM 6.3, mais peut être définie via la configuration OSGi Apache Jackrabbit Query Engine et le bean JMX QueryEngineSettings (propriété LimitReads).
Expliquer all et assurez-vous que leurs plans de requête ne contiennent pas les éléments /* traverse leurs explications. Exemple de parcours du plan de requête :
[nt:unstructured] as [a] /* traverse "/content//*" where ([a].[unindexedProperty] = 'some value') and (isdescendantnode([a], [/content])) */
Contrôlez le journal error.log
à la recherche de requêtes de traversée sans index :
*INFO* org.apache.jackrabbit.oak.query.QueryImpl Traversal query (query without index) ... ; consider creating and index
Visitez l’AEM Performances des requêtes la console des opérations et Expliquer requêtes lentes recherchant des explications de traversée ou aucune requête d’index.
Expliquez toutes les requêtes et assurez-vous qu’elles sont résolues sur un index optimisé afin de correspondre aux restrictions de propriété de la requête.
indexRules
est défini pour toutes les restrictions de propriété et, au minimum, pour les restrictions de propriété les plus strictes de la requête.orderable=true.
.cqPageLucene
ne comporte pas de règle d’index pour jcr:content/cq:tags
Avant d’ajouter la règle d’index cq:tags
Règle d’index cq:tags
Requête Query Builder
type=cq:Page
property=jcr:content/cq:tags
property.value=my:tag
Plan de requête
[cq:Page] as [a] /* lucene:cqPageLucene(/oak:index/cqPageLucene?lang=fr) *:* where [a].[jcr:content/cq:tags] = 'my:tag' */
Cette requête est résolue sur l’index cqPageLucene
. Cependant, étant donné qu’il n’existe pas de règle d’index de propriété pour jcr:content
ou cq:tags
, lorsque cette restriction est évaluée, chaque enregistrement de l’index cqPageLucene
est vérifié afin de déterminer une correspondance. Cela signifie que si l’index contient un million de nœuds cq:Page
, un million d’enregistrements sont vérifiés pour déterminer le jeu de résultats.
Après avoir ajouté la règle d’index cq:tags
Règle d’index cq:tags
/oak:index/cqPageLucene/indexRules/cq:Page/properties/cqTags
@name=jcr:content/cq:tags
@propertyIndex=true
Requête Query Builder
type=cq:Page
property=jcr:content/cq:tags
property.value=myTagNamespace:myTag
Plan de requête
[cq:Page] as [a] /* lucene:cqPageLucene(/oak:index/cqPageLucene?lang=fr) jcr:content/cq:tags:my:tag where [a].[jcr:content/cq:tags] = 'my:tag' */
Ajout de la règle indexRule pour jcr:content/cq:tags
dans le cqPageLucene
index allows cq:tags
données à stocker de manière optimisée.
Lorsqu’une requête avec la variable jcr:content/cq:tags
est effectuée, l’index peut rechercher les résultats par valeur. Cela signifie que si 100 nœuds cq:Page
ont myTagNamespace:myTag
comme valeur, seuls ces 100 résultats sont renvoyés. Les 999 000 autres résultats sont exclus des contrôles de restriction, ce qui améliore les performances d’un facteur 10 000.
Il va sans dire que des restrictions de requête supplémentaires réduisent les jeux de résultats éligibles et améliorent encore l’optimisation des requêtes.
De même, sans règle d’index supplémentaire pour la variable cq:tags
, même une requête en texte intégral avec une restriction sur cq:tags
ne fonctionnerait pas correctement, car les résultats de l’index retourneraient toutes les correspondances de texte intégral. La restriction sur cq:tags sera filtrée après.
Les listes de contrôle d’accès constituent une autre cause de filtrage post index. Bien souvent, il n’en est pas tenu compte en cours de développement. Tâchez de vous assurer que la requête ne renvoie pas de chemins d’accès auxquels l’utilisateur risque ne pas avoir accès. En règle générale, cela passe par une meilleure structure de contenu, ainsi que la définition d’une restriction de chemin d’accès appropriée sur la requête.
Une méthode utile pour déterminer si l’index Lucene renvoie de nombreux résultats pour renvoyer un très petit sous-ensemble en tant que résultat de la requête consiste à activer les journaux DEBUG pour org.apache.jackrabbit.oak.plugins.index.lucene.LucenePropertyIndex
et voir le nombre de documents chargés à partir de l’index. Le nombre de résultats finaux par rapport au nombre de documents chargés ne devrait pas être disproportionné. Pour plus d’informations, voir Journalisation.
Surveillez la variable error.log
pour les requêtes traversales :
*WARN* org.apache.jackrabbit.oak.spi.query.Cursors$TraversingCursor Traversed ### nodes ... consider creating an index or changing the query
Visitez l’AEM Performances des requêtes la console des opérations et Expliquer requêtes lentes recherchant des plans de requête qui ne résolvent pas les restrictions de propriété de requête en règles de propriété d’index.
Définissez des seuils bas pour oak.queryLimitInMemory (par exemple, 10000) et oak.queryLimitReads (par exemple, 5000) et optimisez les requêtes coûteuses lorsque vous obtenez une exception UnsupportedOperationException indiquant que la requête lit plus de x nœuds… (The query read more than x nodes…).
Cela permet d’éviter les requêtes gourmandes en ressources (c’est-à-dire non soutenues par un index ou soutenues par un index moins étendu). Par exemple, une requête qui lit 1 million de nœuds générerait un grand nombre d’E/S et aurait un impact négatif sur les performances globales de l’application. Par conséquent, toute requête qui échoue en raison des limites ci-dessus doit être analysée et optimisée.
Surveillez les journaux à la recherche de requêtes déclenchant une traversée de nœuds importante ou une consommation élevée de mémoire de tas :
*WARN* ... java.lang.UnsupportedOperationException: The query read or traversed more than 100000 nodes. To avoid affecting other tasks, processing was stopped.
Surveillez les journaux à la recherche de requêtes déclenchant une consommation importante de mémoire de tas :
*WARN* ... java.lang.UnsupportedOperationException: The query read more than 500000 nodes in memory. To avoid running out of memory, processing was stopped
Pour les versions 6.0 à 6.2 d’AEM, vous pouvez régler le seuil de traversée de noeuds via les paramètres JVM dans le script de démarrage AEM afin d’éviter que les requêtes volumineuses ne surchargent l’environnement. Les valeurs recommandées sont les suivantes :
-Doak.queryLimitInMemory=500000
-Doak.queryLimitReads=100000
Dans AEM 6.3, les 2 paramètres ci-dessus sont préconfigurés par défaut et peuvent être modifiés dans les paramètres OSGi QueryEngineSettings.
Plus d’informations disponibles sous : https://jackrabbit.apache.org/oak/docs/query/query-engine.html#Slow_Queries_and_Read_Limits
S’agissant de l’optimisation des performances des requêtes dans AEM, la devise est :
« Plus il y a de restrictions, mieux c’est ! »
Voici un aperçu des réglages recommandés pour garantir les performances des requêtes. Commencez par optimiser la requête, qui est une opération plus discrète, puis, si nécessaire, optimisez les définitions d’index.
AEM prend en charge les langages de requête suivants :
Query Builder est utilisé dans l’exemple suivant, car il s’agit du langage de requête employé le plus couramment par les développeurs AEM. Cependant, les mêmes principes s’appliquent également à JCR-SQL2 et XPath.
Ajoutez une restriction de type de nœud, de sorte que la requête soit résolue sur un index de propriété Lucene existant.
Requête non optimisée
property=jcr:content/contentType
property.value=article-page
Requête optimisée
type=cq:Page
property=jcr:content/contentType
property.value=article-page
Dans le cas des requêtes dépourvues d’une restriction du type de nœud, AEM suppose qu’il s’agit du type de nœud nt:base
, dont chaque nœud d’AEM est un sous-type, ce qui se traduit effectivement par l’absence de restriction de ce type.
Paramètre type=cq:Page
limite cette requête à uniquement cq:Page
et résout la requête en AEM cqPageLucene, en limitant les résultats à un sous-ensemble de noeuds (uniquement cq:Page
) dans AEM.
Réglez la restriction de type de nœud de la requête, de sorte que cette dernière soit résolue sur un index de propriété Lucene existant.
Requête non optimisée
type=nt:hierarchyNode
property=jcr:content/contentType
property.value=article-page
Requête optimisée
type=cq:Page
property=jcr:content/contentType
property.value=article-page
nt:hierarchyNode
est le type de noeud parent de cq:Page
, et en supposant jcr:content/contentType=article-page
n’est appliqué qu’à cq:Page
noeuds via notre application personnalisée, cette requête ne renverra que cq:Page
noeud où jcr:content/contentType=article-page
. Il s’agit toutefois d’une restriction sous-optimale, pour les raisons suivantes :
nt:hierarchyNode
(p. ex. dam:Asset
) en ajoutant inutilement à l’ensemble des résultats potentiels.nt:hierarchyNode
, toutefois, car il existe un index fourni pour cq:Page
.Le fait de définir type=cq:Page
limite cette requête aux seuls nœuds cq:Page
et résout la requête sur l’index cqPageLucene d’AEM, ce qui limite les résultats à un sous-ensemble de nœuds (uniquement les nœuds cq:Page) dans AEM.
Vous pouvez également ajuster la ou les restrictions de propriété afin que la requête soit résolue sur un index de propriété existant.
Requête non optimisée
property=jcr:content/contentType
property.value=article-page
Requête optimisée
property=jcr:content/sling:resourceType
property.value=my-site/components/structure/article-page
Modification de la restriction de propriété de jcr:content/contentType
(valeur personnalisée) à la propriété bien connue sling:resourceType
permet à la requête de se résoudre sur l’index de propriété slingResourceType
qui indexe tout le contenu en sling:resourceType
.
Les index de propriété (contrairement aux index de propriété Lucene) conviennent mieux lorsque la requête ne fait pas de distinction par type de nœud et qu’une seule restriction de propriété domine le jeu de résultats.
Ajoutez la restriction de chemin la plus stricte possible à la requête. Par exemple, préférez /content/my-site/us/en
over /content/my-site
ou /content/dam
over /
.
Requête non optimisée
type=cq:Page
path=/content
property=jcr:content/contentType
property.value=article-page
Requête optimisée
type=cq:Page
path=/content/my-site/us/en
property=jcr:content/contentType
property.value=article-page
Application de la restriction de chemin à partir de path=/content
to path=/content/my-site/us/en
permet aux index de réduire le nombre d’entrées d’index qui doivent être inspectées. Lorsque la requête peut très bien restreindre le chemin, au-delà de la seule /content
ou /content/dam
, assurez-vous que l’index comporte evaluatePathRestrictions=true
.
Remarque : evaluatePathRestrictions
augmente la taille de l’index.
Si possible, évitez d’utiliser des fonctions/opérations de requête telles que LIKE
et fn:XXXX
, car leur coût évolue en fonction du nombre de résultats basés sur une restriction.
Requête non optimisée
type=cq:Page
property=jcr:content/contentType
property.operation=like
property.value=%article%
Requête optimisée
type=cq:Page
fulltext=article
fulltext.relPath=jcr:content/contentType
La condition LIKE est lente à être évaluée, car aucun index ne peut être utilisé si le texte commence par un caractère générique ("%…"). La condition jcr:contains autorise un index en texte intégral et est, de ce fait, à privilégier. Cela nécessite que l’index de propriété Lucene résolu ait indexRule pour jcr:content/contentType
avec analayzed=true
.
Utilisation de fonctions de requête comme fn:lowercase(..)
peut s’avérer plus difficile à optimiser, car il n’existe pas d’équivalents plus rapides (en dehors des configurations d’analyseur d’index plus complexes et intrusives). Il est préférable d’identifier d’autres restrictions d’étendue afin d’améliorer les performances globales des requêtes, ce qui exige que les fonctions s’exécutent sur le plus petit jeu possible de résultats potentiels.
Ce réglage est spécifique à Query Builder et ne s’applique pas à JCR-SQL2 ni à XPath.
Utilisation guessTotal de Query Builder lorsque l’ensemble complet des résultats n’est pas nécessaire immédiatement.
Requête non optimisée
type=cq:Page
path=/content
Requête optimisée
type=cq:Page
path=/content
p.guessTotal=100
Dans les cas où l’exécution de la requête est rapide mais où le nombre de résultats est élevé, p. guessTotal
est une optimisation essentielle pour les requêtes Query Builder.
Le paramètre p.guessTotal=100
indique à Query Builder de ne collecter que les 100 premiers résultats et de définir un indicateur booléen pour signaler l’existence d’au moins un résultat supplémentaire (sans calculer toutefois ce nombre, car cela entraînerait un ralentissement des performances). Cette optimisation donne d’excellents résultats pour la pagination ou le chargement infini, deux scénarios dans lesquels seul un sous-ensemble de résultats est affiché de manière incrémentielle.
Si la requête optimale est résolue sur un index de propriété, il n’y a rien d’autre à faire, dans la mesure où les index de ce type présentent des capacités de réglage minimales.
Dans le cas contraire, la requête doit se résoudre sur un index de propriété Lucene. Si aucun index ne peut être résolu, passez à la création d’un index.
Le cas échéant, convertissez la requête au format XPath ou JCR-SQL2.
Requête Query Builder
query type=cq:Page
path=/content/my-site/us/en
property=jcr:content/contentType
property.value=article-page
orderby=@jcr:content/publishDate
orderby.sort=desc
XPath généré à partir de la requête Query Builder
/jcr:root/content/my-site/us/en//element(*, cq:Page)[jcr:content/@contentType = 'article-page'] order by jcr:content/@publishDate descending
Fournissez le XPath (ou JCR-SQL2) au Générateur de définitions d’index en Oak afin de générer la définition d’index de propriété Lucene optimisée.
Définition d’index de propriété Lucene générée
- evaluatePathRestrictions = true
- compatVersion = 2
- type = "lucene"
- async = "async"
- jcr:primaryType = oak:QueryIndexDefinition
+ indexRules
+ cq:Page
+ properties
+ contentType
- name = "jcr:content/contentType"
- propertyIndex = true
+ publishDate
- ordered = true
- name = "jcr:content/publishDate"
Fusionnez manuellement la définition générée dans l’index de propriété Lucene existant de manière additive. Veillez à ne pas supprimer les configurations existantes, car elles peuvent être utilisées pour accomplir d’autres requêtes.
/oak:index/cqPageLucene
.Vérifiez que la requête n’est pas résolue sur un index de propriété Lucene existant. Si tel est le cas, consultez la section précédente traitant de l’optimisation d’un index existant.
Le cas échéant, convertissez la requête au format XPath ou JCR-SQL2.
Requête Query Builder
type=myApp:Author
property=firstName
property.value=ira
XPath généré à partir de la requête Query Builder
//element(*, myApp:Page)[@firstName = 'ira']
Fournissez le XPath (ou JCR-SQL2) au Générateur de définitions d’index en Oak afin de générer la définition d’index de propriété Lucene optimisée.
Définition d’index de propriété Lucene générée
- compatVersion = 2
- type = "lucene"
- async = "async"
- jcr:primaryType = oak:QueryIndexDefinition
+ indexRules
+ myApp:AuthorModel
+ properties
+ firstName
- name = "firstName"
- propertyIndex = true
Déployez la définition d’index de propriété Lucene générée.
Ajoutez la définition XML fournie par le Générateur de définitions d’index en Oak du nouvel index au projet AEM qui gère les définitions d’index Oak (pour rappel, traitez les définitions d’index Oak comme du code, car le code dépend de celles-ci).
Déployez et testez le nouvel index suivant le cycle de vie de développement habituel des logiciels AEM. Vérifiez également que la requête est résolue sur l’index et qu’elle est performante.
Lors du déploiement initial de cet index, AEM va le remplir avec les données requises.
Compte tenu de l’architecture de contenu flexible d’AEM, il est difficile d’affirmer que les structures de contenu n’évolueront pas au fil du temps pour atteindre des proportions inacceptables.
Par conséquent, assurez-vous que les index répondent aux requêtes, sauf si la combinaison de la restriction de chemin d’accès et de la restriction de type de noeud garantit que moins de 20 noeuds sont jamais parcourus.
Débogueur Query Builder
CRXDE LITE : outil de requête
Requêtes lentes/les plus courantes
Journalisation de Query Builder
DEBUG @ com.day.cq.search.impl.builder.QueryImpl
Journalisation de l’exécution des requêtes Oak
DEBUG @ org.apache.jackrabbit.oak.query
Configuration OSGi des paramètres du moteur de requête Apache Jackrabbit
Mbean JMX NodeCounter
Générateur de définitions d’index Oak