[ACS-12256] Backport search_after deep pagination to 25.N (#4325)

This commit is contained in:
tathagta15
2026-08-21 13:00:48 +05:30
committed by GitHub
parent b431d65eab
commit dc543fa0ab
12 changed files with 145 additions and 20 deletions
@@ -44,7 +44,7 @@ import org.alfresco.service.cmr.repository.StoreRef;
import org.alfresco.service.namespace.NamespaceService;
/**
* This class provides parameters to define a search. TODO - paging of results page number and page size - paging isolation - REPEATABLE READ, READ COMMITTED, may SEE ONCE tracking node refs in previous result sets - how long repeatable read may be held - limit by the number of permission evaluations
* This class provides parameters to define a search.
*
* @author Andy Hind
*/
@@ -198,6 +198,8 @@ public class SearchParameters implements BasicSearchParameters
private boolean trackScore = true;
private String searchAfterToken;
/**
* Default constructor
*/
@@ -250,6 +252,7 @@ public class SearchParameters implements BasicSearchParameters
sp.timezone = this.timezone;
sp.trackTotalHits = this.trackTotalHits;
sp.trackScore = this.trackScore;
sp.searchAfterToken = this.searchAfterToken;
return sp;
}
@@ -600,6 +603,16 @@ public class SearchParameters implements BasicSearchParameters
this.limit = limit;
}
public String getSearchAfterToken()
{
return searchAfterToken;
}
public void setSearchAfterToken(String searchAfterToken)
{
this.searchAfterToken = searchAfterToken;
}
/**
* The way in which multilingual fields are treated durig a search. By default, only the specified locale is used and it must be an exact match.
*
@@ -1197,6 +1210,7 @@ public class SearchParameters implements BasicSearchParameters
result = prime * result + ((ranges == null) ? 0 : ranges.hashCode());
result = prime * result + ((searchTerm == null) ? 0 : searchTerm.hashCode());
result = prime * result + (spellCheck ? 1231 : 1237);
result = prime * result + ((searchAfterToken == null) ? 0 : searchAfterToken.hashCode());
return result;
}
@@ -1359,6 +1373,17 @@ public class SearchParameters implements BasicSearchParameters
return false;
if (spellCheck != other.spellCheck)
return false;
if (searchAfterToken == null)
{
if (other.searchAfterToken != null)
{
return false;
}
}
else if (!searchAfterToken.equals(other.searchAfterToken))
{
return false;
}
return true;
}
@@ -1401,7 +1426,8 @@ public class SearchParameters implements BasicSearchParameters
.append(", interval=").append(this.interval)
.append(", range=").append(this.ranges)
.append(", timezone=").append(this.timezone)
.append(", spellCheck=").append(this.spellCheck).append("]");
.append(", spellCheck=").append(this.spellCheck)
.append(", searchAfterToken=").append(this.searchAfterToken).append("]");
return builder.toString();
}
@@ -85,7 +85,7 @@ public class SearchApiWebscript extends AbstractWebScript implements RecognizedP
{
try
{
// Turn JSON into a Java object respresentation
// Turn JSON into a Java object representation
SearchQuery searchQuery = extractJsonContent(webScriptRequest, assistant.getJsonHelper(), SearchQuery.class);
// Parse the parameters
@@ -231,7 +231,11 @@ public class ResultMapper
.map(resultSet -> toSearchContext(resultSet, searchRequestContext, searchQuery))
.orElse(null);
return CollectionWithPagingInfo.asPaged(params.getPaging(), noderesults, results.hasMore(), setTotal(results), null, context);
String nextSearchAfterToken = toSearchEngineResultSet(results)
.map(SearchEngineResultSet::getNextSearchAfterToken)
.orElse(null);
return CollectionWithPagingInfo.asPaged(params.getPaging(), noderesults, results.hasMore(), setTotal(results), null, context, nextSearchAfterToken);
}
/**
@@ -199,6 +199,10 @@ public class SearchMapper
sp.setLimitBy(LimitBy.FINAL_SIZE);
sp.setLimit(paging.getMaxItems());
sp.setSkipCount(paging.getSkipCount());
if (paging.getSearchAfterToken() != null)
{
sp.setSearchAfterToken(paging.getSearchAfterToken());
}
}
}
@@ -103,6 +103,10 @@ public class SerializerOfCollectionWithPaging extends StdSerializer<Serializable
jgen.writeNumberField(RecognizedParamsExtractor.PARAM_PAGING_SKIP, pagedCol.getPaging().getSkipCount());
jgen.writeNumberField(RecognizedParamsExtractor.PARAM_PAGING_MAX, pagedCol.getPaging().getMaxItems());
}
if (pagedCol.getNextSearchAfterToken() != null)
{
jgen.writeStringField("nextSearchAfterToken", pagedCol.getNextSearchAfterToken());
}
jgen.writeEndObject();
}
}
@@ -2,7 +2,7 @@
* #%L
* Alfresco Remote API
* %%
* Copyright (C) 2005 - 2016 Alfresco Software Limited
* Copyright (C) 2005 - 2026 Alfresco Software Limited
* %%
* This file is part of the Alfresco software.
* If the software was purchased under a paid Alfresco license, the terms of
@@ -70,4 +70,12 @@ public interface SerializablePagedCollection<T>
* The search context for the collection
*/
SearchContext getContext();
/**
* The search_after token for fetching the next page, or null if not applicable.
*/
default String getNextSearchAfterToken()
{
return null;
}
}
@@ -50,6 +50,7 @@ public class CollectionWithPagingInfo<T> implements SerializablePagedCollection<
private final Paging paging;
private final Object sourceEntity;
private final SearchContext context;
private final String nextSearchAfterToken;
/**
* Constructs a new CollectionWithPagingInfo.
@@ -64,6 +65,11 @@ public class CollectionWithPagingInfo<T> implements SerializablePagedCollection<
* - The total number of items available.
*/
protected CollectionWithPagingInfo(Collection<T> collection, Paging paging, boolean hasMoreItems, Integer totalItems, Object sourceEntity, SearchContext context)
{
this(collection, paging, hasMoreItems, totalItems, sourceEntity, context, null);
}
protected CollectionWithPagingInfo(Collection<T> collection, Paging paging, boolean hasMoreItems, Integer totalItems, Object sourceEntity, SearchContext context, String nextSearchAfterToken)
{
super();
this.hasMoreItems = hasMoreItems;
@@ -81,6 +87,7 @@ public class CollectionWithPagingInfo<T> implements SerializablePagedCollection<
}
this.sourceEntity = sourceEntity;
this.context = context;
this.nextSearchAfterToken = nextSearchAfterToken;
}
/**
@@ -93,7 +100,7 @@ public class CollectionWithPagingInfo<T> implements SerializablePagedCollection<
public static <T> CollectionWithPagingInfo<T> from(SerializablePagedCollection<T> pagedCollection)
{
return new CollectionWithPagingInfo<>(pagedCollection.getCollection(), pagedCollection.getPaging(), pagedCollection.hasMoreItems(), pagedCollection.getTotalItems(),
pagedCollection.getSourceEntity(), pagedCollection.getContext());
pagedCollection.getSourceEntity(), pagedCollection.getContext(), pagedCollection.getNextSearchAfterToken());
}
/**
@@ -143,7 +150,7 @@ public class CollectionWithPagingInfo<T> implements SerializablePagedCollection<
}
/**
* Constructs a new CollectionWithPagingInfo. Not for public use.
* Constructs a new CollectionWithPagingInfo.
*
* @param paging
* - Paging request info
@@ -163,7 +170,7 @@ public class CollectionWithPagingInfo<T> implements SerializablePagedCollection<
}
/**
* Constructs a new CollectionWithPagingInfo. Not for public use.
* Constructs a new CollectionWithPagingInfo.
*
* @param paging
* - Paging request info
@@ -184,6 +191,30 @@ public class CollectionWithPagingInfo<T> implements SerializablePagedCollection<
return new CollectionWithPagingInfo<T>(aCollection, paging, hasMoreItems, totalItems, sourceEntity, context);
}
/**
* Constructs a new CollectionWithPagingInfo carrying a search_after token.
*
* @param paging
* - Paging request info
* @param aCollection
* - the collection that needs to be paged.
* @param hasMoreItems
* - Are there more items after this Collection?
* @param totalItems
* - The total number of items available.
* @param sourceEntity
* - The parent/source entity responsible for the collection
* @param context
* - The search context
* @param nextSearchAfterToken
* - The search_after token for fetching the next page
* @return CollectionWithPagingInfo
*/
public static <T> CollectionWithPagingInfo<T> asPaged(Paging paging, Collection<T> aCollection, boolean hasMoreItems, Integer totalItems, Object sourceEntity, SearchContext context, String nextSearchAfterToken)
{
return new CollectionWithPagingInfo<>(aCollection, paging, hasMoreItems, totalItems, sourceEntity, context, nextSearchAfterToken);
}
/**
* Returns the Collection object
*
@@ -240,4 +271,10 @@ public class CollectionWithPagingInfo<T> implements SerializablePagedCollection<
return context;
}
@Override
public String getNextSearchAfterToken()
{
return nextSearchAfterToken;
}
}
@@ -27,6 +27,8 @@ package org.alfresco.rest.framework.resource.parameters;
import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
import com.fasterxml.jackson.databind.deser.std.StringDeserializer;
import org.alfresco.rest.framework.core.exceptions.InvalidArgumentException;
@@ -46,8 +48,9 @@ public class Paging
private final int skipCount;
private final int maxItems;
private final String searchAfterToken;
private Paging(int skipCount, int maxItems)
private Paging(int skipCount, int maxItems, String searchAfterToken)
{
super();
if (skipCount < 0)
@@ -60,6 +63,7 @@ public class Paging
}
this.skipCount = skipCount;
this.maxItems = maxItems;
this.searchAfterToken = searchAfterToken;
}
/**
@@ -82,21 +86,39 @@ public class Paging
return this.maxItems;
}
@JsonCreator
public static Paging valueOf(@JsonProperty("skipCount") int skipCount, @JsonProperty("maxItems") int maxItems)
/**
* The opaque search_after cursor for the next page, or null if not using cursor-based paging.
*
* @return String
*/
public String getSearchAfterToken()
{
return new Paging(skipCount, maxItems);
return this.searchAfterToken;
}
public static Paging valueOf(int skipCount, int maxItems)
{
return new Paging(skipCount, maxItems, null);
}
@JsonCreator
public static Paging valueOf(@JsonProperty("skipCount") int skipCount, @JsonProperty("maxItems") int maxItems,
@JsonProperty("searchAfterToken") @JsonDeserialize(using = StringDeserializer.class) String searchAfterToken)
{
return new Paging(skipCount, maxItems, searchAfterToken);
}
@Override
public String toString()
{
StringBuilder builder = new StringBuilder();
builder.append("Paging [skipCount=");
builder.append(this.skipCount);
builder.append(", maxItems=");
builder.append(this.maxItems);
builder.append("]");
builder.append("Paging [skipCount=")
.append(this.skipCount)
.append(", maxItems=")
.append(this.maxItems)
.append(", searchAfterToken=")
.append(this.searchAfterToken)
.append("]");
return builder.toString();
}
@@ -153,7 +153,7 @@ public class ResourceWebScriptHelper
}
}
return CollectionWithPagingInfo.asPaged(collectionToWrap.getPaging(), resultCollection, collectionToWrap.hasMoreItems(),
collectionToWrap.getTotalItems(), sourceEntity, collectionToWrap.getContext());
collectionToWrap.getTotalItems(), sourceEntity, collectionToWrap.getContext(), collectionToWrap.getNextSearchAfterToken());
}
else
{
@@ -1265,7 +1265,7 @@
<property name="resultMapper" ref="searchapiResultMapper" />
<property name="searchMapper" ref="searchapiSearchMapper" />
</bean>
<bean id="webscript.org.alfresco.api.SearchSQLApiWebscript.post"
class="org.alfresco.rest.api.search.SearchSQLApiWebscript" parent="webscript">
<property name="serviceRegistry" ref="ServiceRegistry" />
@@ -194,6 +194,21 @@ public class SearchMapperTests
assertEquals(searchParameters.getSkipCount(), paging.getSkipCount());
}
@Test
public void fromSearchAfter() throws Exception
{
SearchParameters searchParameters = new SearchParameters();
searchMapper.fromPaging(searchParameters, Paging.valueOf(0, 100));
assertNull(searchParameters.getSearchAfterToken());
searchMapper.fromPaging(searchParameters, Paging.valueOf(0, 100, "SEARCH_AFTER_TOKEN"));
assertEquals("SEARCH_AFTER_TOKEN", searchParameters.getSearchAfterToken());
// An explicit empty searchAfter starts a new cursor-paging session (first page).
searchMapper.fromPaging(searchParameters, Paging.valueOf(0, 100, ""));
assertEquals("", searchParameters.getSearchAfterToken());
}
@Test
public void fromSort() throws Exception
{
@@ -2,7 +2,7 @@
* #%L
* Alfresco Data model classes
* %%
* Copyright (C) 2005 - 2021 Alfresco Software Limited
* Copyright (C) 2005 - 2026 Alfresco Software Limited
* %%
* This file is part of the Alfresco software.
* If the software was purchased under a paid Alfresco license, the terms of
@@ -52,4 +52,9 @@ public interface SearchEngineResultSet extends ResultSet, SearchEngineResultMeta
long getLastIndexedTxId();
boolean getProcessedDenies();
default String getNextSearchAfterToken()
{
return null;
}
}