Long, long time ago, we described a problem called deep paging. To keep things short – the deeper you want to go in the results, the slower the query will be. This is because Solr needs to prepare the data from the beginning for each query. Until Solr 4.7 there wasn’t a good solution for that problem. With the recently released Solr version, we got a possibility of using so called cursor to drastically improve performance of deep paging.
The problem
The deep paging problem is quite easy to define. To return search results Solr must prepare an in-memory structure and return part of it. Returning the part of the structure is simple, if that part comes from the beginning of the structure. However, if we want to return page number 10.000 (where we return 20 results per page) Solr needs to prepare a structure containing minimum of 200.000 elements (10.000 * 20). You see that it not only takes time, but also memory.
The good thing is, that with the release of Solr 4.7 the situation had changed – the cursor has been introduced. Cursor is a logic structure, that doesn’t require its state to be stored on the server side. Cursor contains information about storing and lest document returned in the results. Because of that, Solr doesn’t need to start search from beginning each time we want to get next page of results. It results in drastic performance improvement when using cursor and going deep into results.
Usage
Cursor usage is very simple. To tell Solr to return cursor, in the first query we need to pass an additional parameter – cursorMark=*. In result, apart from documents, we will get a cursor identifier returned in the nextCursorMark parameter. Let’s look at the example.
The query
Let’s start with a very simple query:
curl 'localhost:8983/solr/select?q=*:*&rows=1&sort=score+desc,id+asc&cursorMark=*'
There are four things here that we are interested in. First of off, we either omit the start parameter or we set it to 0. The rows parameter can take values we need, there is no limitation on it. Of course, we passed the cursorMark=* parameter, to tell Solr that we want the cursor to be used. The final thing we did is sorting definition. We need to define sorting for cursor to be working, one that will tell cursor how to behave. That’s why we needed to overwrite default sorting and include sorting not only by score, by also by document identifier.
Search results
Our query returns the following search results:
<?xml version="1.0" encoding="UTF-8"?> <response> <lst name="responseHeader"> <int name="status">0</int> <int name="QTime">33</int> <lst name="params"> <str name="sort">score desc,id asc</str> <str name="start">0</str> <str name="q">*:*</str> <str name="cursorMark">*</str> <str name="rows">1</str> </lst> </lst> <result name="response" numFound="32" start="0"> <doc> <str name="id">0579B002</str> <str name="name">Canon PIXMA MP500 All-In-One Photo Printer</str> <str name="manu">Canon Inc.</str> <str name="manu_id_s">canon</str> <arr name="cat"> <str>electronics</str> <str>multifunction printer</str> <str>printer</str> <str>scanner</str> <str>copier</str> </arr> <arr name="features"> <str>Multifunction ink-jet color photo printer</str> <str>Flatbed scanner, optical scan resolution of 1,200 x 2,400 dpi</str> <str>2.5" color LCD preview screen</str> <str>Duplex Copying</str> <str>Printing speed up to 29ppm black, 19ppm color</str> <str>Hi-Speed USB</str> <str>memory card: CompactFlash, Micro Drive, SmartMedia, Memory Stick, Memory Stick Pro, SD Card, and MultiMediaCard</str> </arr> <float name="weight">352.0</float> <float name="price">179.99</float> <str name="price_c">179.99,USD</str> <int name="popularity">6</int> <bool name="inStock">true</bool> <str name="store">45.19214,-93.89941</str> <long name="_version_">1461375031699308544</long></doc> </result> <str name="nextCursorMark">AoIIP4AAACgwNTc5QjAwMg==</str> </response>
As we can see, in addition to standard search results, we got the cursor identifier in the nextCursorMark section. Now, to get the next results bound to that cursor, we need to pass that identifier using the cursorMark parameter.
Next query
Our next query looks as follows (note the cursorMark parameter value):
curl 'localhost:8983/solr/select?q=*:*&rows=1&sort=score+desc,id+asc&cursorMark=AoIIP4AAACgwNTc5QjAwMg=='
The results were as follows:
<?xml version="1.0" encoding="UTF-8"?> <response> <lst name="responseHeader"> <int name="status">0</int> <int name="QTime">2</int> <lst name="params"> <str name="sort">score desc,id asc</str> <str name="indent">true</str> <str name="q">*:*</str> <str name="cursorMark">AoIIP4AAACgwNTc5QjAwMg==</str> <str name="rows">1</str> </lst> </lst> <result name="response" numFound="32" start="0"> <doc> <str name="id">100-435805</str> <str name="name">ATI Radeon X1900 XTX 512 MB PCIE Video Card</str> <str name="manu">ATI Technologies</str> <str name="manu_id_s">ati</str> <arr name="cat"> <str>electronics</str> <str>graphics card</str> </arr> <arr name="features"> <str>ATI RADEON X1900 GPU/VPU clocked at 650MHz</str> <str>512MB GDDR3 SDRAM clocked at 1.55GHz</str> <str>PCI Express x16</str> <str>dual DVI, HDTV, svideo, composite out</str> <str>OpenGL 2.0, DirectX 9.0</str> </arr> <float name="weight">48.0</float> <float name="price">649.99</float> <str name="price_c">649.99,USD</str> <int name="popularity">7</int> <bool name="inStock">false</bool> <date name="manufacturedate_dt">2006-02-13T00:00:00Z</date> <str name="store">40.7143,-74.006</str> <long name="_version_">1461375031846109184</long></doc> </result> <str name="nextCursorMark">AoIIP4AAACoxMDAtNDM1ODA1</str> </response>
As we can see, the returned nextCursorMark was different again.
Further queries
Logic for further queries is simple – we use the cursorMark parameter with the value returned with the previous search results. So again, our next query would look as follows:
curl 'localhost:8983/solr/select?q=*:*&rows=1&sort=score+desc,id+asc&cursorMark=AoIIP4AAACoxMDAtNDM1ODA1'
Summary
Simple API and massive gain on performance in case of deep paging. That’s how I think the cursor introduced in Solr 4.7 could be summarized. I decided not do re-do performance tests, there are ones already done by Chris Hostetter in his entry about this functionality. If you are interested please look at: http://searchhub.org/2013/12/12/coming-soon-to-solr-efficient-cursor-based-iteration-of-large-result-sets/.