Dawno, dawno temu, opisywaliśmy problem tzw. głębokiego stronicowania. W skrócie – im dalszą stronę wyników chcemy pobrać, tym wolniejsze będzie zapytanie. Dzieje się tak ze względu na konieczność przygotowania danych w kolejce. Niestety, nie było w Solr dobrego rozwiązania tego problemu – aż do premiery Solr 4.7. Wraz z niedawno wypuszczoną wersją dostaliśmy możliwość skorzystania z kursora i znaczącego poprawienia wydajności głębokiego stronicowania.
Problem
Problem głębokiego stronicowania jest całkiem prosty do zdefiniowania. Aby zwrócić wyniki wyszukiwania, Solr musi przygotować odpowiednią strukturę w pamięci i zwrócić jej część. Sytuacja jest prosta w przypadku pierwszej strony wyszukiwania, jednak komplikuje się dalej. Aby zwrócić stronę numer 10.000 (gdzie mamy 20 wyników na stronę) Solr musi przygotować strukturę zawierającą minimum 200.000 dokumentów (10.000 * 20). Można sobie wyobrazić, iż nie tylko zabiera to czas, ale także znaczne ilości pamięci.
Na szczęście, wraz z premierą Solr 4.7 sytuacja się zmieniła – wprowadzono pojęcie kursora. Kursor jest logiczną strukturą, która nie wymaga przechowywania stanu po stronie serwera. Kursor zawiera informacje na temat sortowania i ostatniego dokumentu w zwróconych wynikach wyszukiwania, dzięki temu Solr nie musi za każdym razem budować całej struktury od początku. Skutkuje to bardzo dużym wzrostem wydajności zapytań korzystających z kursora i głębokiego stronicowania.
Działanie
Zasada działania jest dość prosta. Aby Solr mógł skorzystać z nowej funkcjonalności, w pierwszym zapytaniu przekazujemy parametr cursorMark=*. W odpowiedzi, oprócz wyników wyszukiwania zostanie zwrócony identyfikator kursora, który przekazujemy w ramach następnych zapytań. Spójrzmy na przykład.
Zapytanie
Na początek zadajemy proste zapytanie:
curl 'localhost:8983/solr/select?q=*:*&rows=1&sort=score+desc,id+asc&cursorMark=*'
Warto spojrzeć na cztery rzeczy. Po pierwsze – albo pomijamy parametr start, albo ustawiamy parametr start na 0. Parametr rows może przyjmować wartości jakich potrzebujemy. Oczywiście przekazaliśmy parametr cursorMark=*, aby skorzystać z kursora. Ostatnią rzeczą jaką zrobiliśmy, to zdefiniowanie sortowania. Aby uniknąć problemów z sortowaniem, nasze sortowanie musi zawierać sortowanie po identyfikatorze – z tego względu musieliśmy nadpisać domyślne sortowanie przekazując do Solr parametr sort=score+desc,id+asc.
Wyniki wyszukiwania
Nasze zapytanie zwraca następujący wynik wyszukiwania:
<?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>
Jak widzimy, oprócz standardowych informacji, Solr zwrócił także identyfikator kursora – nextCursorMark. Teraz, aby pobrać kolejne wyniki wyszukiwania musimy uwzględnić go w naszym zapytaniu korzystając z cursorMark.
Kolejne zapytanie
Zatem nasze kolejne zapytanie wyglądać będzie następująco:
curl 'localhost:8983/solr/select?q=*:*&rows=1&sort=score+desc,id+asc&cursorMark=AoIIP4AAACgwNTc5QjAwMg=='
Wynik tego zapytania będzie następujący:
<?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>
Jak widzimy Solr znów zwrócił informacje o kursorze – tym razem innym, niż poprzednio.
Wszystkie kolejne zapytania
Dalsza logika jest prosta, korzystamy z parametru cursorMark używając wartości zwróconej przez Solr wraz z poprzednim wynikiem wyszukiwania. Zatem nasze kolejne zapytanie wyglądałoby następująco:
curl 'localhost:8983/solr/select?q=*:*&rows=1&sort=score+desc,id+asc&cursorMark=AoIIP4AAACoxMDAtNDM1ODA1'
Podsumowanie
Proste API, ogromny zysk wydajności w przypadku głębokiego stronicowania, brak konieczności zadawania zapytania do tej samej instancji Solr. Tak w skrócie można byłoby podsumować nowe możliwości stronicowania wprowadzone w Solr 4.7. Aby ponownie nie wynajdować koła, stwierdziłem, że daruje sobie testy wydajnościowe, szczególnie, że dość dokładne testy przeprowadził Chris Hostetter we swoim wpisie dostępnym pod adresem: http://searchhub.org/2013/12/12/coming-soon-to-solr-efficient-cursor-based-iteration-of-large-result-sets/.