Korzystając z SolrCloud, kiedy nasza kolekcja rozproszona jest pomiędzy wiele instancji Solr dotyka nas problem związany z obliczaniem tzw. odwrotnej częstotliwości dokumentu, czyli zmiennej określającej w jak dużej liczbie dokumentów występuje dany term. Problem w tym, iż domyślnie każda instanacja Solr trzyma dane na temat tej częstotliwości tylko sobie i nie dzieli się z innymi 😉 A co za tym idzie, im bardziej rozproszona jest nasza kolekcja, tym mniejsza jest precyzja naszego wyszukiwania. Zobaczmy zatem co możemy na to poradzić.
IDF i Solr
Wraz z premierą Lucene i Solr 6.0 domyślny algortym odpowiadający za score TF/IDF został zastąpiony algorytmem BM25, który powinien spisywać się lepiej ze względu na ograniczenia związane z maksymalnym wpływem częstotliwości termu.
W algorytmie BM25 współczynnik IDF obliczany jest w następujący sposób:
log(1 + (docCount - docFreq + 0.5)/(docFreq + 0.5))
Oznacza to, iż wartość IDF jest dużo szybciej i mocniej ograniczana w przypadku często występujących termów porównując to algorytmu TF/IDF obecnego przez we wcześniejszych wersjach Lucene i Solr.
Problemem jest to, iż informacja ja, domyślnie nie jest rozproszona. Oznacza to, że każdy shard przechowuje te informacje powodując, iż im bardziej kolekcja jest rozproszona, tym mniejsza dokładność. W większości przypadków nie jest to problem, jednak są takie gdzie jest to jednak coś, czym zaczynamy się przejmować. Dodatkowo, zmiany w architekturze kolekcji powodują zmiany w wyliczanym współczynniku score, nawet przy tych samych dokumentach. Jest to czasem niedopuszczalne.
Rozproszone IDF
Wraz z Solr 5.0 wprowadzona została możliwość skonfigurowania Solr do korzystania z rozproszonego obliczania IDF. Wystarczy do pliku solrconfig.xml dodać definicję statsCache i to wszystko. Na przykład:
<statsCache class="org.apache.solr.search.stats.ExactStatsCache" />
Proste prawda? Spójrzmy zatem jakie mamy opcje jeżeli chodzi o implementację:
LocalStatsCache – domyślna implementacja, kiedy nie mamy zdefiniowanego elementu statsCache w pliku solrconfig.xml. W tym wypadku sytuacja jest bardzo prosta – brak rozproszonego obliczania współczynnika IDF.
ExactStatsCache – implementacja cacheująca statystyki dotyczące termów wymagająca dodatkowego kroku podczas przetwarzania zapytania, tak aby zapytanie było analizowane na każdym shardzie wchodzącym w skład kolekcji. Może skutkować spadkiem wydajności zapytań ze względu na dodatkowe operacje.
ExactSharedStatsCache – implementacja podobna do opisywanej powyżej z tą różnicą, iż statystki są współdzielone pomiędzy żądaniami. Może skutkować większą wydajnością, niż ExactStatsCache, ale cechować go będzie większe wykorzystanie pamięci.
LRUStatsCache – w odróżnieniu do ExactStatsCache implementacja przechowująca statystyki dotyczące termów w LRU cache. Bazując na informacjach w cache’u Solr jest w stanie określić, czy dodatkowe zapytania są potrzebne tym samym zmniejszając ich częstotliwość, a tym samym zwiększając wydajność zapytań. Termy przechowywane są w mapach – liczba map determinowana jest liczbą shardów z których składa się kolekcja – map będzie tyle, ile jest shardów, a każdy shard trzyma jedną mapę ze statystykami termów.
Czy to działa?
Na tak postawione pytanie możemy odpowiedzieć tylko – trzeba to po prostu sprawdzić. W tym celu stworzymy dwie kolekcje distrib_idf oraz non_distrib_idf oraz zaindeksujemy przykładowe dane (konfigurację i dane można znaleźć na naszym Githubie – https://github.com/solrpl/blog). Każda z kolekcji będzie miała dwa shardy, jedną replikę, a sam klaster SolrCloud będzie składał się z dwóch instancji Solr.
Po zaindeksowaniu kilku przykładowych dokumentów możemy sprawdzić różnicę w wyliczaniu score zadając następujące zapytanie do każdej z kolekcji:
.../select?q=title:solr&fl=*,score
W przypadku kolekcji, która została skonfigurowana bez statsCache wyniki wyglądają następująco:
{ "responseHeader":{ "zkConnected":true, "status":0, "QTime":7, "params":{ "q":"title:solr", "fl":"*,score"}}, "response":{"numFound":4,"start":0,"maxScore":0.082873434,"docs":[ { "id":"2", "title":["Solr document two"], "version":1633875263859195904, "score":0.082873434}, { "id":"3", "title":["Solr document three"], "version":1633875263924207616, "score":0.082873434}, { "id":"1", "title":["Solr document one"], "version":1633875263562448896, "score":0.082873434}, { "id":"4", "title":["Solr document four"], "version":1633875263679889408, "score":0.082873434}] }}
W przypadku kolekcji w której statsCache jest skonfigurowany wyniki wyglądają następująco:
{ "responseHeader":{ "zkConnected":true, "status":0, "QTime":12, "params":{ "q":"title:solr", "fl":"*,score"}}, "response":{"numFound":4,"start":0,"maxScore":0.04789114,"docs":[ { "id":"2", "title":["Solr document two"], "version":1633875264251363328, "score":0.04789114}, { "id":"3", "title":["Solr document three"], "version":1633875264254509056, "score":0.04789114}, { "id":"1", "title":["Solr document one"], "version":1633875264186351616, "score":0.04789114}, { "id":"4", "title":["Solr document four"], "version":1633875264189497344, "score":0.04789114}] }}
Jest to oczywiście bardzo prosty przykład, jednak jak widać w przypadku kolekcji w której nie mamy skonfigurowanego statsCache score dokumentów jest prawie dwa razy wyższy. Ma to sens biorąc pod uwagę, iż term solr występuje w polu title cztery razy, po dwa razy w każdym shardzie. Oznacza to, iż kolekcja bez skonfigurowanego rozproszonego wyliczania IDF nie widzi pełnych danych.
Podsumowanie
Brak rozproszonego obliczania współczynnika IDF może powodować problemy – wtedy, kiedy jakość wyników wyszukiwania jest dla nas szczególnie ważna. Na szczęście, od wersji 5 silnika wyszukiwania Solr jesteśmy w stanie w bardzo prosty sposób włączyć rozproszony cache statystyk termów, a tym samym całkowicie rozprawić się z problemem. Oczywiście należy pamiętać, iż włączenie mechanizmu statsCache niesie za sobą obniżenie wydajności zapytań ze względu na konieczność dodatkowych operacji. Powodzenia 🙂