W pierwszej części naszego cyklu stworzyliśmy pewną standardową strukturę indeksu, odpowiednio konfigurując plik schema.xml. Przy takiej konfiguracji, na pierwsze skargi klientów, dotyczących działania silnika wyszukiwawczego nie trzeba było długo czekać. Dlaczego wpisując w kryteria wyszukiwania frazę „audi a” nie otrzymuję ofert związanych z autami „Audi A6” lub „Audi A8” ? Wpisałem „Honda crv” – 0 wyników. „Suzuki maruti” – też nic. Czy takich ofert nie ma w bazie z ogłoszeniami ? Otóż są, ale konfiguracja typu pola, po którym wyszukujemy (pole „content” – typ „text”) uniemożliwia w obecnym stanie znalezienie tych ogłoszeń przy zastosowaniu powyższych zapytań. Na pomoc rusza nam chyba najbardziej popularny filtr – WordDelimiterFilter, oraz PatternReplaceFilter, których odpowiednia konfiguracja pozwoli sprostać naszym potrzebom.
Analiza wymagań
W celu dokonania analizy danych, które wchodzą w skład pola, po którym wyszukujemy, weźmy następującą próbkę, na której oprzemy naszą konfigurację:
- Marka: Audi
Modele: 80, 90, A6, A8, TT
- Marka: BMW
Modele: M3, M5, Seria 7, Seria 8, X1, X3
- Marka: Chevrolet
Modele: TrailBlazer
- Marka: Citroen
Modele: C-Crosser, C3 Pluriel, C4 Picasso
- Marka: Ford
Modele: C-MAX, S-MAX
- Marka: Honda
Modele: Accord, CR-V, FR-V, HR-V
- Marka: Kia
Modele: Cee’d
- Marka: Suzuki
Modele: Alto/Maruti
Nazwy marek są prostymi słowami, z którymi aktualna konfiguracja (WhitespaceTokenizer + LowerCaseFilter) poradzi sobie bez problemu. Problem pojawia się przy modelach aut, które zawierają dodatkowe znaki oraz separatory, które często ignorujemy przy wyszukiwaniu. Pogrupujmy sobie powyższą próbkę ze względu na charakterystykę danych:
- Nazwy modeli, które nie wymagają dodatkowych filtrów i obecna konfiguracja jest wystarczająca – 80, 90, TT, Seria 7, Seria 8, Accord
- Nazwy modeli, których nazwy składają się z cyfr i liter, których to rozdzielenie jest pożądane – A6, A8, M3, M5, X1, X3, C3 Pluriel, C4 Picasso. Chcielibyśmy móc wyszukiwać powyższe modele wpisując tylko literę lub tylko cyfrę, ale również wpisując całą nazwę modelu.
- Modele, które mają zmianę wielkości znaków w nazwie – TrailBlazer. Chcielibyśmy znaleźć taki model wpisując „trail”, „blazer”, „trailBlazer”, „trailblazer”.
- Nazwy modeli, które zawierają separatory, które chcemy ignorować (wpisując nazwę modelu jako pełny wyraz – uwzględniając separator lub nie – oraz po częściach nazwy modelu, które taki separator generuje) – C-Crosser, C-MAX, S-MAX, CR-V, FR-V, HR-V, Alto/Maruti.
Przykład: chcielibyśmy znaleźć ogłoszenie z modelem „C-MAX” wpisująć frazy „c”, „max”, „c-max” „cmax”. - Celowo w punkcie 4 pominąłem model „Cee’d”. Ten model przy wyszukiwaniu chcielibyśmy traktować trochę inaczej, a mianowicie uniemożliwić znalezienie ogłoszenia przy wpisaniu „cee” lub „d”. Traktujemy nazwę „Cee’d” tylko i wyłącznie jako jeden wyraz, czyli realizujemy wyszukiwanie tylko dla przypadków „cee’d” oraz „ceed”.
Konfiguracja WordDelimiterFilter
Na podstawie opisanej charakterystyki dobierzmy takie wartości atrybutów filtra WordDelimiterFilter, aby wszystkie powyższe wymagania zostały spełnione:
- WordDelimiterFilter jest w tym wypadku zbędny, do realizacji wymagań z pkt 1 wystarczy WhitespaceTokenizer + LowerCaseFilter.
- W celu realizacji wymagań z pkt 2 należy zadbać o odpowiednie ustawienie następujących atrybutów:
- generateWordParts=”1″ – wartość musi być ustawiona na „1”, jeżeli chcemy mieć możliwość generowania części słów
- generateNumberParts=”1″ – wartość musi być ustawiona na „1”, jeżeli chcemy mieć możliwość generowania części liczbowych
- splitOnNumerics=”1″ – wartość musi być ustawiona na „1”, jeżeli chcemy mieć możliwość rozdzielania literek od liczb
- W celu realizacji wymagań z pkt 3, musimy ustawić następujące atrybuty:
- generateWordParts=”1″
- splitOnCaseChange=”1″ – wartość musi być ustawiona na „1”, jeżeli chcemy mieć możliwość generowania części słów przy przejściu z dużej litery na małą i odwrotnie
- W celu realizacji wymagań z pkt 4, ustawiamy następujące atrybuty:
- generateWordParts=”1″
- catenateWords=”1″ – wartość musi być ustawiona na „1”, abyśmy mogli dodatkowo ignorować separatory, poprzez łączenie wyrazów, które są takim separatorem rozdzielone
Zatem konfiguracja naszego filtra wygląda następująco:
<filter class="solr.WordDelimiterFilterFactory" splitOnNumerics="1" splitOnNumerics="1" generateWordParts="1" generateNumberParts="1" catenateWords="1" />
Dodatkowo okazuje się, że domyślna wartość atrybutów „splitOnNumerics” oraz „splitOnNumerics” to właśnie „1”. Pozostałe atrybuty, których nie wykorzystujemy (poza „stemEnglishPossessive”), mają domyślną wartość na „0”. Konfiguracja naszego filtra zatem upraszcza się do następującej postaci:
<filter class="solr.WordDelimiterFilterFactory" generateWordParts="1" generateNumberParts="1" catenateWords="1" stemEnglishPossessive="0" />
Co zrobić z punktem nr. 5 naszej charakterystyki danych? Ustaliliśmy, że nie chcielibyśmy dla tego przypadku traktować znaku ” ’ ” jako separatora, a tak właśnie by się stało przy powyższej konfiguracji. Może zatem użyć w filtrze opcji, która zachowa to słowo w stanie niezmienionym, czyli wykorzystać atrybut protected=”protwords.txt” i dodać słowo „Cee’d” do pliku protwords.txt? No tak, ale co z faktem, że chcemy móc wyszukać taki dokument, przy wpisaniu frazy „ceed” ? Najlepiej by było zająć się tym przypadkiem w oddzielnym filtrze, a do filtra WordDelimiterFilter wprowadzić wartość, której ten filtr nie będzie musiał już analizować.
Konfiguracja PatternReplaceFilter
Filtr PatternReplaceFilter zastosujemy przed filtrem WordDelimiterFilter. Za pomocą PatternReplaceFilter będziemy mogli po prostu wyciąć znak „'” z nazwy tego specyficznego modelu, zastępując go pustym znakiem. W ten sposób, do filtra WordDelimiterFilter trafi nam nazwa „Ceed”, która przy obecnej konfiguracji nie zastosuje na takiej wartości żadnej modyfikacji. Filtry będą miały taką samą konfigurację przy indeksowaniu jak i przy wyszukiwaniu, zatem użytkownik będzie w stanie znaleźć ogłoszenie z marką „Cee’d” przy wpisaniu frazy „cee’d” jak i „ceed”:
<filter class="solr.PatternReplaceFilterFactory" pattern="'" replacement="" replace="all" />
Wizualizacja działania nowej konfiguracji typu pola „text”
Podsumowując, nasz typ „text” zmienił się następująco:
<fieldType name="text" positionIncrementGap="100"> <analyzer> <tokenizer class="solr.WhitespaceTokenizerFactory"/> <filter class="solr.PatternReplaceFilterFactory" pattern="'" replacement="" replace="all" /> <filter class="solr.WordDelimiterFilterFactory" generateWordParts="1" generateNumberParts="1" catenateWords="1" stemEnglishPossessive="0" /> <filter class="solr.LowerCaseFilterFactory"/> </analyzer> </fieldType>
Wykorzystajmy panel administracyjny solr, aby zobaczyć na przykładzie każdego z punktów, czy konfigurując nasz typ tak jak powyżej, otrzymamy to, czego oczekujemy:
- (Model: „80”) Tak jak oczekiwaliśmy, wprowadzone filtry nie mają wpływu na dane charakterystyczne dla punktu 1.
- (Model: „A8”) WordDelimiterFilter rozdzielił nam liczbę od wyrazu.
- (Model: „TrailBlazer”)WordDelimiterFilter rozdzielił nam „trail” od „Blazer”. Dodatkowo mamy możliwość wyszukiwania po „trailblazer”. Super.
- (Model: „CR-V”) WordDelimiterFilter rozdzielił nam wyraz po separatorze (w tym wypadku „-„). Dodatkowo mamy możliwość wyszukiwania po nazwie modelu nie uwzględniając separatora („crv”).
- (Model: „Cee’d”) PatternReplaceFilter zamienił nam „Cee’d” na „Ceed” a WordDelimiterFilter zachował tę wartość. O to nam chodziło.
Podsumowanie
W drugiej części naszego cyklu użyliśmy dwóch nowych filtrów w celu poprawy jakości wyników wyszukiwania. Na przykładzie naszych „samochodowych” danych omówiliśmy użycie WordDelimiterFilter oraz PatternReplaceFilter. Poprawka wprowadzona, klient usatysfakcjonowany … ale na jak długo ?