Monthly Archives: February 2016

Solr, uso de cursorMark para paginaciones profundas (deep paging)

Solr tiene un problema de rendimiento cuando empezamos a pedirle páginas muy profundas, lo conocido como deep paging. Es decir, dada una búsqueda, el tiempo de respuesta en devolver las primeras páginas de resultados y el tiempo en devolver las páginas, pongamos, 5000, 5001, etc es muy diferente, siendo este último caso el más penalizado.

Es este magnífico post explican el funcionamiento del cursorMark, cuya idea principal es que Solr se guarda en memoria un cursor apuntando a la página profunda que le has pedido, e identifica dicho cursor con una clave única. Posteriormente si quieres seguir la paginación en ese punto, solo tienes que pasarle de nuevo esa clave única, y Solr irá directamente a la siguiente página de resultados, ahorrando muchísimo tiempo al tenerlo ya en memoria.

El funcionamiento un poco más detallado es el siguiente:

Query a Solr con parámetro cursorMark=*

Al hacer la primera query, con los criterios de búsqueda que se necesiten, se añade un parámetro adicional cursorMark=*

http://localhost:8983/solr/example?q=*:*&rows=100&cursorMark=*

Con esto le estamos indicando a Solr que cree un nuevo cursor para esta consulta.

 

Solr devolverá la clave del cursorMark.

En el response, Solr añadirá la clave con la que identificar el cursor.

<?xml version="1.0" encoding="UTF-8"?>
<response>
 <lst name="responseHeader">
  ...
  </lst>
 </lst>
<result name="response" numFound="32" start="0">
 <doc>
 ...
 </doc>
 </result>
 <str name="nextCursorMark">AoIIP4AAACgwNTc5QjAwMg==</str>
</response>

 

Siguiente query a Solr con parámetro cursorMark=CLAVE

Con la segunda query le pasamos el cursorMark recibido en el response de la primera query, sin especificarle otro parámetro start. Al recibir este parámetro Solr sabrá que tiene que continuar por donde se había quedado y nos devolverá el siguiente bloque de documentos.

http://localhost:8983/solr/example?q=*:*&rows=100&cursorMark=AoIIP4AAACgwNTc5QjAwMg==

Solr devolverá la siguiente clave del cursorMark. En el response, Solr añadirá la nueva clave con la que identificar el cursor para el siguiente bloque de resultados

<?xml version="1.0" encoding="UTF-8"?>
<response>
 <lst name="responseHeader">
  ...
  </lst>
 </lst>
<result name="response" numFound="32" start="0">
 <doc>
 ...
 </doc>
 </result>
 <str name="nextCursorMark">AoIIP4AAACoxMDAtNDM1ODA1</str>
</response>

Y así podemos continuar mientras se quiera.

Yo intenté utilizar este mecanismo de Solr en un proceso batch en el que lanzaba queries que me devolvían muchos resultados, estoy hablando de cientos de miles, y necesitaba procesarlos todos, del primero al último. Pero la gran mejora en rendimiento que se producía inicialmente se iba degradando con el tiempo hasta dejarme el servidor frito por exceder el uso de memoria que tenía asignado.

La cuestión es que todos estos cursores Solr los va almacenando en memoria, de ahí que en caso de utilizarlos vayan tan rápidos, pero si pedimos demasiados, no tendrá nunca suficiente espacio. Por tanto, el cursorMark es una buena opción cuando vamos a reutilizar mucho los cursores, no cuanto tenemos una variabilidad alta.

¿Y como solucioné en mi caso el problema del deep paging? Pues troceando la query original, que me devolvía muchas páginas de resultados, en N “subqueries” cada una de las cuales me devolvía unas pocas páginas. Algo así:

Mi query original era más o menos así:

http://localhost:8983/solr/example?q=expresion de busqueda&start=0&rows=10000

que de esa forma, me devolvía cientos de miles de documentos y por tanto tenía que ir paginando con el consiguiente incremento del tiempo de respuesta cuanto más profunda era la página pedida.

http://localhost:8983/solr/example?q=expresion de busqueda&start=10000&rows=10000
http://localhost:8983/solr/example?q=expresion de busqueda&start=20000&rows=10000
http://localhost:8983/solr/example?q=expresion de busqueda&start=500000&rows=10000
...etc

Para trocearlo en subqueries busqué otro campo que no influyera en cada búsqueda concreta, del que pudiera conocer los valores mínimos y máximos, que me sirviera para lanzar queries por rangos más o menos del mismo tamaño y que me devolviera pocas páginas. Por suerte tenía mi identificador interno de los datos que indexaba, un autoincremental de tipo Long que cumplía estos criterios. Llamemos a este campo myID.

Manos a la obra, cogí myID y transformé la query original en N subqueries con filtro por rango de myID.

http://localhost:8983/solr/example?q=expresion de busqueda&fq=myID:[0 TO 200000]&start=0&rows=10000
http://localhost:8983/solr/example?q=expresion de busqueda&fq=myID:[0 TO 200000]&start=10000&rows=10000
http://localhost:8983/solr/example?q=expresion de busqueda&fq=myID:[2000000 TO 400000]&start=0&rows=10000
http://localhost:8983/solr/example?q=expresion de busqueda&fq=myID:[2000000 TO 400000]&start=10000&rows=10000

Está claro que no me ahorré por completo la paginación, pero si tener que irme a páginas extremadamente profundas para las que Solr tarda mucho tiempo en responder.

Además, al utilizar Filter Queries (fq), aprovechaba el cacheo que hace Solr de dichos Filter Queries, con lo que otras búsquedas del mismo proceso con diferente expresión en la query, pero con los mismos rangos de filter queries, se beneficiaban del cacheo previo.

http://localhost:8983/solr/example?q=otra busqueda&fq=myID:[0 TO 200000]&start=0&rows=10000

Con este enfoque conseguí leer de Solr enormes cantidades de resultados de búsqueda sin colapsar en ningún caso el servidor por uso excesivo de memoria.

 

Advertisements