W dużej ilości wdrożeń z jakimi miałem do czynienia zawsze pojawiało się pytanie – co możemy zrobić, aby uzyskać od Solr drzewiastą strukturę facetingu. Oczywiście są na to metody, jednak ich wykorzystanie polegało na modyfikacji danych i odpowiednim przetwarzaniu po stronie aplikacji. Nie było to szczególnie funkcjonalne, jak i szczególnie wygodne. Jednak kilka dni temu Solr w wersji 4.0 został wzbogacony o kod oznaczony jako SOLR-792 w systemie JIRA. Zobaczmy w takim wypadku, jak pobrać wyniki facetingu w postaci drzewa.
Ważna uwaga – funkcjonalność ta w tym momencie jest dostępna tylko i wyłącznie w wersji 4.0 Solr, czyli w wersji rozwojowej. Oznaczenie 4.0 jest oznaczeniem kodu, który znajduje się w trunk’u repozytorium SVN.
Kilka słów na początek
W wielu projektach w jakich miałem okazję zajmować się była konieczność wprowadzenia hierarchicznego facetingu. Jednym z prostszych przykładów jest wymaganie polegające na pokazaniu miejscowości w województwach i ilości dokumentów zarówno w województwach, jak i w poszczególnych miejscowościach. Do tej pory, bez zmiany struktury danych, nie było możliwości zrealizowania takiej funkcjonalności. Teraz już jest 😉
Indeksowanie
Aby nie potrzebnie nie komplikować opisywanych funkcjonalności zdecydowałem się na skorzystanie z przykładowych dokumentów XML dostępnych w katalogu /exampledocs przykładowego wdrożenia. Nie modyfikowałem także pliku schema.xml, czy solrconfig.xml, tak więc konfiguracje zostały standardowe. I tyle jeżeli chodzi o konfigurację. Tak więc możemy uruchomić indeksację (komenda wywołana z katalogu $SOLR_HOME/exampledocs/):
./post.sh *.xml
Kilka ekranów informacji i mamy zaindeksowane dane.
Mechanizm
Samo skorzystanie z hierarchicznego facetingu nie jest trudne. Twórcy Solr dali nam do dyspozycji dwa dodatkowe parametry:
- facet.pivot – lista pól oddzielonych przecinkami, która pokazuje po jakich polach i w jakiej kolejności wyliczyć strukturę,
- facet.pivot.mincount – minimalna ilość dokumentów, aby wynik został uwzględniony w facetingu. Wartość domyślna parametru to 1.
Spróbujmy więc.
Zapytania
Na początek próba z dwoma polami. Pobieram wszystkie dokumenty z indeksu i dodaje parametr facet.pivot=cat,inStock, czyli mówię Solr, że chce dostać wyniki hierarchicznego facetingu, gdzie pierwszym poziomem hierarchii jest pole cat, a drugim poziomem jest pole inStock. Zapytanie wygląda w następujący sposób:
http://localhost:8983/solr/select/?q=*:*&facet=true&facet.pivot=cat,inStock
Aby skrócić listing pominąłem część odpowiedzialną za wyniki wyszukiwania wraz z nagłówkiem.
<?xml version="1.0" encoding="UTF-8"?> <response> . . . <result name="response" numFound="19" start="0"/> <lst name="facet_counts"> <lst name="facet_queries"/> <lst name="facet_fields"/> <lst name="facet_dates"/> <lst name="facet_ranges"/> <lst name="facet_pivot"> <arr name="cat,inStock"> <lst> <str name="field">cat</str> <str name="value">electronics</str> <int name="count">17</int> <arr name="pivot"> <lst> <str name="field">inStock</str> <bool name="value">true</bool> <int name="count">13</int> </lst> <lst> <str name="field">inStock</str> <bool name="value">false</bool> <int name="count">4</int> </lst> </arr> </lst> <lst> <str name="field">cat</str> <str name="value">memory</str> <int name="count">6</int> <arr name="pivot"> <lst> <str name="field">inStock</str> <bool name="value">true</bool> <int name="count">6</int> </lst> </arr> </lst> <lst> <str name="field">cat</str> <str name="value">connector</str> <int name="count">2</int> <arr name="pivot"> <lst> <str name="field">inStock</str> <bool name="value">false</bool> <int name="count">2</int> </lst> </arr> </lst> <lst> <str name="field">cat</str> <str name="value">graphics card</str> <int name="count">2</int> <arr name="pivot"> <lst> <str name="field">inStock</str> <bool name="value">false</bool> <int name="count">2</int> </lst> </arr> </lst> <lst> <str name="field">cat</str> <str name="value">hard drive</str> <int name="count">2</int> <arr name="pivot"> <lst> <str name="field">inStock</str> <bool name="value">true</bool> <int name="count">2</int> </lst> </arr> </lst> <lst> <str name="field">cat</str> <str name="value">monitor</str> <int name="count">2</int> <arr name="pivot"> <lst> <str name="field">inStock</str> <bool name="value">true</bool> <int name="count">2</int> </lst> </arr> </lst> <lst> <str name="field">cat</str> <str name="value">search</str> <int name="count">2</int> <arr name="pivot"> <lst> <str name="field">inStock</str> <bool name="value">true</bool> <int name="count">2</int> </lst> </arr> </lst> <lst> <str name="field">cat</str> <str name="value">software</str> <int name="count">2</int> <arr name="pivot"> <lst> <str name="field">inStock</str> <bool name="value">true</bool> <int name="count">2</int> </lst> </arr> </lst> </arr> </lst> </lst> </response>
Sama prezentacja wyników facetingu, w tym wypadku, uległa zmianie. Dla każdej wartości głównego poziomu mamy znaczniki określające pole (znacznik z atrybutem name=”field”), wartość (znacznik z atrybutem name=”value”) oraz ilość dokumentów (znacznik z atrybutem name=”count”). Następnie mamy tablicę wyników drugiego poziomu (znacznik z atrybutem name=”pivot”). Tablica ta zawiera elementy takie same jak poziom pierwszy, czyli nazwa pola, wartość w polu oraz ilość dokumentów z daną wartością.
Zobaczmy, jak mechanizm ten daje sobie radę z większą ilością zagłębienia. W tym celu zadałem następujące zapytanie do tej samej wersji Solr:
http://localhost:8983/solr/select/?q=*:*&facet=true&facet.pivot=cat,inStock,features
Jak w powyższym przypadku w pominąłem nagłówek odpowiedzi wraz z wynikami zostawiając same wyniki facetingu. Dodatkowo, ze względu na długość wyników facetingu przedstawiam wyniki tylko dla jednej kategorii głównej pomijając resztę:
<?xml version="1.0" encoding="UTF-8"?> <response> . . . <result name="response" numFound="19" start="0"/> <lst name="facet_counts"> <lst name="facet_queries"/> <lst name="facet_fields"/> <lst name="facet_dates"/> <lst name="facet_ranges"/> <lst name="facet_pivot"> <arr name="cat,inStock,features"> <lst> <str name="field">cat</str> <str name="value">electronics</str> <int name="count">17</int> <arr name="pivot"> <lst> <str name="field">inStock</str> <bool name="value">true</bool> <int name="count">13</int> <arr name="pivot"> <lst> <str name="field">features</str> <str name="value">2</str> <int name="count">7</int> </lst> <lst> <str name="field">features</str> <str name="value">3</str> <int name="count">7</int> </lst> <lst> <str name="field">features</str> <str name="value">lcd</str> <int name="count">5</int> </lst> <lst> <str name="field">features</str> <str name="value">x</str> <int name="count">5</int> </lst> <lst> <str name="field">features</str> <str name="value">ca</str> <int name="count">4</int> </lst> <lst> <str name="field">features</str> <str name="value">latenc</str> <int name="count">4</int> </lst> <lst> <str name="field">features</str> <str name="value">tft</str> <int name="count">4</int> </lst> <lst> <str name="field">features</str> <str name="value">v</str> <int name="count">4</int> </lst> <lst> <str name="field">features</str> <str name="value">0</str> <int name="count">3</int> </lst> <lst> <str name="field">features</str> <str name="value">1</str> <int name="count">3</int> </lst> <lst> <str name="field">features</str> <str name="value">25</str> <int name="count">3</int> </lst> <lst> <str name="field">features</str> <str name="value">30</str> <int name="count">3</int> </lst> <lst> <str name="field">features</str> <str name="value">5</str> <int name="count">3</int> </lst> <lst> <str name="field">features</str> <str name="value">7</str> <int name="count">3</int> </lst> <lst> <str name="field">features</str> <str name="value">8</str> <int name="count">3</int> </lst> <lst> <str name="field">features</str> <str name="value">time</str> <int name="count">3</int> </lst> <lst> <str name="field">features</str> <str name="value">up</str> <int name="count">3</int> </lst> <lst> <str name="field">features</str> <str name="value">000</str> <int name="count">2</int> </lst> <lst> <str name="field">features</str> <str name="value">19</str> <int name="count">2</int> </lst> <lst> <str name="field">features</str> <str name="value">20</str> <int name="count">2</int> </lst> <lst> <str name="field">features</str> <str name="value">2336</str> <int name="count">2</int> </lst> <lst> <str name="field">features</str> <str name="value">27</str> <int name="count">2</int> </lst> <lst> <str name="field">features</str> <str name="value">275</str> <int name="count">2</int> </lst> <lst> <str name="field">features</str> <str name="value">6</str> <int name="count">2</int> </lst> <lst> <str name="field">features</str> <str name="value">75</str> <int name="count">2</int> </lst> <lst> <str name="field">features</str> <str name="value">activ</str> <int name="count">2</int> </lst> <lst> <str name="field">features</str> <str name="value">built</str> <int name="count">2</int> </lst> <lst> <str name="field">features</str> <str name="value">cach</str> <int name="count">2</int> </lst> <lst> <str name="field">features</str> <str name="value">color</str> <int name="count">2</int> </lst> <lst> <str name="field">features</str> <str name="value">flash</str> <int name="count">2</int> </lst> <lst> <str name="field">features</str> <str name="value">heat</str> <int name="count">2</int> </lst> <lst> <str name="field">features</str> <str name="value">heatspread</str> <int name="count">2</int> </lst> <lst> <str name="field">features</str> <str name="value">matrix</str> <int name="count">2</int> </lst> <lst> <str name="field">features</str> <str name="value">mb</str> <int name="count">2</int> </lst> <lst> <str name="field">features</str> <str name="value">ms</str> <int name="count">2</int> </lst> <lst> <str name="field">features</str> <str name="value">photo</str> <int name="count">2</int> </lst> <lst> <str name="field">features</str> <str name="value">resolut</str> <int name="count">2</int> </lst> <lst> <str name="field">features</str> <str name="value">seek</str> <int name="count">2</int> </lst> <lst> <str name="field">features</str> <str name="value">speed</str> <int name="count">2</int> </lst> <lst> <str name="field">features</str> <str name="value">spreader</str> <int name="count">2</int> </lst> <lst> <str name="field">features</str> <str name="value">unbuff</str> <int name="count">2</int> </lst> <lst> <str name="field">features</str> <str name="value">usb</str> <int name="count">2</int> </lst> </arr> </lst> <lst> <str name="field">inStock</str> <bool name="value">false</bool> <int name="count">4</int> <arr name="pivot"> <lst> <str name="field">features</str> <str name="value">0</str> <int name="count">2</int> </lst> <lst> <str name="field">features</str> <str name="value">1</str> <int name="count">2</int> </lst> <lst> <str name="field">features</str> <str name="value">16</str> <int name="count">2</int> </lst> <lst> <str name="field">features</str> <str name="value">2</str> <int name="count">2</int> </lst> <lst> <str name="field">features</str> <str name="value">20</str> <int name="count">2</int> </lst> <lst> <str name="field">features</str> <str name="value">3</str> <int name="count">2</int> </lst> <lst> <str name="field">features</str> <str name="value">9</str> <int name="count">2</int> </lst> <lst> <str name="field">features</str> <str name="value">90</str> <int name="count">2</int> </lst> <lst> <str name="field">features</str> <str name="value">adapt</str> <int name="count">2</int> </lst> <lst> <str name="field">features</str> <str name="value">car</str> <int name="count">2</int> </lst> <lst> <str name="field">features</str> <str name="value">clock</str> <int name="count">2</int> </lst> <lst> <str name="field">features</str> <str name="value">direct</str> <int name="count">2</int> </lst> <lst> <str name="field">features</str> <str name="value">directx</str> <int name="count">2</int> </lst> <lst> <str name="field">features</str> <str name="value">dual</str> <int name="count">2</int> </lst> <lst> <str name="field">features</str> <str name="value">dvi</str> <int name="count">2</int> </lst> <lst> <str name="field">features</str> <str name="value">express</str> <int name="count">2</int> </lst> <lst> <str name="field">features</str> <str name="value">gddr</str> <int name="count">2</int> </lst> <lst> <str name="field">features</str> <str name="value">ghz</str> <int name="count">2</int> </lst> <lst> <str name="field">features</str> <str name="value">gl</str> <int name="count">2</int> </lst> <lst> <str name="field">features</str> <str name="value">gpu</str> <int name="count">2</int> </lst> <lst> <str name="field">features</str> <str name="value">gpuvpu</str> <int name="count">2</int> </lst> <lst> <str name="field">features</str> <str name="value">hdtv</str> <int name="count">2</int> </lst> <lst> <str name="field">features</str> <str name="value">mb</str> <int name="count">2</int> </lst> <lst> <str name="field">features</str> <str name="value">mhz</str> <int name="count">2</int> </lst> <lst> <str name="field">features</str> <str name="value">open</str> <int name="count">2</int> </lst> <lst> <str name="field">features</str> <str name="value">opengl</str> <int name="count">2</int> </lst> <lst> <str name="field">features</str> <str name="value">out</str> <int name="count">2</int> </lst> <lst> <str name="field">features</str> <str name="value">pci</str> <int name="count">2</int> </lst> <lst> <str name="field">features</str> <str name="value">power</str> <int name="count">2</int> </lst> <lst> <str name="field">features</str> <str name="value">vpu</str> <int name="count">2</int> </lst> <lst> <str name="field">features</str> <str name="value">white</str> <int name="count">2</int> </lst> <lst> <str name="field">features</str> <str name="value">x</str> <int name="count">2</int> </lst> </arr> </lst> </arr> </lst> </arr> </lst> </lst> </response>
Jak widać na zaprezentowanym przykładzie, również w tym wypadku Solr nie miał problemów z poprawnym wyliczeniem hierarchii. Sama część prezentacyjna wzbogaciła się o jeden poziom zagłębienia, który podlega tym samym zasadom co reszta poziomów.
Kilka słów na koniec
Moim zdaniem jedna z bardziej przydatnych funkcjonalności dla „zwykłego” użytkownika. Niestety na razie dostępna tylko w wersji developerskiej Solr. Nie znalazłem także informacji o tym, czy planowane jest przeniesienie tej funkcjonalności do wersji 1.5 Solr, czyli gałęzi o nazwie branch_3x w SVN. Jednak, ważne jest to, że taka funkcjonalność powstała i wcześniej, czy później użytkownicy Solr będą mogli z niej korzystać.