Hierarchiczny faceting – czyli Pivot Facet w trunk’u

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

This post is also available in: angielski

This entry was posted on poniedziałek, Październik 25th, 2010 at 07:25 and is filed under Solr. You can follow any responses to this entry through the RSS 2.0 feed. You can leave a response, or trackback from your own site.

3 komentarze to “Hierarchiczny faceting – czyli Pivot Facet w trunk’u”

  1. Solr.pl – 23.11.2010 | Solr, Lucene i inne Says:

    […] „Hierarchiczny faceting, czyli Pivot Facet w trunk’u” – artykuł poświęcony funkcjonalności hierarchicznego facetingu w Solr. […]

  2. Pawel Says:

    Cześć, czy można zastosować ten mechanizm to wyświetlania drzewa kategorii produktów? Np. chciałbym, żeby razem z wynikiem zwrócona została lista głównych kategorii w jakich znaleziono produkty i dla każdej z nich lista bezpośrednich podkategorii wraz z ilością produktów, czyli mniej więcej to co dostajemy przy wyszukiwaniu na allegro.pl. Po tym co właśnie przeczytałem to wydaje mi się, że tak, ale jakw takim razie powinien wyglądać plik schema.xml? W jaki sposób indeksować ścieżkę kategorii dla produktu?

  3. gr0 Says:

    Jeżeli miałbyś w indeksie pole ‚kategoria’ oraz pole ‚podkategoria’, mógłbyś zrobić faceting na zasadzie facet.pivot=kategoria,podkategoria, który na pierwszy poziomie zostały podzielony na kategorie, a w pierwszym poziomie zaglębienia dostępne byłby podkategorie.

    Sytuacja komplikuje się z większą ilością kategorii przypisanych do pojedynczego produktu. Można też zrealizować taki faceting samemu wspomagając się np. http://wiki.apache.org/solr/AnalyzersTokenizersTokenFilters#solr.PathHierarchyTokenizerFactory i facetingiem po takim polu (ale już wtedy nie pivot, tylko field).