From ad217b69acf121f4c2deedc179527c1b55bb0aca Mon Sep 17 00:00:00 2001
From: Simon Urli <simon.urli@xwiki.com>
Date: Fri, 20 Nov 2020 10:56:21 +0100
Subject: [PATCH] XWIKI-18078: Likes should be kept on a page if it is moved

  * Add a method for updating ratings and average ratings information when an entity
    reference has been moved
  * Add a new listener for updating ratings information in case of page
    rename
  * Improve existing listener for Likes for cleaning cache in case of
    page rename
  * Add a new utility method in SolrUtils
  * Small refactoring to fix a typo in RatingsManager signature
---
 xwiki-platform-core/pom.xml                   |  15 ++
 .../internal/DeletedEntityLikeListener.java   |   9 +-
 .../DeletedEntityLikeListenerTest.java        |  14 +-
 .../org/xwiki/ratings/RatingsManager.java     |  16 +-
 .../DefaultRatingsManagerFactory.java         |   2 +-
 .../internal/RatingDeletedEntityListener.java |  11 +-
 .../internal/RatingMovedEntityListener.java   |  92 +++++++++
 .../ratings/internal/SolrRatingsManager.java  |  86 ++++++++-
 .../averagerating/AverageRatingManager.java   |  13 ++
 .../SolrAverageRatingManager.java             |  72 ++++++-
 .../XObjectAverageRatingManager.java          |   8 +
 .../main/resources/META-INF/components.txt    |   3 +-
 .../RatingDeletedEntityListenerTest.java      |  23 +++
 .../RatingMovedEntityListenerTest.java        |  72 +++++++
 .../internal/SolrRatingsManagerTest.java      | 182 ++++++++++++++++--
 .../AbstractAverageRatingManagerTest.java     |   7 +
 .../SolrAverageRatingManagerTest.java         | 166 ++++++++++++++++
 .../java/org/xwiki/search/solr/SolrUtils.java |  19 ++
 .../solr/internal/DefaultSolrUtils.java       |   7 +
 19 files changed, 793 insertions(+), 24 deletions(-)
 create mode 100644 xwiki-platform-core/xwiki-platform-ratings/xwiki-platform-ratings-api/src/main/java/org/xwiki/ratings/internal/RatingMovedEntityListener.java
 create mode 100644 xwiki-platform-core/xwiki-platform-ratings/xwiki-platform-ratings-api/src/test/java/org/xwiki/ratings/internal/RatingMovedEntityListenerTest.java

diff --git a/xwiki-platform-core/pom.xml b/xwiki-platform-core/pom.xml
index bb6800f639b..6d6b6f6e6ad 100644
--- a/xwiki-platform-core/pom.xml
+++ b/xwiki-platform-core/pom.xml
@@ -192,6 +192,21 @@
                 <new>field org.xwiki.search.solr.AbstractSolrCoreInitializer.SCHEMA_BASE_VERSION</new>
                 <justification>Not a breackage</justification>
               </item>
+              <item>
+                <code>java.method.addedToInterface</code>
+                <new>method long org.xwiki.ratings.RatingsManager::moveRatings(org.xwiki.model.reference.EntityReference, org.xwiki.model.reference.EntityReference) throws org.xwiki.ratings.RatingsException</new>
+                <justification>Young API.</justification>
+              </item>
+              <item>
+                <code>java.method.removed</code>
+                <old>method void org.xwiki.ratings.RatingsManager::setIdentifer(java.lang.String)</old>
+                <justification>Young API: fix typo.</justification>
+              </item>
+              <item>
+                <code>java.method.addedToInterface</code>
+                <new>method void org.xwiki.ratings.RatingsManager::setIdentifier(java.lang.String)</new>
+                <justification>Young API: fix typo.</justification>
+              </item>
             </revapi.ignore>
           </analysisConfiguration>
         </configuration>
diff --git a/xwiki-platform-core/xwiki-platform-like/xwiki-platform-like-api/src/main/java/org/xwiki/like/internal/DeletedEntityLikeListener.java b/xwiki-platform-core/xwiki-platform-like/xwiki-platform-like-api/src/main/java/org/xwiki/like/internal/DeletedEntityLikeListener.java
index 9026d641efb..ec01226626a 100644
--- a/xwiki-platform-core/xwiki-platform-like/xwiki-platform-like-api/src/main/java/org/xwiki/like/internal/DeletedEntityLikeListener.java
+++ b/xwiki-platform-core/xwiki-platform-like/xwiki-platform-like-api/src/main/java/org/xwiki/like/internal/DeletedEntityLikeListener.java
@@ -34,6 +34,7 @@
 import org.xwiki.model.reference.EntityReference;
 import org.xwiki.observation.AbstractEventListener;
 import org.xwiki.observation.event.Event;
+import org.xwiki.refactoring.event.DocumentRenamedEvent;
 
 import com.xpn.xwiki.doc.XWikiDocument;
 import com.xpn.xwiki.internal.event.XObjectDeletedEvent;
@@ -48,6 +49,8 @@
  *     <li>deletion of entire wiki</li>
  *     <li>deletion of users</li>
  * </ul>
+ *
+ * Note that this component also listen to rename of documents.
  * The deletion of a documents will perform a partial clean of cache, to remove the information related to that
  * document (see {@link LikeManager#clearCache(EntityReference)}), while the other kinds lead to full clean of the
  * cache (see {@link LikeManager#clearCache()}).
@@ -64,7 +67,8 @@ public class DeletedEntityLikeListener extends AbstractEventListener
     private static final List<Event> EVENT_LIST = Arrays.asList(
         new DocumentDeletedEvent(),
         new WikiDeletedEvent(),
-        new XObjectDeletedEvent(BaseObjectReference.any(XWikiUsersDocumentInitializer.CLASS_REFERENCE_STRING))
+        new XObjectDeletedEvent(BaseObjectReference.any(XWikiUsersDocumentInitializer.CLASS_REFERENCE_STRING)),
+        new DocumentRenamedEvent()
     );
 
     @Inject
@@ -84,6 +88,9 @@ public void onEvent(Event event, Object source, Object data)
         if (event instanceof DocumentDeletedEvent) {
             XWikiDocument sourceDoc = (XWikiDocument) source;
             this.likeManagerProvider.get().clearCache(sourceDoc.getDocumentReference());
+        } else if (event instanceof DocumentRenamedEvent) {
+            DocumentRenamedEvent documentRenamedEvent = (DocumentRenamedEvent) event;
+            this.likeManagerProvider.get().clearCache(documentRenamedEvent.getSourceReference());
         } else {
             this.likeManagerProvider.get().clearCache();
         }
diff --git a/xwiki-platform-core/xwiki-platform-like/xwiki-platform-like-api/src/test/java/org/xwiki/like/internal/DeletedEntityLikeListenerTest.java b/xwiki-platform-core/xwiki-platform-like/xwiki-platform-like-api/src/test/java/org/xwiki/like/internal/DeletedEntityLikeListenerTest.java
index 794ccd33e6b..cde994a9fc2 100644
--- a/xwiki-platform-core/xwiki-platform-like/xwiki-platform-like-api/src/test/java/org/xwiki/like/internal/DeletedEntityLikeListenerTest.java
+++ b/xwiki-platform-core/xwiki-platform-like/xwiki-platform-like-api/src/test/java/org/xwiki/like/internal/DeletedEntityLikeListenerTest.java
@@ -28,6 +28,7 @@
 import org.xwiki.bridge.event.WikiDeletedEvent;
 import org.xwiki.like.LikeManager;
 import org.xwiki.model.reference.DocumentReference;
+import org.xwiki.refactoring.event.DocumentRenamedEvent;
 import org.xwiki.test.junit5.mockito.ComponentTest;
 import org.xwiki.test.junit5.mockito.InjectMockComponents;
 import org.xwiki.test.junit5.mockito.MockComponent;
@@ -63,7 +64,7 @@ void setup()
     }
 
     @Test
-    void onEventDocument()
+    void onEventDeletedDocument()
     {
         XWikiDocument sourceDocument = mock(XWikiDocument.class);
         DocumentReference documentReference = mock(DocumentReference.class);
@@ -73,6 +74,17 @@ void onEventDocument()
         verify(this.likeManager).clearCache(documentReference);
     }
 
+    @Test
+    void onEventRenamedDocument()
+    {
+        DocumentRenamedEvent documentRenamedEvent = mock(DocumentRenamedEvent.class);
+        DocumentReference documentReference = mock(DocumentReference.class);
+        when(documentRenamedEvent.getSourceReference()).thenReturn(documentReference);
+
+        this.listener.onEvent(documentRenamedEvent, null, null);
+        verify(this.likeManager).clearCache(documentReference);
+    }
+
     @Test
     void onEventWiki()
     {
diff --git a/xwiki-platform-core/xwiki-platform-ratings/xwiki-platform-ratings-api/src/main/java/org/xwiki/ratings/RatingsManager.java b/xwiki-platform-core/xwiki-platform-ratings/xwiki-platform-ratings-api/src/main/java/org/xwiki/ratings/RatingsManager.java
index 28c0d789ba3..0d3e0c66205 100644
--- a/xwiki-platform-core/xwiki-platform-ratings/xwiki-platform-ratings-api/src/main/java/org/xwiki/ratings/RatingsManager.java
+++ b/xwiki-platform-core/xwiki-platform-ratings/xwiki-platform-ratings-api/src/main/java/org/xwiki/ratings/RatingsManager.java
@@ -78,7 +78,7 @@ public String getFieldName()
      *
      * @param identifier the identifier to be set.
      */
-    void setIdentifer(String identifier);
+    void setIdentifier(String identifier);
 
     /**
      * @return the upper bound of the scale used by this manager for rating.
@@ -172,6 +172,20 @@ List<Rating> getRatings(Map<RatingQueryField, Object> queryParameters,
      */
     long removeRatings(EntityReference entityReference) throws RatingsException;
 
+    /**
+     * Update all ratings concerning the given reference to point to the new reference.
+     * This update is performed for both ratings targeting directly the given reference, but also for those having the
+     * reference as ancestor.
+     *
+     * @param oldReference the old reference to be updated.
+     * @param newReference the new reference.
+     * @return the total number of updated ratings.
+     * @throws RatingsException in case of problem during the query.
+     * @since 12.10
+     */
+    @Unstable
+    long moveRatings(EntityReference oldReference, EntityReference newReference) throws RatingsException;
+
     /**
      * Retrieve the average rating information of the given reference.
      *
diff --git a/xwiki-platform-core/xwiki-platform-ratings/xwiki-platform-ratings-api/src/main/java/org/xwiki/ratings/internal/DefaultRatingsManagerFactory.java b/xwiki-platform-core/xwiki-platform-ratings/xwiki-platform-ratings-api/src/main/java/org/xwiki/ratings/internal/DefaultRatingsManagerFactory.java
index e409c9cf1d6..0baa4582657 100644
--- a/xwiki-platform-core/xwiki-platform-ratings/xwiki-platform-ratings-api/src/main/java/org/xwiki/ratings/internal/DefaultRatingsManagerFactory.java
+++ b/xwiki-platform-core/xwiki-platform-ratings/xwiki-platform-ratings-api/src/main/java/org/xwiki/ratings/internal/DefaultRatingsManagerFactory.java
@@ -98,7 +98,7 @@ public RatingsManager getRatingsManager(String managerName) throws RatingsExcept
 
                 // step 4: set the information of the RatingManager
                 result.setRatingConfiguration(ratingsConfiguration);
-                result.setIdentifer(managerName);
+                result.setIdentifier(managerName);
 
                 // step 5: copy the descriptor and modifies the hint and the instantiation strategy
                 DefaultComponentDescriptor<RatingsManager> componentDescriptorCopy =
diff --git a/xwiki-platform-core/xwiki-platform-ratings/xwiki-platform-ratings-api/src/main/java/org/xwiki/ratings/internal/RatingDeletedEntityListener.java b/xwiki-platform-core/xwiki-platform-ratings/xwiki-platform-ratings-api/src/main/java/org/xwiki/ratings/internal/RatingDeletedEntityListener.java
index bc1dbdf42f9..0593989caea 100644
--- a/xwiki-platform-core/xwiki-platform-ratings/xwiki-platform-ratings-api/src/main/java/org/xwiki/ratings/internal/RatingDeletedEntityListener.java
+++ b/xwiki-platform-core/xwiki-platform-ratings/xwiki-platform-ratings-api/src/main/java/org/xwiki/ratings/internal/RatingDeletedEntityListener.java
@@ -34,10 +34,12 @@
 import org.xwiki.model.reference.EntityReference;
 import org.xwiki.model.reference.WikiReference;
 import org.xwiki.observation.AbstractEventListener;
+import org.xwiki.observation.ObservationContext;
 import org.xwiki.observation.event.Event;
 import org.xwiki.ratings.RatingsException;
 import org.xwiki.ratings.RatingsManager;
 import org.xwiki.ratings.RatingsManagerFactory;
+import org.xwiki.refactoring.event.DocumentRenamingEvent;
 
 import com.xpn.xwiki.doc.XWikiDocument;
 import com.xpn.xwiki.internal.event.XObjectDeletedEvent;
@@ -62,12 +64,17 @@ public class RatingDeletedEntityListener extends AbstractEventListener
         new DocumentDeletedEvent(),
         new WikiDeletedEvent());
 
+    private static final DocumentRenamingEvent RENAMING_EVENT = new DocumentRenamingEvent();
+
     @Inject
     private Logger logger;
 
     @Inject
     private RatingsManagerFactory ratingsManagerFactory;
 
+    @Inject
+    private ObservationContext observationContext;
+
     /**
      * Default constructor.
      */
@@ -79,7 +86,9 @@ public RatingDeletedEntityListener()
     @Override
     public void onEvent(Event event, Object source, Object data)
     {
-        if (event instanceof DocumentDeletedEvent) {
+        // if the event is sent because of a rename, we ignore it: in that case RatingMovedEntityListener will be called
+        // and will handle properly the changes to be done.
+        if (event instanceof DocumentDeletedEvent && !this.observationContext.isIn(RENAMING_EVENT)) {
             XWikiDocument sourceDoc = (XWikiDocument) source;
             this.handleDeletedReference(sourceDoc.getDocumentReference());
         } else if (event instanceof XObjectDeletedEvent) {
diff --git a/xwiki-platform-core/xwiki-platform-ratings/xwiki-platform-ratings-api/src/main/java/org/xwiki/ratings/internal/RatingMovedEntityListener.java b/xwiki-platform-core/xwiki-platform-ratings/xwiki-platform-ratings-api/src/main/java/org/xwiki/ratings/internal/RatingMovedEntityListener.java
new file mode 100644
index 00000000000..4d550a66ffc
--- /dev/null
+++ b/xwiki-platform-core/xwiki-platform-ratings/xwiki-platform-ratings-api/src/main/java/org/xwiki/ratings/internal/RatingMovedEntityListener.java
@@ -0,0 +1,92 @@
+/*
+ * See the NOTICE file distributed with this work for additional
+ * information regarding copyright ownership.
+ *
+ * This is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as
+ * published by the Free Software Foundation; either version 2.1 of
+ * the License, or (at your option) any later version.
+ *
+ * This software is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this software; if not, write to the Free
+ * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
+ * 02110-1301 USA, or see the FSF site: http://www.fsf.org.
+ */
+package org.xwiki.ratings.internal;
+
+import java.util.Collections;
+import java.util.List;
+
+import javax.inject.Inject;
+import javax.inject.Named;
+import javax.inject.Singleton;
+
+import org.apache.commons.lang3.exception.ExceptionUtils;
+import org.slf4j.Logger;
+import org.xwiki.component.annotation.Component;
+import org.xwiki.model.reference.DocumentReference;
+import org.xwiki.model.reference.EntityReference;
+import org.xwiki.observation.AbstractEventListener;
+import org.xwiki.observation.event.Event;
+import org.xwiki.ratings.RatingsException;
+import org.xwiki.ratings.RatingsManager;
+import org.xwiki.ratings.RatingsManagerFactory;
+import org.xwiki.refactoring.event.DocumentRenamedEvent;
+
+/**
+ * This listener aims at updating any ratings related to the moved entities.
+ * This component listens on {@link DocumentRenamedEvent} and calls
+ * {@link RatingsManager#moveRatings(EntityReference, EntityReference)} with the appropriate references
+ * on all instantiated ratings managers.
+ *
+ * @version $Id$
+ * @since 12.10
+ */
+@Component
+@Singleton
+@Named(RatingMovedEntityListener.NAME)
+public class RatingMovedEntityListener extends AbstractEventListener
+{
+    static final String NAME = "RatingMovedEntityListener";
+    private static final List<Event> EVENT_LIST = Collections.singletonList(
+        new DocumentRenamedEvent()
+    );
+
+    @Inject
+    private Logger logger;
+
+    @Inject
+    private RatingsManagerFactory ratingsManagerFactory;
+
+    /**
+     * Default constructor.
+     */
+    public RatingMovedEntityListener()
+    {
+        super(NAME, EVENT_LIST);
+    }
+
+    @Override
+    public void onEvent(Event event, Object source, Object data)
+    {
+        if (event instanceof DocumentRenamedEvent) {
+            DocumentRenamedEvent renamedEvent = (DocumentRenamedEvent) event;
+            DocumentReference oldReference = renamedEvent.getSourceReference();
+            DocumentReference newReference = renamedEvent.getTargetReference();
+
+            try {
+                for (RatingsManager manager : this.ratingsManagerFactory.getInstantiatedManagers()) {
+                    manager.moveRatings(oldReference, newReference);
+                }
+            } catch (RatingsException e) {
+                logger.error("Error while updating ratings related to old reference [{}] from ratings: [{}]",
+                    oldReference, ExceptionUtils.getRootCause(e));
+            }
+        }
+    }
+}
diff --git a/xwiki-platform-core/xwiki-platform-ratings/xwiki-platform-ratings-api/src/main/java/org/xwiki/ratings/internal/SolrRatingsManager.java b/xwiki-platform-core/xwiki-platform-ratings/xwiki-platform-ratings-api/src/main/java/org/xwiki/ratings/internal/SolrRatingsManager.java
index cb47aa3e89c..446c4ad336f 100644
--- a/xwiki-platform-core/xwiki-platform-ratings/xwiki-platform-ratings-api/src/main/java/org/xwiki/ratings/internal/SolrRatingsManager.java
+++ b/xwiki-platform-core/xwiki-platform-ratings/xwiki-platform-ratings-api/src/main/java/org/xwiki/ratings/internal/SolrRatingsManager.java
@@ -21,6 +21,7 @@
 
 import java.io.IOException;
 import java.util.ArrayList;
+import java.util.Collection;
 import java.util.Collections;
 import java.util.Date;
 import java.util.Iterator;
@@ -74,7 +75,9 @@
 @InstantiationStrategy(ComponentInstantiationStrategy.PER_LOOKUP)
 public class SolrRatingsManager implements RatingsManager
 {
-    private static final int AVERAGE_COMPUTATION_BATCH_SIZE = 100;
+    private static final int BULK_OPERATIONS_BATCH_SIZE = 100;
+
+    private static final String FILTER_REFERENCE_OR_PARENTS = "filter(%s:%s) AND (filter(%s:%s) OR filter(%s:%s))";
 
     private static final String AVERAGE_RATING_NOT_ENABLED_ERROR_MESSAGE =
         "This rating manager is not configured to store average rating.";
@@ -138,7 +141,7 @@ public String getIdentifier()
     }
 
     @Override
-    public void setIdentifer(String identifier)
+    public void setIdentifier(String identifier)
     {
         this.identifier = identifier;
     }
@@ -355,6 +358,13 @@ public Rating saveRating(EntityReference reference, UserReference user, int vote
     @Override
     public List<Rating> getRatings(Map<RatingQueryField, Object> queryParameters, int offset, int limit,
         RatingQueryField orderBy, boolean asc) throws RatingsException
+    {
+        SolrDocumentList rawRatings = getRawRatings(queryParameters, offset, limit, orderBy, asc);
+        return this.getRatingsFromQueryResult(rawRatings);
+    }
+
+    private SolrDocumentList getRawRatings(Map<RatingQueryField, Object> queryParameters, int offset, int limit,
+        RatingQueryField orderBy, boolean asc) throws RatingsException
     {
         SolrQuery solrQuery = new SolrQuery()
             .addFilterQuery(this.mapToQuery(queryParameters))
@@ -364,7 +374,7 @@ public List<Rating> getRatings(Map<RatingQueryField, Object> queryParameters, in
 
         try {
             QueryResponse query = this.getRatingSolrClient().query(solrQuery);
-            return this.getRatingsFromQueryResult(query.getResults());
+            return query.getResults();
         } catch (SolrServerException | IOException | SolrException e) {
             throw new RatingsException("Error while trying to get ratings", e);
         }
@@ -415,7 +425,7 @@ public boolean removeRating(String ratingIdentifier) throws RatingsException
     public long removeRatings(EntityReference entityReference) throws RatingsException
     {
         String escapedEntityReference = this.solrUtils.toFilterQueryString(entityReference, EntityReference.class);
-        String filterQuery = String.format("filter(%s:%s) AND (filter(%s:%s) OR filter(%s:%s))",
+        String filterQuery = String.format(FILTER_REFERENCE_OR_PARENTS,
             RatingQueryField.MANAGER_ID.getFieldName(), solrUtils.toFilterQueryString(this.getIdentifier()),
             RatingQueryField.ENTITY_REFERENCE.getFieldName(), escapedEntityReference,
             RatingQueryField.PARENTS_REFERENCE.getFieldName(), escapedEntityReference);
@@ -439,6 +449,70 @@ public long removeRatings(EntityReference entityReference) throws RatingsExcepti
         return result;
     }
 
+    @Override
+    public long moveRatings(EntityReference oldReference, EntityReference newReference)
+        throws RatingsException
+    {
+        String escapedEntityReference = this.solrUtils.toFilterQueryString(oldReference, EntityReference.class);
+        String filterQuery = String.format(FILTER_REFERENCE_OR_PARENTS,
+            RatingQueryField.MANAGER_ID.getFieldName(), solrUtils.toFilterQueryString(this.getIdentifier()),
+            RatingQueryField.ENTITY_REFERENCE.getFieldName(), escapedEntityReference,
+            RatingQueryField.PARENTS_REFERENCE.getFieldName(), escapedEntityReference);
+        int offset = 0;
+        SolrDocumentList rawRatings;
+        long result = 0;
+        do {
+            SolrQuery solrQuery = new SolrQuery()
+                .addFilterQuery(filterQuery)
+                .setStart(offset)
+                .setRows(BULK_OPERATIONS_BATCH_SIZE)
+                .setSort(RatingQueryField.CREATED_DATE.getFieldName(), this.getOrder(true));
+
+            try {
+                QueryResponse queryResponse = this.getRatingSolrClient().query(solrQuery);
+                rawRatings = queryResponse.getResults();
+
+                offset += BULK_OPERATIONS_BATCH_SIZE;
+                for (SolrDocument rawRating : rawRatings) {
+                    SolrInputDocument solrInputDocument = new SolrInputDocument();
+                    this.solrUtils.setId(this.solrUtils.getId(rawRating), solrInputDocument);
+
+                    EntityReference ratingReference = this.solrUtils.get(
+                        RatingQueryField.ENTITY_REFERENCE.getFieldName(), rawRating, EntityReference.class);
+
+                    if (oldReference.equals(ratingReference)) {
+                        this.solrUtils.setAtomic(SolrUtils.ATOMIC_UPDATE_MODIFIER_SET,
+                            RatingQueryField.ENTITY_REFERENCE.getFieldName(), newReference,
+                            EntityReference.class, solrInputDocument);
+                    }
+
+                    Collection<EntityReference> parentReferences = this.solrUtils
+                        .getCollection(RatingQueryField.PARENTS_REFERENCE.getFieldName(), rawRating,
+                            EntityReference.class);
+                    if (parentReferences.contains(oldReference)) {
+                        this.solrUtils.setAtomic(SolrUtils.ATOMIC_UPDATE_MODIFIER_REMOVE,
+                            RatingQueryField.PARENTS_REFERENCE
+                            .getFieldName(), oldReference, EntityReference.class, solrInputDocument);
+                        this.solrUtils.setAtomic(SolrUtils.ATOMIC_UPDATE_MODIFIER_ADD,
+                            RatingQueryField.PARENTS_REFERENCE
+                                .getFieldName(), newReference, EntityReference.class, solrInputDocument);
+                    }
+                    this.getRatingSolrClient().add(solrInputDocument);
+                    result++;
+                }
+                if (!rawRatings.isEmpty()) {
+                    this.getRatingSolrClient().commit();
+                }
+            } catch (SolrException | IOException | SolrServerException e) {
+                throw new RatingsException("Error while trying to update rating reference", e);
+            }
+        } while (!rawRatings.isEmpty());
+        if (this.getRatingConfiguration().isAverageStored()) {
+            this.getAverageRatingManager().moveAverageRatings(oldReference, newReference);
+        }
+        return result;
+    }
+
     @Override
     public AverageRating getAverageRating(EntityReference entityReference) throws RatingsException
     {
@@ -473,11 +547,11 @@ public AverageRating recomputeAverageRating(EntityReference entityReference) thr
             List<Rating> ratings;
             int offsetIndex = 0;
             do {
-                ratings = this.getRatings(queryMap, offsetIndex, AVERAGE_COMPUTATION_BATCH_SIZE,
+                ratings = this.getRatings(queryMap, offsetIndex, BULK_OPERATIONS_BATCH_SIZE,
                     RatingQueryField.CREATED_DATE, true);
                 sumOfVotes += ratings.stream().map(Rating::getVote).map(Long::valueOf).reduce(0L, Long::sum);
                 numberOfVotes += ratings.size();
-                offsetIndex += AVERAGE_COMPUTATION_BATCH_SIZE;
+                offsetIndex += BULK_OPERATIONS_BATCH_SIZE;
             } while (!ratings.isEmpty());
 
             float newAverage = Float.valueOf(sumOfVotes) / numberOfVotes;
diff --git a/xwiki-platform-core/xwiki-platform-ratings/xwiki-platform-ratings-api/src/main/java/org/xwiki/ratings/internal/averagerating/AverageRatingManager.java b/xwiki-platform-core/xwiki-platform-ratings/xwiki-platform-ratings-api/src/main/java/org/xwiki/ratings/internal/averagerating/AverageRatingManager.java
index 0f6269c4c8c..99c639ded64 100644
--- a/xwiki-platform-core/xwiki-platform-ratings/xwiki-platform-ratings-api/src/main/java/org/xwiki/ratings/internal/averagerating/AverageRatingManager.java
+++ b/xwiki-platform-core/xwiki-platform-ratings/xwiki-platform-ratings-api/src/main/java/org/xwiki/ratings/internal/averagerating/AverageRatingManager.java
@@ -24,6 +24,7 @@
 import org.xwiki.ratings.AverageRating;
 import org.xwiki.ratings.RatingsException;
 import org.xwiki.ratings.RatingsManager;
+import org.xwiki.stability.Unstable;
 
 /**
  * Generic interface to manage {@link AverageRating}.
@@ -173,4 +174,16 @@ AverageRating resetAverageRating(EntityReference entityReference, float averageV
      * @throws RatingsException in case of problem when removing average ratings.
      */
     long removeAverageRatings(EntityReference entityReference) throws RatingsException;
+
+    /**
+     * Update reference data in case of a move of the given reference.
+     *
+     * @param oldReference the old reference that has been moved.
+     * @param newReference the new reference to store.
+     * @return the total number of average ratings updated.
+     * @throws RatingsException in case of problem during the update.
+     * @since 12.10
+     */
+    @Unstable
+    long moveAverageRatings(EntityReference oldReference, EntityReference newReference) throws RatingsException;
 }
diff --git a/xwiki-platform-core/xwiki-platform-ratings/xwiki-platform-ratings-api/src/main/java/org/xwiki/ratings/internal/averagerating/SolrAverageRatingManager.java b/xwiki-platform-core/xwiki-platform-ratings/xwiki-platform-ratings-api/src/main/java/org/xwiki/ratings/internal/averagerating/SolrAverageRatingManager.java
index 6283e3f0872..2dc82f49ed3 100644
--- a/xwiki-platform-core/xwiki-platform-ratings/xwiki-platform-ratings-api/src/main/java/org/xwiki/ratings/internal/averagerating/SolrAverageRatingManager.java
+++ b/xwiki-platform-core/xwiki-platform-ratings/xwiki-platform-ratings-api/src/main/java/org/xwiki/ratings/internal/averagerating/SolrAverageRatingManager.java
@@ -21,6 +21,7 @@
 
 import java.io.IOException;
 import java.util.ArrayList;
+import java.util.Collection;
 import java.util.List;
 import java.util.UUID;
 
@@ -32,6 +33,7 @@
 import org.apache.solr.client.solrj.SolrServerException;
 import org.apache.solr.client.solrj.response.QueryResponse;
 import org.apache.solr.common.SolrDocument;
+import org.apache.solr.common.SolrDocumentList;
 import org.apache.solr.common.SolrInputDocument;
 import org.xwiki.component.annotation.Component;
 import org.xwiki.component.annotation.InstantiationStrategy;
@@ -39,6 +41,7 @@
 import org.xwiki.model.reference.EntityReference;
 import org.xwiki.ratings.AverageRating;
 import org.xwiki.ratings.RatingsException;
+import org.xwiki.ratings.RatingsManager;
 import org.xwiki.search.solr.Solr;
 import org.xwiki.search.solr.SolrException;
 import org.xwiki.search.solr.SolrUtils;
@@ -54,6 +57,10 @@
 @InstantiationStrategy(ComponentInstantiationStrategy.PER_LOOKUP)
 public class SolrAverageRatingManager extends AbstractAverageRatingManager
 {
+    private static final int BULK_OPERATIONS_BATCH_SIZE = 100;
+
+    private static final String FILTER_REFERENCE_OR_PARENTS = "filter(%s:%s) AND (filter(%s:%s) OR filter(%s:%s))";
+
     @Inject
     private SolrUtils solrUtils;
 
@@ -132,7 +139,7 @@ public AverageRating getAverageRating(EntityReference entityReference) throws Ra
     public long removeAverageRatings(EntityReference entityReference) throws RatingsException
     {
         String escapedEntityReference = this.solrUtils.toFilterQueryString(entityReference, EntityReference.class);
-        String filterQuery = String.format("filter(%s:%s) AND (filter(%s:%s) OR filter(%s:%s))",
+        String filterQuery = String.format(FILTER_REFERENCE_OR_PARENTS,
             AverageRatingQueryField.MANAGER_ID.getFieldName(), solrUtils.toFilterQueryString(this.getIdentifier()),
             AverageRatingQueryField.ENTITY_REFERENCE.getFieldName(), escapedEntityReference,
             AverageRatingQueryField.PARENTS.getFieldName(), escapedEntityReference);
@@ -153,6 +160,69 @@ public long removeAverageRatings(EntityReference entityReference) throws Ratings
         return result;
     }
 
+    @Override
+    public long moveAverageRatings(EntityReference oldReference, EntityReference newReference)
+        throws RatingsException
+    {
+        String escapedEntityReference = this.solrUtils.toFilterQueryString(oldReference, EntityReference.class);
+        String filterQuery = String.format(FILTER_REFERENCE_OR_PARENTS,
+            AverageRatingQueryField.MANAGER_ID.getFieldName(), solrUtils.toFilterQueryString(this.getIdentifier()),
+            AverageRatingQueryField.ENTITY_REFERENCE.getFieldName(), escapedEntityReference,
+            AverageRatingQueryField.PARENTS.getFieldName(), escapedEntityReference);
+        int offset = 0;
+        SolrDocumentList rawRatings;
+        long result = 0;
+        do {
+            SolrQuery solrQuery = new SolrQuery()
+                .addFilterQuery(filterQuery)
+                .setStart(offset)
+                .setRows(BULK_OPERATIONS_BATCH_SIZE)
+                .setSort(AverageRatingQueryField.UPDATED_AT.getFieldName(), this.getOrder(true));
+
+            try {
+                QueryResponse queryResponse = this.getAverageRatingSolrClient().query(solrQuery);
+                rawRatings = queryResponse.getResults();
+
+                offset += BULK_OPERATIONS_BATCH_SIZE;
+                for (SolrDocument rawRating : rawRatings) {
+                    SolrInputDocument solrInputDocument = new SolrInputDocument();
+                    this.solrUtils.setId(this.solrUtils.getId(rawRating), solrInputDocument);
+
+                    EntityReference ratingReference = this.solrUtils.get(
+                        RatingsManager.RatingQueryField.ENTITY_REFERENCE.getFieldName(), rawRating,
+                        EntityReference.class);
+
+                    if (oldReference.equals(ratingReference)) {
+                        this.solrUtils.setAtomic(SolrUtils.ATOMIC_UPDATE_MODIFIER_SET,
+                            RatingsManager.RatingQueryField.ENTITY_REFERENCE.getFieldName(), newReference,
+                            EntityReference.class, solrInputDocument);
+                    }
+
+                    Collection<EntityReference> parentReferences = this.solrUtils
+                        .getCollection(RatingsManager.RatingQueryField.PARENTS_REFERENCE.getFieldName(), rawRating,
+                            EntityReference.class);
+                    if (parentReferences.contains(oldReference)) {
+                        this.solrUtils.setAtomic(SolrUtils.ATOMIC_UPDATE_MODIFIER_REMOVE,
+                            RatingsManager.RatingQueryField.PARENTS_REFERENCE
+                                .getFieldName(), oldReference, EntityReference.class, solrInputDocument);
+                        this.solrUtils.setAtomic(SolrUtils.ATOMIC_UPDATE_MODIFIER_ADD,
+                            RatingsManager.RatingQueryField.PARENTS_REFERENCE
+                                .getFieldName(), newReference, EntityReference.class, solrInputDocument);
+                    }
+                    this.getAverageRatingSolrClient().add(solrInputDocument);
+                    result++;
+
+                }
+                if (!rawRatings.isEmpty()) {
+                    this.getAverageRatingSolrClient().commit();
+                }
+            } catch (SolrException | IOException | SolrServerException e) {
+                throw new RatingsException("Error while trying to update average rating reference", e);
+            }
+        } while (!rawRatings.isEmpty());
+        return result;
+    }
+
     @Override
     protected void saveAverageRating(AverageRating averageRating) throws RatingsException
     {
diff --git a/xwiki-platform-core/xwiki-platform-ratings/xwiki-platform-ratings-api/src/main/java/org/xwiki/ratings/internal/averagerating/XObjectAverageRatingManager.java b/xwiki-platform-core/xwiki-platform-ratings/xwiki-platform-ratings-api/src/main/java/org/xwiki/ratings/internal/averagerating/XObjectAverageRatingManager.java
index cb05c8e1e27..b9b8d13e2b0 100644
--- a/xwiki-platform-core/xwiki-platform-ratings/xwiki-platform-ratings-api/src/main/java/org/xwiki/ratings/internal/averagerating/XObjectAverageRatingManager.java
+++ b/xwiki-platform-core/xwiki-platform-ratings/xwiki-platform-ratings-api/src/main/java/org/xwiki/ratings/internal/averagerating/XObjectAverageRatingManager.java
@@ -111,6 +111,14 @@ public long removeAverageRatings(EntityReference entityReference) throws Ratings
         return result;
     }
 
+    @Override
+    public long moveAverageRatings(EntityReference oldReference, EntityReference newReference)
+        throws RatingsException
+    {
+        // We don't need to do anything here: if a document reference has been moved then the xobject move with it.
+        return 0;
+    }
+
     private BaseObject retrieveAverageRatingXObject(EntityReference entityReference) throws Exception
     {
         DocumentModelBridge documentInstance = this.documentAccessBridge.getDocumentInstance(entityReference);
diff --git a/xwiki-platform-core/xwiki-platform-ratings/xwiki-platform-ratings-api/src/main/resources/META-INF/components.txt b/xwiki-platform-core/xwiki-platform-ratings/xwiki-platform-ratings-api/src/main/resources/META-INF/components.txt
index 43c088acd5f..6a0a786db27 100644
--- a/xwiki-platform-core/xwiki-platform-ratings/xwiki-platform-ratings-api/src/main/resources/META-INF/components.txt
+++ b/xwiki-platform-core/xwiki-platform-ratings/xwiki-platform-ratings-api/src/main/resources/META-INF/components.txt
@@ -12,4 +12,5 @@ org.xwiki.ratings.internal.averagerating.AverageRatingProtectionListener
 org.xwiki.ratings.internal.migration.R120901000XWIKI17761DataMigration
 org.xwiki.ratings.internal.migration.SolrDocumentMigration120900000
 org.xwiki.ratings.internal.RatingsConfigurationSource
-org.xwiki.ratings.internal.RatingDeletedEntityListener
\ No newline at end of file
+org.xwiki.ratings.internal.RatingDeletedEntityListener
+org.xwiki.ratings.internal.RatingMovedEntityListener
\ No newline at end of file
diff --git a/xwiki-platform-core/xwiki-platform-ratings/xwiki-platform-ratings-api/src/test/java/org/xwiki/ratings/internal/RatingDeletedEntityListenerTest.java b/xwiki-platform-core/xwiki-platform-ratings/xwiki-platform-ratings-api/src/test/java/org/xwiki/ratings/internal/RatingDeletedEntityListenerTest.java
index cdf10b4d350..624d7b40722 100644
--- a/xwiki-platform-core/xwiki-platform-ratings/xwiki-platform-ratings-api/src/test/java/org/xwiki/ratings/internal/RatingDeletedEntityListenerTest.java
+++ b/xwiki-platform-core/xwiki-platform-ratings/xwiki-platform-ratings-api/src/test/java/org/xwiki/ratings/internal/RatingDeletedEntityListenerTest.java
@@ -27,8 +27,10 @@
 import org.xwiki.model.reference.DocumentReference;
 import org.xwiki.model.reference.EntityReference;
 import org.xwiki.model.reference.WikiReference;
+import org.xwiki.observation.ObservationContext;
 import org.xwiki.ratings.RatingsManager;
 import org.xwiki.ratings.RatingsManagerFactory;
+import org.xwiki.refactoring.event.DocumentRenamingEvent;
 import org.xwiki.test.junit5.mockito.ComponentTest;
 import org.xwiki.test.junit5.mockito.InjectMockComponents;
 import org.xwiki.test.junit5.mockito.MockComponent;
@@ -37,6 +39,7 @@
 import com.xpn.xwiki.internal.event.XObjectDeletedEvent;
 
 import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.never;
 import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.when;
 
@@ -54,6 +57,9 @@ public class RatingDeletedEntityListenerTest
     @MockComponent
     private RatingsManagerFactory ratingsManagerFactory;
 
+    @MockComponent
+    private ObservationContext observationContext;
+
     @Test
     void onDocumentDeletedEvent() throws Exception
     {
@@ -70,6 +76,23 @@ void onDocumentDeletedEvent() throws Exception
         verify(manager2).removeRatings(reference);
     }
 
+    @Test
+    void onDocumentDeletedEventBecauseOfRenaming() throws Exception
+    {
+        RatingsManager manager1 = mock(RatingsManager.class);
+        RatingsManager manager2 = mock(RatingsManager.class);
+        when(ratingsManagerFactory.getInstantiatedManagers()).thenReturn(Arrays.asList(manager1, manager2));
+        when(observationContext.isIn(new DocumentRenamingEvent())).thenReturn(true);
+
+        XWikiDocument sourceDoc = mock(XWikiDocument.class);
+        DocumentReference reference = mock(DocumentReference.class);
+        when(sourceDoc.getDocumentReference()).thenReturn(reference);
+
+        this.listener.onEvent(new DocumentDeletedEvent(reference), sourceDoc, null);
+        verify(manager1, never()).removeRatings(reference);
+        verify(manager2, never()).removeRatings(reference);
+    }
+
     @Test
     void onXObjectDeletedEventEvent() throws Exception
     {
diff --git a/xwiki-platform-core/xwiki-platform-ratings/xwiki-platform-ratings-api/src/test/java/org/xwiki/ratings/internal/RatingMovedEntityListenerTest.java b/xwiki-platform-core/xwiki-platform-ratings/xwiki-platform-ratings-api/src/test/java/org/xwiki/ratings/internal/RatingMovedEntityListenerTest.java
new file mode 100644
index 00000000000..bdcb8a5a03a
--- /dev/null
+++ b/xwiki-platform-core/xwiki-platform-ratings/xwiki-platform-ratings-api/src/test/java/org/xwiki/ratings/internal/RatingMovedEntityListenerTest.java
@@ -0,0 +1,72 @@
+/*
+ * See the NOTICE file distributed with this work for additional
+ * information regarding copyright ownership.
+ *
+ * This is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as
+ * published by the Free Software Foundation; either version 2.1 of
+ * the License, or (at your option) any later version.
+ *
+ * This software is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this software; if not, write to the Free
+ * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
+ * 02110-1301 USA, or see the FSF site: http://www.fsf.org.
+ */
+package org.xwiki.ratings.internal;
+
+import java.util.Arrays;
+
+import org.junit.jupiter.api.Test;
+import org.xwiki.bridge.event.DocumentDeletedEvent;
+import org.xwiki.model.reference.DocumentReference;
+import org.xwiki.ratings.RatingsManager;
+import org.xwiki.ratings.RatingsManagerFactory;
+import org.xwiki.refactoring.event.DocumentRenamedEvent;
+import org.xwiki.test.junit5.mockito.ComponentTest;
+import org.xwiki.test.junit5.mockito.InjectMockComponents;
+import org.xwiki.test.junit5.mockito.MockComponent;
+
+import com.xpn.xwiki.doc.XWikiDocument;
+
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+/**
+ * Tests for {@link RatingMovedEntityListener}.
+ *
+ * @version $Id$
+ * @since 12.10
+ */
+@ComponentTest
+public class RatingMovedEntityListenerTest
+{
+    @InjectMockComponents
+    private RatingMovedEntityListener listener;
+
+    @MockComponent
+    private RatingsManagerFactory ratingsManagerFactory;
+
+    @Test
+    void onDocumentRenamedEvent() throws Exception
+    {
+        RatingsManager manager1 = mock(RatingsManager.class);
+        RatingsManager manager2 = mock(RatingsManager.class);
+        when(ratingsManagerFactory.getInstantiatedManagers()).thenReturn(Arrays.asList(manager1, manager2));
+
+        DocumentRenamedEvent documentRenamedEvent = mock(DocumentRenamedEvent.class);
+        DocumentReference oldReference = mock(DocumentReference.class);
+        DocumentReference newReference = mock(DocumentReference.class);
+        when(documentRenamedEvent.getSourceReference()).thenReturn(oldReference);
+        when(documentRenamedEvent.getTargetReference()).thenReturn(newReference);
+
+        this.listener.onEvent(documentRenamedEvent, null, null);
+        verify(manager1).moveRatings(oldReference, newReference);
+        verify(manager2).moveRatings(oldReference, newReference);
+    }
+}
diff --git a/xwiki-platform-core/xwiki-platform-ratings/xwiki-platform-ratings-api/src/test/java/org/xwiki/ratings/internal/SolrRatingsManagerTest.java b/xwiki-platform-core/xwiki-platform-ratings/xwiki-platform-ratings-api/src/test/java/org/xwiki/ratings/internal/SolrRatingsManagerTest.java
index f70d78a6ace..23669edbe6d 100644
--- a/xwiki-platform-core/xwiki-platform-ratings/xwiki-platform-ratings-api/src/test/java/org/xwiki/ratings/internal/SolrRatingsManagerTest.java
+++ b/xwiki-platform-core/xwiki-platform-ratings/xwiki-platform-ratings-api/src/test/java/org/xwiki/ratings/internal/SolrRatingsManagerTest.java
@@ -20,6 +20,7 @@
 package org.xwiki.ratings.internal;
 
 import java.lang.reflect.Type;
+import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.Collections;
 import java.util.Date;
@@ -75,6 +76,7 @@
 import static org.mockito.Mockito.doAnswer;
 import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.times;
 import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.when;
 
@@ -182,7 +184,7 @@ void countRatings() throws Exception
         queryParameters.put(RatingQueryField.SCALE, 12);
 
         String managerId = "managerTest";
-        this.manager.setIdentifer(managerId);
+        this.manager.setIdentifier(managerId);
         when(this.configuration.hasDedicatedCore()).thenReturn(true);
         when(this.solr.getClient(managerId)).thenReturn(this.solrClient);
 
@@ -207,7 +209,7 @@ void getRatings() throws Exception
         queryParameters.put(RatingQueryField.SCALE, "6");
 
         String managerId = "otherId";
-        this.manager.setIdentifer(managerId);
+        this.manager.setIdentifier(managerId);
         when(this.configuration.hasDedicatedCore()).thenReturn(false);
         when(this.solr.getClient(RatingSolrCoreInitializer.DEFAULT_RATINGS_SOLR_CORE)).thenReturn(this.solrClient);
 
@@ -311,7 +313,7 @@ void getRatings() throws Exception
     void getAverageRating() throws Exception
     {
         String managerId = "averageId2";
-        this.manager.setIdentifer(managerId);
+        this.manager.setIdentifier(managerId);
         EntityReference reference = new EntityReference("xwiki:Something", EntityType.PAGE);
         AverageRating expectedAverageRating = new DefaultAverageRating("average1")
             .setAverageVote(2.341f)
@@ -332,7 +334,7 @@ void removeRatingNotExisting() throws Exception
     {
         String ratingingId = "ratinging389";
         String managerId = "removeRating1";
-        this.manager.setIdentifer(managerId);
+        this.manager.setIdentifier(managerId);
         when(this.configuration.hasDedicatedCore()).thenReturn(false);
         when(this.solr.getClient(RatingSolrCoreInitializer.DEFAULT_RATINGS_SOLR_CORE)).thenReturn(this.solrClient);
 
@@ -353,7 +355,7 @@ void removeRatingExisting() throws Exception
     {
         String ratingingId = "ratinging429";
         String managerId = "removeRating2";
-        this.manager.setIdentifer(managerId);
+        this.manager.setIdentifier(managerId);
         when(this.configuration.hasDedicatedCore()).thenReturn(false);
         when(this.solr.getClient(RatingSolrCoreInitializer.DEFAULT_RATINGS_SOLR_CORE)).thenReturn(this.solrClient);
 
@@ -409,7 +411,7 @@ void removeRatingExisting() throws Exception
     void saveRatingOutScale()
     {
         when(this.configuration.getScaleUpperBound()).thenReturn(5);
-        this.manager.setIdentifer("saveRating1");
+        this.manager.setIdentifier("saveRating1");
         RatingsException exception = assertThrows(RatingsException.class, () -> {
             this.manager.saveRating(new EntityReference("test", EntityType.PAGE), mock(UserReference.class), -1);
         });
@@ -425,7 +427,7 @@ void saveRatingOutScale()
     void saveRatingZeroNotExisting() throws Exception
     {
         String managerId = "saveRating2";
-        this.manager.setIdentifer(managerId);
+        this.manager.setIdentifier(managerId);
         int scale = 10;
         when(this.configuration.getScaleUpperBound()).thenReturn(scale);
         EntityReference reference = mock(EntityReference.class);
@@ -505,7 +507,7 @@ void saveRatingZeroNotExisting() throws Exception
     void saveRatingExisting() throws Exception
     {
         String managerId = "saveRating3";
-        this.manager.setIdentifer(managerId);
+        this.manager.setIdentifier(managerId);
         int scale = 8;
         int newVote = 2;
         int oldVote = 3;
@@ -595,7 +597,7 @@ void saveRatingExisting() throws Exception
     void saveRatingExistingToZero() throws Exception
     {
         String managerId = "saveRating4";
-        this.manager.setIdentifer(managerId);
+        this.manager.setIdentifier(managerId);
         int scale = 8;
         int newVote = 0;
         int oldVote = 3;
@@ -751,7 +753,7 @@ void recomputeAverageRating() throws Exception
         when(inputReference.toString()).thenReturn("document:Input.Reference");
         String managerId = "myManager";
 
-        this.manager.setIdentifer(managerId);
+        this.manager.setIdentifier(managerId);
 
         String filterQuery = String.format("filter(%s:%s) AND filter(%s:%s)",
             RatingQueryField.ENTITY_REFERENCE.getFieldName(), "document\\:Input.Reference",
@@ -824,7 +826,7 @@ void recomputeAverageRating() throws Exception
     void removeRatings() throws Exception
     {
         String managerId = "myRatingManager";
-        this.manager.setIdentifer(managerId);
+        this.manager.setIdentifier(managerId);
         when(this.solr.getClient(RatingSolrCoreInitializer.DEFAULT_RATINGS_SOLR_CORE)).thenReturn(this.solrClient);
 
         EntityReference entityReference = mock(EntityReference.class);
@@ -848,4 +850,162 @@ void removeRatings() throws Exception
         verify(this.solrClient).commit();
         verify(this.averageRatingManager).removeAverageRatings(entityReference);
     }
+
+    @Test
+    void moveRatings() throws Exception
+    {
+        String managerId = "moveRatingsManagerId";
+        this.manager.setIdentifier(managerId);
+        when(this.solr.getClient(RatingSolrCoreInitializer.DEFAULT_RATINGS_SOLR_CORE)).thenReturn(this.solrClient);
+        
+        EntityReference oldReference = mock(EntityReference.class);
+        EntityReference newReference = mock(EntityReference.class);
+        when(oldReference.toString()).thenReturn("document:My.Old.Doc");
+
+        String filterQuery = String.format("filter(%s:%s) AND (filter(%s:%s) OR filter(%s:%s))",
+            RatingQueryField.MANAGER_ID.getFieldName(), managerId,
+            RatingQueryField.ENTITY_REFERENCE.getFieldName(), "document\\:My.Old.Doc",
+            RatingQueryField.PARENTS_REFERENCE.getFieldName(), "document\\:My.Old.Doc");
+
+        SolrQuery expectedQuery1 = new SolrQuery()
+            .addFilterQuery(filterQuery)
+            .setRows(100)
+            .setStart(0)
+            .setSort(RatingQueryField.CREATED_DATE.getFieldName(), SolrQuery.ORDER.asc);
+
+        SolrDocument rating1 = mock(SolrDocument.class);
+        SolrDocument rating2 = mock(SolrDocument.class);
+        SolrDocument rating3 = mock(SolrDocument.class);
+        SolrDocument rating4 = mock(SolrDocument.class);
+
+        // rating1 have the appropriate reference, but not the appropriate parent
+        when(rating1.get(RatingQueryField.IDENTIFIER.getFieldName())).thenReturn("rating1");
+        when(this.solrUtils.get(RatingQueryField.ENTITY_REFERENCE.getFieldName(), rating1, EntityReference.class))
+            .thenReturn(oldReference);
+        when(this.solrUtils.getCollection(RatingQueryField.PARENTS_REFERENCE.getFieldName(), rating1,
+            EntityReference.class))
+            .thenReturn(Collections.emptyList());
+
+        // rating2 have not the appropriate reference but the appropriate parent
+        when(rating2.get(RatingQueryField.IDENTIFIER.getFieldName())).thenReturn("rating2");
+        when(this.solrUtils.get(RatingQueryField.ENTITY_REFERENCE.getFieldName(), rating2, EntityReference.class))
+            .thenReturn(mock(EntityReference.class));
+        when(this.solrUtils.getCollection(RatingQueryField.PARENTS_REFERENCE.getFieldName(), rating2,
+            EntityReference.class))
+            .thenReturn(Collections.singletonList(oldReference));
+
+        // rating3 have the appropriate reference and also contain the appropriate parent
+        when(rating3.get(RatingQueryField.IDENTIFIER.getFieldName())).thenReturn("rating3");
+        when(this.solrUtils.get(RatingQueryField.ENTITY_REFERENCE.getFieldName(), rating3, EntityReference.class))
+            .thenReturn(oldReference);
+        when(this.solrUtils.getCollection(RatingQueryField.PARENTS_REFERENCE.getFieldName(), rating3,
+            EntityReference.class))
+            .thenReturn(Arrays.asList(mock(EntityReference.class), oldReference, mock(EntityReference.class)));
+
+        // rating4 only contain the appropriate parent
+        when(rating4.get(RatingQueryField.IDENTIFIER.getFieldName())).thenReturn("rating4");
+        when(this.solrUtils.get(RatingQueryField.ENTITY_REFERENCE.getFieldName(), rating4, EntityReference.class))
+            .thenReturn(mock(EntityReference.class));
+        when(this.solrUtils.getCollection(RatingQueryField.PARENTS_REFERENCE.getFieldName(), rating4,
+            EntityReference.class))
+            .thenReturn(Arrays.asList(mock(EntityReference.class), mock(EntityReference.class), oldReference));
+
+        when(this.documentList.iterator())
+            .thenReturn(Arrays.asList(rating1, rating2, rating3, rating4).iterator());
+
+        SolrQuery expectedQuery2 = new SolrQuery()
+            .addFilterQuery(filterQuery)
+            .setRows(100)
+            .setStart(100)
+            .setSort(RatingQueryField.CREATED_DATE.getFieldName(), SolrQuery.ORDER.asc);
+
+        QueryResponse response1 = mock(QueryResponse.class);
+        QueryResponse response2 = mock(QueryResponse.class);
+
+        AtomicInteger queryCounter = new AtomicInteger(0);
+        when(solrClient.query(any())).then(invocationOnMock -> {
+
+            SolrQuery givenQuery = invocationOnMock.getArgument(0);
+            QueryResponse result = null;
+            if (queryCounter.get() == 0) {
+                assertEquals(expectedQuery1.getQuery(), givenQuery.getQuery());
+                assertArrayEquals(expectedQuery1.getFilterQueries(), givenQuery.getFilterQueries());
+                assertEquals(expectedQuery1.getRows(), givenQuery.getRows());
+                assertEquals(expectedQuery1.getStart(), givenQuery.getStart());
+                assertEquals(expectedQuery1.getSorts(), givenQuery.getSorts());
+                result = response1;
+            } else if (queryCounter.get() == 1) {
+                assertEquals(expectedQuery2.getQuery(), givenQuery.getQuery());
+                assertArrayEquals(expectedQuery2.getFilterQueries(), givenQuery.getFilterQueries());
+                assertEquals(expectedQuery2.getRows(), givenQuery.getRows());
+                assertEquals(expectedQuery2.getStart(), givenQuery.getStart());
+                assertEquals(expectedQuery2.getSorts(), givenQuery.getSorts());
+                result = response2;
+            } else {
+                fail("Too many requests performed.");
+            }
+            queryCounter.getAndIncrement();
+            return result;
+        });
+        when(response1.getResults()).thenReturn(this.documentList);
+        when(response2.getResults()).thenReturn(new SolrDocumentList());
+
+        doAnswer(invocationOnMock -> {
+            String modifier = invocationOnMock.getArgument(0);
+            String fieldName = invocationOnMock.getArgument(1);
+            Object fieldValue = invocationOnMock.getArgument(2);
+            // we normally only invoke it with entity reference.
+            assertEquals(EntityReference.class, invocationOnMock.getArgument(3));
+            SolrInputDocument solrInputDocument = invocationOnMock.getArgument(4);
+            solrInputDocument.setField(fieldName, Collections.singletonMap(modifier, fieldValue));
+            return null;
+        }).when(this.solrUtils).setAtomic(any(), any(), any(), any(), any());
+
+        // expected committed documents
+        SolrInputDocument solrInputDocument1 = new SolrInputDocument();
+        solrInputDocument1.setField("id", "rating1");
+        solrInputDocument1.setField(RatingQueryField.ENTITY_REFERENCE.getFieldName(),
+            Collections.singletonMap(SolrUtils.ATOMIC_UPDATE_MODIFIER_SET, newReference));
+
+        SolrInputDocument solrInputDocument2 = new SolrInputDocument();
+        solrInputDocument2.setField("id", "rating2");
+        solrInputDocument2.setField(RatingQueryField.PARENTS_REFERENCE.getFieldName(),
+            Collections.singletonMap(SolrUtils.ATOMIC_UPDATE_MODIFIER_REMOVE, oldReference));
+        solrInputDocument2.setField(RatingQueryField.PARENTS_REFERENCE.getFieldName(),
+            Collections.singletonMap(SolrUtils.ATOMIC_UPDATE_MODIFIER_ADD, newReference));
+
+        SolrInputDocument solrInputDocument3 = new SolrInputDocument();
+        solrInputDocument3.setField("id", "rating3");
+        solrInputDocument3.setField(RatingQueryField.ENTITY_REFERENCE.getFieldName(),
+            Collections.singletonMap(SolrUtils.ATOMIC_UPDATE_MODIFIER_SET, newReference));
+        solrInputDocument3.setField(RatingQueryField.PARENTS_REFERENCE.getFieldName(),
+            Collections.singletonMap(SolrUtils.ATOMIC_UPDATE_MODIFIER_REMOVE, oldReference));
+        solrInputDocument3.setField(RatingQueryField.PARENTS_REFERENCE.getFieldName(),
+            Collections.singletonMap(SolrUtils.ATOMIC_UPDATE_MODIFIER_ADD, newReference));
+
+        SolrInputDocument solrInputDocument4 = new SolrInputDocument();
+        solrInputDocument4.setField("id", "rating4");
+        solrInputDocument4.setField(RatingQueryField.PARENTS_REFERENCE.getFieldName(),
+            Collections.singletonMap(SolrUtils.ATOMIC_UPDATE_MODIFIER_REMOVE, oldReference));
+        solrInputDocument4.setField(RatingQueryField.PARENTS_REFERENCE.getFieldName(),
+            Collections.singletonMap(SolrUtils.ATOMIC_UPDATE_MODIFIER_ADD, newReference));
+
+        List<SolrInputDocument> expectedAddDocuments = new ArrayList<>(Arrays.asList(
+            solrInputDocument1, solrInputDocument2, solrInputDocument3, solrInputDocument4));
+
+        when(this.solrClient.add(any(SolrInputDocument.class))).then(invocationOnMock -> {
+            SolrInputDocument solrInputDocument = invocationOnMock.getArgument(0);
+            SolrInputDocument expectedSolrInputDocument = expectedAddDocuments.remove(0);
+
+            // There's no proper equals method for SolrInputDocument, so we're comparing the toString
+            assertEquals(expectedSolrInputDocument.toString(), solrInputDocument.toString());
+            return null;
+        });
+
+        when(this.configuration.isAverageStored()).thenReturn(true);
+        this.manager.moveRatings(oldReference, newReference);
+        verify(this.solrClient, times(4)).add(any(SolrInputDocument.class));
+        verify(this.solrClient).commit();
+        verify(this.averageRatingManager).moveAverageRatings(oldReference, newReference);
+    }
 }
diff --git a/xwiki-platform-core/xwiki-platform-ratings/xwiki-platform-ratings-api/src/test/java/org/xwiki/ratings/internal/averagerating/AbstractAverageRatingManagerTest.java b/xwiki-platform-core/xwiki-platform-ratings/xwiki-platform-ratings-api/src/test/java/org/xwiki/ratings/internal/averagerating/AbstractAverageRatingManagerTest.java
index 6d1180db4fb..e0db08415c4 100644
--- a/xwiki-platform-core/xwiki-platform-ratings/xwiki-platform-ratings-api/src/test/java/org/xwiki/ratings/internal/averagerating/AbstractAverageRatingManagerTest.java
+++ b/xwiki-platform-core/xwiki-platform-ratings/xwiki-platform-ratings-api/src/test/java/org/xwiki/ratings/internal/averagerating/AbstractAverageRatingManagerTest.java
@@ -85,6 +85,13 @@ public long removeAverageRatings(EntityReference entityReference) throws Ratings
             return 0;
         }
 
+        @Override
+        public long moveAverageRatings(EntityReference oldReference, EntityReference newReference)
+            throws RatingsException
+        {
+            return 0;
+        }
+
         @Override
         protected ObservationManager getObservationManager()
         {
diff --git a/xwiki-platform-core/xwiki-platform-ratings/xwiki-platform-ratings-api/src/test/java/org/xwiki/ratings/internal/averagerating/SolrAverageRatingManagerTest.java b/xwiki-platform-core/xwiki-platform-ratings/xwiki-platform-ratings-api/src/test/java/org/xwiki/ratings/internal/averagerating/SolrAverageRatingManagerTest.java
index 0e1682a82ba..0674c9e9db0 100644
--- a/xwiki-platform-core/xwiki-platform-ratings/xwiki-platform-ratings-api/src/test/java/org/xwiki/ratings/internal/averagerating/SolrAverageRatingManagerTest.java
+++ b/xwiki-platform-core/xwiki-platform-ratings/xwiki-platform-ratings-api/src/test/java/org/xwiki/ratings/internal/averagerating/SolrAverageRatingManagerTest.java
@@ -20,9 +20,14 @@
 package org.xwiki.ratings.internal.averagerating;
 
 import java.lang.reflect.Type;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
 import java.util.Date;
 import java.util.HashMap;
+import java.util.List;
 import java.util.Map;
+import java.util.concurrent.atomic.AtomicInteger;
 
 import org.apache.solr.client.solrj.SolrClient;
 import org.apache.solr.client.solrj.SolrQuery;
@@ -38,6 +43,7 @@
 import org.xwiki.model.reference.EntityReferenceSerializer;
 import org.xwiki.ratings.AverageRating;
 import org.xwiki.ratings.RatingsManager;
+import org.xwiki.ratings.internal.RatingSolrCoreInitializer;
 import org.xwiki.search.solr.Solr;
 import org.xwiki.search.solr.SolrUtils;
 import org.xwiki.test.junit5.mockito.ComponentTest;
@@ -46,9 +52,11 @@
 
 import static org.junit.jupiter.api.Assertions.assertArrayEquals;
 import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.fail;
 import static org.mockito.ArgumentMatchers.any;
 import static org.mockito.Mockito.doAnswer;
 import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.times;
 import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.when;
 
@@ -284,4 +292,162 @@ void saveAverageRating() throws Exception
         verify(averageSolrClient).add(any(SolrInputDocument.class));
         verify(averageSolrClient).commit();
     }
+
+    @Test
+    void moveAverageRatings() throws Exception
+    {
+        String managerId = "moveRatingsManagerId";
+        when(this.ratingsManager.getIdentifier()).thenReturn(managerId);
+        when(this.solr.getClient(AverageRatingSolrCoreInitializer.DEFAULT_AVERAGE_RATING_SOLR_CORE))
+            .thenReturn(this.solrClient);
+
+        EntityReference oldReference = mock(EntityReference.class);
+        EntityReference newReference = mock(EntityReference.class);
+        when(oldReference.toString()).thenReturn("document:My.Old.Doc");
+
+        String filterQuery = String.format("filter(%s:%s) AND (filter(%s:%s) OR filter(%s:%s))",
+            AverageRatingQueryField.MANAGER_ID.getFieldName(), managerId,
+            AverageRatingQueryField.ENTITY_REFERENCE.getFieldName(), "document\\:My.Old.Doc",
+            AverageRatingQueryField.PARENTS.getFieldName(), "document\\:My.Old.Doc");
+
+        SolrQuery expectedQuery1 = new SolrQuery()
+            .addFilterQuery(filterQuery)
+            .setRows(100)
+            .setStart(0)
+            .setSort(AverageRatingQueryField.UPDATED_AT.getFieldName(), SolrQuery.ORDER.asc);
+
+        SolrDocument rating1 = mock(SolrDocument.class);
+        SolrDocument rating2 = mock(SolrDocument.class);
+        SolrDocument rating3 = mock(SolrDocument.class);
+        SolrDocument rating4 = mock(SolrDocument.class);
+
+        // rating1 have the appropriate reference, but not the appropriate parent
+        when(rating1.get("id")).thenReturn("rating1");
+        when(this.solrUtils.get(AverageRatingQueryField.ENTITY_REFERENCE.getFieldName(), rating1,
+            EntityReference.class))
+            .thenReturn(oldReference);
+        when(this.solrUtils.getCollection(AverageRatingQueryField.PARENTS.getFieldName(), rating1,
+            EntityReference.class))
+            .thenReturn(Collections.emptyList());
+
+        // rating2 have not the appropriate reference but the appropriate parent
+        when(rating2.get("id")).thenReturn("rating2");
+        when(this.solrUtils.get(AverageRatingQueryField.ENTITY_REFERENCE.getFieldName(), rating2, EntityReference.class))
+            .thenReturn(mock(EntityReference.class));
+        when(this.solrUtils.getCollection(AverageRatingQueryField.PARENTS.getFieldName(), rating2,
+            EntityReference.class))
+            .thenReturn(Collections.singletonList(oldReference));
+
+        // rating3 have the appropriate reference and also contain the appropriate parent
+        when(rating3.get("id")).thenReturn("rating3");
+        when(this.solrUtils.get(AverageRatingQueryField.ENTITY_REFERENCE.getFieldName(), rating3, EntityReference.class))
+            .thenReturn(oldReference);
+        when(this.solrUtils.getCollection(AverageRatingQueryField.PARENTS.getFieldName(), rating3,
+            EntityReference.class))
+            .thenReturn(Arrays.asList(mock(EntityReference.class), oldReference, mock(EntityReference.class)));
+
+        // rating4 only contain the appropriate parent
+        when(rating4.get("id")).thenReturn("rating4");
+        when(this.solrUtils.get(AverageRatingQueryField.ENTITY_REFERENCE.getFieldName(), rating4, EntityReference.class))
+            .thenReturn(mock(EntityReference.class));
+        when(this.solrUtils.getCollection(AverageRatingQueryField.PARENTS.getFieldName(), rating4,
+            EntityReference.class))
+            .thenReturn(Arrays.asList(mock(EntityReference.class), mock(EntityReference.class), oldReference));
+
+        when(this.documentList.iterator())
+            .thenReturn(Arrays.asList(rating1, rating2, rating3, rating4).iterator());
+
+        SolrQuery expectedQuery2 = new SolrQuery()
+            .addFilterQuery(filterQuery)
+            .setRows(100)
+            .setStart(100)
+            .setSort(AverageRatingQueryField.UPDATED_AT.getFieldName(), SolrQuery.ORDER.asc);
+
+        QueryResponse response1 = mock(QueryResponse.class);
+        QueryResponse response2 = mock(QueryResponse.class);
+
+        AtomicInteger queryCounter = new AtomicInteger(0);
+        when(solrClient.query(any())).then(invocationOnMock -> {
+
+            SolrQuery givenQuery = invocationOnMock.getArgument(0);
+            QueryResponse result = null;
+            if (queryCounter.get() == 0) {
+                assertEquals(expectedQuery1.getQuery(), givenQuery.getQuery());
+                assertArrayEquals(expectedQuery1.getFilterQueries(), givenQuery.getFilterQueries());
+                assertEquals(expectedQuery1.getRows(), givenQuery.getRows());
+                assertEquals(expectedQuery1.getStart(), givenQuery.getStart());
+                assertEquals(expectedQuery1.getSorts(), givenQuery.getSorts());
+                result = response1;
+            } else if (queryCounter.get() == 1) {
+                assertEquals(expectedQuery2.getQuery(), givenQuery.getQuery());
+                assertArrayEquals(expectedQuery2.getFilterQueries(), givenQuery.getFilterQueries());
+                assertEquals(expectedQuery2.getRows(), givenQuery.getRows());
+                assertEquals(expectedQuery2.getStart(), givenQuery.getStart());
+                assertEquals(expectedQuery2.getSorts(), givenQuery.getSorts());
+                result = response2;
+            } else {
+                fail("Too many requests performed.");
+            }
+            queryCounter.getAndIncrement();
+            return result;
+        });
+        when(response1.getResults()).thenReturn(this.documentList);
+        when(response2.getResults()).thenReturn(new SolrDocumentList());
+
+        doAnswer(invocationOnMock -> {
+            String modifier = invocationOnMock.getArgument(0);
+            String fieldName = invocationOnMock.getArgument(1);
+            Object fieldValue = invocationOnMock.getArgument(2);
+            // we normally only invoke it with entity reference.
+            assertEquals(EntityReference.class, invocationOnMock.getArgument(3));
+            SolrInputDocument solrInputDocument = invocationOnMock.getArgument(4);
+            solrInputDocument.setField(fieldName, Collections.singletonMap(modifier, fieldValue));
+            return null;
+        }).when(this.solrUtils).setAtomic(any(), any(), any(), any(), any());
+
+        // expected committed documents
+        SolrInputDocument solrInputDocument1 = new SolrInputDocument();
+        solrInputDocument1.setField("id", "rating1");
+        solrInputDocument1.setField(AverageRatingQueryField.ENTITY_REFERENCE.getFieldName(),
+            Collections.singletonMap(SolrUtils.ATOMIC_UPDATE_MODIFIER_SET, newReference));
+
+        SolrInputDocument solrInputDocument2 = new SolrInputDocument();
+        solrInputDocument2.setField("id", "rating2");
+        solrInputDocument2.setField(AverageRatingQueryField.PARENTS.getFieldName(),
+            Collections.singletonMap(SolrUtils.ATOMIC_UPDATE_MODIFIER_REMOVE, oldReference));
+        solrInputDocument2.setField(AverageRatingQueryField.PARENTS.getFieldName(),
+            Collections.singletonMap(SolrUtils.ATOMIC_UPDATE_MODIFIER_ADD, newReference));
+
+        SolrInputDocument solrInputDocument3 = new SolrInputDocument();
+        solrInputDocument3.setField("id", "rating3");
+        solrInputDocument3.setField(AverageRatingQueryField.ENTITY_REFERENCE.getFieldName(),
+            Collections.singletonMap(SolrUtils.ATOMIC_UPDATE_MODIFIER_SET, newReference));
+        solrInputDocument3.setField(AverageRatingQueryField.PARENTS.getFieldName(),
+            Collections.singletonMap(SolrUtils.ATOMIC_UPDATE_MODIFIER_REMOVE, oldReference));
+        solrInputDocument3.setField(AverageRatingQueryField.PARENTS.getFieldName(),
+            Collections.singletonMap(SolrUtils.ATOMIC_UPDATE_MODIFIER_ADD, newReference));
+
+        SolrInputDocument solrInputDocument4 = new SolrInputDocument();
+        solrInputDocument4.setField("id", "rating4");
+        solrInputDocument4.setField(AverageRatingQueryField.PARENTS.getFieldName(),
+            Collections.singletonMap(SolrUtils.ATOMIC_UPDATE_MODIFIER_REMOVE, oldReference));
+        solrInputDocument4.setField(AverageRatingQueryField.PARENTS.getFieldName(),
+            Collections.singletonMap(SolrUtils.ATOMIC_UPDATE_MODIFIER_ADD, newReference));
+
+        List<SolrInputDocument> expectedAddDocuments = new ArrayList<>(Arrays.asList(
+            solrInputDocument1, solrInputDocument2, solrInputDocument3, solrInputDocument4));
+
+        when(this.solrClient.add(any(SolrInputDocument.class))).then(invocationOnMock -> {
+            SolrInputDocument solrInputDocument = invocationOnMock.getArgument(0);
+            SolrInputDocument expectedSolrInputDocument = expectedAddDocuments.remove(0);
+
+            // There's no proper equals method for SolrInputDocument, so we're comparing the toString
+            assertEquals(expectedSolrInputDocument.toString(), solrInputDocument.toString());
+            return null;
+        });
+
+        this.averageRatingManager.moveAverageRatings(oldReference, newReference);
+        verify(this.solrClient, times(4)).add(any(SolrInputDocument.class));
+        verify(this.solrClient).commit();
+    }
 }
diff --git a/xwiki-platform-core/xwiki-platform-search/xwiki-platform-search-solr/xwiki-platform-search-solr-api/src/main/java/org/xwiki/search/solr/SolrUtils.java b/xwiki-platform-core/xwiki-platform-search/xwiki-platform-search-solr/xwiki-platform-search-solr-api/src/main/java/org/xwiki/search/solr/SolrUtils.java
index 012e968915f..1a67a04265e 100644
--- a/xwiki-platform-core/xwiki-platform-search/xwiki-platform-search-solr/xwiki-platform-search-solr-api/src/main/java/org/xwiki/search/solr/SolrUtils.java
+++ b/xwiki-platform-core/xwiki-platform-search/xwiki-platform-search-solr/xwiki-platform-search-solr-api/src/main/java/org/xwiki/search/solr/SolrUtils.java
@@ -162,6 +162,23 @@ public interface SolrUtils
      */
     void setAtomic(String modifier, String fieldName, Object fieldValue, SolrInputDocument document);
 
+    /**
+     * Store in the document the value associated with the passed field name.
+     *
+     * @param modifier the atomic update modifier to apply (set, add, add-distinct, remove, removeregex, inc)
+     * @param fieldName the name of the field in the document
+     * @param fieldValue the value to store in the {@link SolrDocument}
+     * @param valueType the type to use as reference to serialize the value
+     * @param document the Solr document
+     * @since 12.10
+     */
+    @Unstable
+    default void setAtomic(String modifier, String fieldName, Object fieldValue, Type valueType,
+        SolrInputDocument document)
+    {
+        setAtomic(modifier, fieldName, fieldValue, document);
+    }
+
     /**
      * Store in the document the values associated with the passed field name.
      * <p>
@@ -173,6 +190,8 @@ public interface SolrUtils
      */
     void set(String fieldName, Collection<?> fieldValue, SolrInputDocument document);
 
+
+
     /**
      * Store in the document the value associated with the passed field name.
      * <p>
diff --git a/xwiki-platform-core/xwiki-platform-search/xwiki-platform-search-solr/xwiki-platform-search-solr-api/src/main/java/org/xwiki/search/solr/internal/DefaultSolrUtils.java b/xwiki-platform-core/xwiki-platform-search/xwiki-platform-search-solr/xwiki-platform-search-solr-api/src/main/java/org/xwiki/search/solr/internal/DefaultSolrUtils.java
index 87c44d046a5..6223577bd30 100644
--- a/xwiki-platform-core/xwiki-platform-search/xwiki-platform-search-solr/xwiki-platform-search-solr-api/src/main/java/org/xwiki/search/solr/internal/DefaultSolrUtils.java
+++ b/xwiki-platform-core/xwiki-platform-search/xwiki-platform-search-solr/xwiki-platform-search-solr-api/src/main/java/org/xwiki/search/solr/internal/DefaultSolrUtils.java
@@ -367,6 +367,13 @@ public void setAtomic(String modifier, String fieldName, Object fieldValue, Solr
         document.setField(fieldName, Collections.singletonMap(modifier, fieldValue));
     }
 
+    @Override
+    public void setAtomic(String modifier, String fieldName, Object fieldValue, Type valueType,
+        SolrInputDocument document)
+    {
+        document.setField(fieldName, Collections.singletonMap(modifier, toString(fieldValue, valueType)));
+    }
+
     @Override
     public void setString(String fieldName, Object fieldValue, SolrInputDocument document)
     {
-- 
GitLab