Paginación del contenido
La función de paginación permite dividir grandes cantidades de datos en partes manejables.
Información general
La API de contenido de Rapid permite consumir grandes cantidades de datos de los alojamientos. Debido al gran tamaño de estos datos, Content API admite la paginación para dividirlos en partes manejables. En este documento se dan algunos ejemplos y prácticas recomendadas para usar la función de paginación.
Ejemplo básico
El proceso de paginación comienza con la búsqueda de alojamientos y la obtención de más resultados de los que caben en una página. Cuando esto sucede, la respuesta contendrá la primera página de resultados y, luego, un encabezado de respuesta Link
que se puede seguir para pasar a la página siguiente.
Ejemplo de solicitud:
https://api.ean.com/v3/properties/content?language=en-US&supply_source=expedia
Ejemplo de encabezado de respuesta:
Link: <https://api.ean.com/v3/properties/content?token=WVZTCUNRUQ4SUSNHAXIWFk0VRQ5JZhFWExRaXAgRVnpDA1RWTUkBB10FHFYGQwZyHwBNFA1HBhIMC1IGAUsGBhkHBGcHBBUGdlMHQAd1UA8WBwwMB1NcBAhdahBWUAdRXjtfVwpEBiEdASBHREpdEVwUQRxuRVgRWg1UaVkHS1kcA3IWBXEVAFZMAz1VBVRWXT5KRQNKFQVEACMXASJFVlRBVzoTRQZVQQRVOUdHVUAVDRRXIBNXJxdYAwtWQFJeVgpHAiYTCwoEWhRmZ0MHCxwFJhNbUEcGU1tCHW1dAWwAGlEIEAFVXEYNIRQBIRcTSltIAUVHTTxdAghAU3VDDSFCVkYCXFE8XgIMQwF7QAAlFwVZEVJpTUdWBBcHU2cBXgEKRgFwFVdxR1tWQQtHQhhuUFgAAA5WE1oKH0JcBEZVDGdVBBdVQl4BVQgFVRIVEFwWBBdHS2xKBU1RDANvDFFfX0cNekZTcxJeE1gQW24XDw8RDEdTIUBTJhFTAxZXb1lUAVNRa1ZZAFxHAXQVUHxDVxdDUAxcFRVmVFpQBlRbFFNxEAwgRXcMXAdfFUZbBFQAXFQGV1YCAVI=>; rel="next"; expires=2023-06-01T17:13:19.699379618Z
Si se sigue el enlace provisto antes de que expire, se devolverá la siguiente página de resultados junto con un nuevo encabezado Link
para la página que viene tras esta. Para pasar por todas las páginas de la respuesta, simplemente hay que seguir cada uno de los encabezados Link
que se devuelven, hasta que no se devuelvan más encabezados Link
. Esto señala el final del conjunto de datos solicitado.
Filtrado de los datos solicitados
Aunque en el sencillo ejemplo anterior se muestra cómo funciona la paginación, hay que tener en cuenta que se trata de una búsqueda enorme. Cuando hay muchos alojamientos, puede llevar algún tiempo pasar por todas las páginas. Es útil incluir parámetros de consulta adicionales para buscar solo los alojamientos que se necesitan.
Por ejemplo, en lugar de buscar todos los alojamientos, tal vez solo se necesite consultar alojamientos en Estados Unidos. Para solicitar este subconjunto de alojamientos, se puede cambiar la solicitud de modo que incluya un parámetro de consulta mediante el objeto country_code
.
Solicitud de ejemplo con parámetro de país:
https://api.ean.com/v3/properties/content?language=en-US&supply_source=expedia&country_code=US
Se seguirán ofreciendo las mismas funciones de paginación que se mencionaron anteriormente, pero habrá menos páginas de alojamientos por las que pasar.
Otra forma de reducir la cantidad de alojamientos que se solicitan consiste en obtener solo los alojamientos que han cambiado desde la última vez que se extrajeron los datos de alojamientos. Si se usa el objeto date_updated_start
, se devuelven solo los alojamientos que han cambiado desde la fecha indicada.
Solicitud de ejemplo con parámetros de país y fecha:
https://api.ean.com/v3/properties/content?language=en-US&supply_source=expedia&country_code=US&date_updated_start=2023-01-02
Para mejorar la velocidad de paginación y reducir la cantidad de datos transferidos, es fundamental asegurarse de que se solicitan solo los alojamientos que se necesitan.
División de búsquedas para la paralelización
A veces, incluso cuando se solicitan solo los alojamientos que se necesitan, el resultado sigue siendo grande. En este caso, es útil realizar varias búsquedas en paralelo para ayudar a acelerar el proceso.
El primer paso consiste en dividir la búsqueda deseada en búsquedas más pequeñas. Esto será diferente para cada caso de uso, pero se puede comenzar con la búsqueda deseada y, luego, añadir más parámetros de consulta a la búsqueda que no se superpongan entre sí.
Por ejemplo, si la búsqueda deseada es para todos los alojamientos en Estados Unidos, debe empezarse filtrando por país, como en el ejemplo anterior.
https://api.ean.com/v3/properties/content?language=en-US&supply_source=expedia&country_code=US
Esta búsqueda se puede dividir aún más si se utilizan, por ejemplo, los objetos property_rating_min
y property_rating_max
.
https://api.ean.com/v3/properties/content?language=en-US&supply_source=expedia&country_code=US&property_rating_min=0.0&property_rating_max=0.9
https://api.ean.com/v3/properties/content?language=en-US&supply_source=expedia&country_code=US&property_rating_min=1.0&property_rating_max=1.9
https://api.ean.com/v3/properties/content?language=en-US&supply_source=expedia&country_code=US&property_rating_min=2.0&property_rating_max=2.9
https://api.ean.com/v3/properties/content?language=en-US&supply_source=expedia&country_code=US&property_rating_min=3.0&property_rating_max=3.9
https://api.ean.com/v3/properties/content?language=en-US&supply_source=expedia&country_code=US&property_rating_min=4.0&property_rating_max=4.9
https://api.ean.com/v3/properties/content?language=en-US&supply_source=expedia&country_code=US&property_rating_min=5.0
Ahora hay seis solicitudes separadas por cuyas páginas se puede pasar de forma independiente y en paralelo. Como resultado se recupera el mismo conjunto de datos, pero más rápido.
Cada situación será diferente, pero si se comienza con la búsqueda deseada y se consulta el encabezado de respuesta pagination-total-results
en la primera página de respuestas, se tendrá un indicador de si sería útil dividir la búsqueda.
Ejemplo de código
Si bien la información anterior describe conceptualmente el proceso de paginación y cómo se dividen los datos, a continuación se muestra un ejemplo de código Java más concreto.
Nota: En el ejemplo de código siguiente no se incluyen la gestión adecuada de excepciones ni otras prácticas recomendadas. Como siempre, es necesario seguir todas las prácticas recomendadas cuando se escribe código listo para producción.
Para comenzar, se puede usar una clase RapidClient
simple como base para realizar llamadas de Rapid.
public class RapidClient {
// Base URL
private static final String RAPID_BASE_URL = "https://api.ean.com";
// Headers
private static final String GZIP = "gzip";
private static final String AUTHORIZATION_HEADER = "EAN APIKey={0},Signature={1},timestamp={2}";
// HTTP Client
private static final Client CLIENT = ClientBuilder.newClient().register(GZipEncoder.class);
private final String apiKey;
private final String sharedSecret;
public RapidClient(String apikey, String sharedSecret) {
this.apiKey = apikey;
this.sharedSecret = sharedSecret;
}
public Response get(String path, MultivaluedMap<String, String> queryParameters) {
WebTarget webTarget = CLIENT.target(RAPID_BASE_URL).path(path);
// Add all query parameters from the map to the web target
for (Map.Entry<String, List<String>> entry : queryParameters.entrySet()) {
for (String value : entry.getValue()) {
webTarget = webTarget.queryParam(entry.getKey(), value);
}
}
return webTarget.request(MediaType.APPLICATION_JSON_TYPE)
.header(HttpHeaders.ACCEPT_ENCODING, GZIP)
.header(HttpHeaders.AUTHORIZATION, generateAuthHeader())
.get();
}
private String generateAuthHeader() {
final String timeStampInSeconds = String.valueOf(ZonedDateTime.now(ZoneOffset.UTC).toEpochSecond());
final String input = apiKey + sharedSecret + timeStampInSeconds;
final String signature = DigestUtils.sha512Hex(input);
return MessageFormat.format(AUTHORIZATION_HEADER, apiKey, signature, timeStampInSeconds);
}
}
Este es simplemente un código estándar que facilitará la lectura de las clases siguientes.
La clase siguiente representará una llamada a la API de contenido específica y usará RapidClient
para realizar esa llamada.
public class PropertyContentCall {
// Path
private static final String PROPERTY_CONTENT_PATH = "v3/properties/content";
// Headers
private static final String LINK = "Link";
private static final String PAGINATION_TOTAL_RESULTS = "Pagination-Total-Results";
// Query parameters keys
private static final String LANGUAGE = "language";
private static final String SUPPLY_SOURCE = "supply_source";
private static final String COUNTRY_CODE = "country_code";
private static final String CATEGORY_ID = "category_id";
private static final String TOKEN = "token";
private static final String INCLUDE = "include";
// Call parameters
private final RapidClient client;
private final String language;
private final String supplySource;
private final List<String> countryCodes;
private final List<String> categoryIds;
private String token;
public PropertyContentCall(RapidClient client, String language, String supplySource,
List<String> countryCodes, List<String> categoryIds) {
this.client = client;
this.language = language;
this.supplySource = supplySource;
this.countryCodes = countryCodes;
this.categoryIds = categoryIds;
}
public Stream<RapidPropertyContent> stream() {
return Stream.generate(() -> {
synchronized (this) {
// Make the call to Rapid.
final Response response = client.get(PROPERTY_CONTENT_PATH, queryParameters());
// Read the response to return.
final Map<String, RapidPropertyContent> propertyContents = response.readEntity(new GenericType<>() { });
// Store the token for pagination if we got one.
token = getTokenFromLink(response.getHeaderString(LINK));
return propertyContents;
}
})
.takeWhile(MapUtils::isNotEmpty)
.map(Map::values)
.flatMap(Collection::stream);
}
public Integer size() {
// Make the call to Rapid.
final MultivaluedMap<String, String> queryParameters = queryParameters();
queryParameters.putSingle(INCLUDE, "property_ids");
final Response response = client.get(PROPERTY_CONTENT_PATH, queryParameters);
// Read the size to return.
final Integer size = Integer.parseInt(response.getHeaderString(PAGINATION_TOTAL_RESULTS));
// Close the response since we're not reading it.
response.close();
return size;
}
private MultivaluedMap<String, String> queryParameters() {
final MultivaluedMap<String, String> queryParams = new MultivaluedHashMap<>();
if (token != null) {
queryParams.putSingle(TOKEN, token);
} else {
// Add required parameters
queryParams.putSingle(LANGUAGE, language);
queryParams.putSingle(SUPPLY_SOURCE, supplySource);
// Add optional parameters
if (CollectionUtils.isNotEmpty(countryCodes)) {
queryParams.put(COUNTRY_CODE, countryCodes);
}
if (CollectionUtils.isNotEmpty(categoryIds)) {
queryParams.put(CATEGORY_ID, categoryIds);
}
}
return queryParams;
}
private String getTokenFromLink(String linkHeader) {
if (StringUtils.isEmpty(linkHeader)) {
return null;
}
final int startOfToken = linkHeader.indexOf("=") + 1;
final int endOfToken = linkHeader.indexOf(">");
return linkHeader.substring(startOfToken, endOfToken);
}
}
PropertyContentCall
representa una sola solicitud a la API de contenido de Rapid y encapsula el proceso de pasar por las páginas de esa llamada hasta su finalización.
Ejemplo:
Compara la siguiente llamada a la API con la solicitud de Java equivalente.
https://api.ean.com/v3/properties/content?language=en-US&supply_source=expedia&country_code=US
PropertyContentCall request = new PropertyContentCall(myRapidClient, "en-US", "expedia", List.of("US"), null);
- El uso de
PropertyContentCall
que se hace aquí es muy específico de este ejemplo. Las llamadas se dividirán porcountry_code
ycategory_id
, aunque esto se puede cambiar según el caso de uso. Dado que esto se está escribiendo específicamente para la paralelización, en este ejemplo se utilizarán flujos paralelos de Java. El métodostream()
público existe para devolver un flujo de objetosRapidPropertyContent
. Un objetoRapidPropertyContent
es simplemente un POJO que representa un solo alojamiento obtenido mediante la llamada a la API de contenido de Rapid. Aunque aquí se utilizan flujos paralelos de Java, basta con usar cualquier forma de ejecutar código en paralelo. - Cuando el código que llama a
stream()
necesita leer otro alojamiento fuera del flujo, este método proporcionará ese alojamiento si ya lo ha recuperado, o llamará a la API de contenido de Rapid para obtener la siguiente página de resultados y devolverá un alojamiento contenido en ella. Con tan solo llamar astream()
y leerlo hasta completarlo, se gestionará el paso por las páginas con todos los alojamientos devueltos mediante la solicitud. - Hay otro método auxiliar público
size()
que proporciona una forma de ver fácilmente el número total de alojamientos que se devolverá mediantePropertyContentCall
. Esto puede ayudar a determinar si una llamada es lo suficientemente pequeña, o si necesita dividirse en llamadas aún más pequeñas para la paralelización.
Los elementos básicos anteriores proporcionan una base para llamar a Rapid y pasar por las páginas de la respuesta. El siguiente código utiliza las clases anteriores para dividir automáticamente una llamada en partes manejables, pasar por las páginas de todas las llamadas más pequeñas en paralelo y escribir la salida combinada en un archivo.
public class ParallelFileMaker {
private static final String APIKEY = System.getenv().get("RAPID_APIKEY");
private static final String SHARED_SECRET = System.getenv().get("RAPID_SHARED_SECRET");
private static final List<String> COUNTRIES = Arrays.asList("AD", "AE", "AF", "AG", "AI", "AL", "AM", "AO", "AQ",
"AR", "AS", "AT", "AU", "AW", "AX", "AZ", "BA", "BB", "BD", "BE", "BF", "BG", "BH", "BI", "BJ", "BL", "BM",
"BN", "BO", "BQ", "BR", "BS", "BT", "BV", "BW", "BY", "BZ", "CA", "CC", "CD", "CF", "CG", "CH", "CI", "CK",
"CL", "CM", "CN", "CO", "CR", "CU", "CV", "CW", "CX", "CY", "CZ", "DE", "DJ", "DK", "DM", "DO", "DZ", "EC",
"EE", "EG", "EH", "ER", "ES", "ET", "FI", "FJ", "FK", "FM", "FO", "FR", "GA", "GB", "GD", "GE", "GF", "GG",
"GH", "GI", "GL", "GM", "GN", "GP", "GQ", "GR", "GS", "GT", "GU", "GW", "GY", "HK", "HM", "HN", "HR", "HT",
"HU", "ID", "IE", "IL", "IM", "IN", "IO", "IQ", "IR", "IS", "IT", "JE", "JM", "JO", "JP", "KE", "KG", "KH",
"KI", "KM", "KN", "KP", "KR", "KW", "KY", "KZ", "LA", "LB", "LC", "LI", "LK", "LR", "LS", "LT", "LU", "LV",
"LY", "MA", "MC", "MD", "ME", "MF", "MG", "MH", "MK", "ML", "MM", "MN", "MO", "MP", "MQ", "MR", "MS", "MT",
"MU", "MV", "MW", "MX", "MY", "MZ", "NA", "NC", "NE", "NF", "NG", "NI", "NL", "NO", "NP", "NR", "NU", "NZ",
"OM", "PA", "PE", "PF", "PG", "PH", "PK", "PL", "PM", "PN", "PR", "PS", "PT", "PW", "PY", "QA", "RE", "RO",
"RS", "RU", "RW", "SA", "SB", "SC", "SD", "SE", "SG", "SH", "SI", "SJ", "SK", "SL", "SM", "SN", "SO", "SR",
"SS", "ST", "SV", "SX", "SY", "SZ", "TC", "TD", "TF", "TG", "TH", "TJ", "TK", "TL", "TM", "TN", "TO", "TR",
"TT", "TV", "TW", "TZ", "UA", "UG", "UM", "US", "UY", "UZ", "VA", "VC", "VE", "VG", "VI", "VN", "VU", "WF",
"WS", "YE", "YT", "ZA", "ZM", "ZW");
private static final List<String> PROPERTY_CATEGORIES = Arrays.asList("0", "1", "2", "3", "4", "5", "6", "7", "8",
"9", "10", "11", "12", "13", "14", "15", "16", "17", "18", "20", "21", "22", "23", "24", "25", "26",
"29", "30", "31", "32", "33", "34", "36", "37", "39", "40", "41", "42", "43", "44");
private static final int MAX_CALL_SIZE = 20_000;
private static final String LANGUAGE = "en-US";
private static final String SUPPLY_SOURCE = "expedia";
private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper()
.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false)
.registerModule(new JavaTimeModule());
private static final RapidClient RAPID_CLIENT = new RapidClient(APIKEY, SHARED_SECRET);
public void run() throws IOException {
final Map<PropertyContentCall, Integer> allCalls = divideUpCalls();
// Make sure we're making the calls in the most efficient order. This list will be smallest to largest, so
// that when the streams get combined and are reversed, the largest stream will be first.
final List<Stream<RapidPropertyContent>> callsToMake = allCalls.entrySet().stream()
.filter(entry -> entry.getValue() > 0) // filter out any calls that don't have results
.sorted(Map.Entry.comparingByValue()) // sort all the calls with the smallest calls first
.map(Map.Entry::getKey) // just need the call itself now
.map(PropertyContentCall::stream) // get the stream for each call
.toList();
// Combine all the streams into one big stream and actually make the calls and write to the file.
try (Stream<RapidPropertyContent> bigStream = combineStreams(callsToMake);
BufferedWriter outputFileWriter = createFileWriter(Path.of("output.jsonl.gz"))) {
bigStream.parallel()
.forEach(property -> {
try {
// Write to output file
synchronized (outputFileWriter) {
outputFileWriter.append(OBJECT_MAPPER.writeValueAsString(property));
outputFileWriter.newLine();
}
} catch (Exception e) {
// Handle exception
}
});
}
}
/**
* This will split up the calls to be made based on the size of each call's results. It will first split into
* calls per country and, if needed, it will then further split into calls per category for any country that is
* too big on its own.
* The size of each call is also kept so that the calls can be further sorted if needed.
*
* @return A map containing all the calls and their respective sizes.
*/
private Map<PropertyContentCall, Integer> divideUpCalls() {
final Map<PropertyContentCall, Integer> allCalls = new HashMap<>();
COUNTRIES.stream().parallel()
.forEach(countryCode -> {
// Check to see if the entire country is small enough to get at once.
final PropertyContentCall countryCall = new PropertyContentCall(RAPID_CLIENT, LANGUAGE,
SUPPLY_SOURCE, List.of(countryCode), null);
final Integer countryCallSize = countryCall.size();
if (countryCallSize < MAX_CALL_SIZE) {
// It's small enough! No need to break this call up further.
allCalls.put(countryCall, countryCallSize);
} else {
// The country is too big, need to break up the call into smaller parts.
PROPERTY_CATEGORIES.stream().parallel()
.forEach(category -> {
final PropertyContentCall categoryCall = new PropertyContentCall(RAPID_CLIENT,
LANGUAGE, SUPPLY_SOURCE, List.of(countryCode), List.of(category));
allCalls.put(categoryCall, categoryCall.size());
});
}
});
return allCalls;
}
/**
* This will combine multiple Streams into a single Stream. Because of how this is reduced, the Streams will end
* up in the reverse order of the list that was passed in.
* <p>
* Note: Because this is concatenating multiple Streams together, each Stream will go on the stack. Thus, if
* there are many Streams then a StackOverflowException can occur when trying to use the combined Stream. Make
* sure the stack size is appropriate for your usage via the `-Xss` JVM parameter.
*
* @param streams A list of the Streams to combine.
* @return The combined Stream that can be treated as one.
*/
private <T> Stream<T> combineStreams(List<Stream<T>> streams) {
return streams.stream()
.filter(Objects::nonNull)
.reduce(Stream::concat)
.orElse(Stream.empty());
}
private BufferedWriter createFileWriter(Path path) throws IOException {
return new BufferedWriter(
new OutputStreamWriter(
new GZIPOutputStream(
Files.newOutputStream(path)),
StandardCharsets.UTF_8));
}
}
Si bien el código anterior tiene muchos comentarios insertados que explican los distintos elementos, se puede resumir de la manera siguiente:
- Se divide la llamada principal en llamadas más pequeñas según el caso de uso. (En este ejemplo, la llamada principal es para obtenerlo todo y la división se realiza por
country_code
y, si es necesario, porcategory_id
). - Las llamadas se ordenan para ejecutarse de manera más eficiente, de acuerdo con la forma específica de este ejemplo de combinar los flujos paralelos.
- A continuación, las llamadas se ejecutan en paralelo y los alojamientos devueltos por esas llamadas se escriben en un archivo.