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ć.