Nadszedł czas, abyśmy dodali do naszej aplikacji sprzedaży samochodów kolejną ważną funkcjonalność. Będzie to mechanizm sprawdzania poprawności wpisanej frazy wyszukiwania oraz podpowiadania frazy poprawnej. Funkcjonalność ta stała się już standardem we wszystkich silnikach wyszukiwania, zatem i my zrobimy z niej użytek.
Analiza wymagań
Nasza baza samochodów jest już tak duża, że zawiera nazwy marek i modeli, których poprawne napisanie może sprawiać naszym klientom problemy, np:
-
- marka: Bugatti
- model: Veyron
-
- marka: Daewoo
- model: Lacetti
-
- marka: Cadillac
- model: Brougham
-
- marka: Ford
- model: Capri
-
- marka: Maserati
- model: Coupe
Przykłady zapytań, które zwróciły 0 wyników, ze względu na niepoprawnie wpisane nazwy (frazy):
- ?q=bugati+weyron
- ?q=daewo+laceti
- ?q=cadilac+brogham
- ?q=ford+kapri
- ?q=maseratti+coupe
Chcemy dodać funkcjonalność, która przy niepoprawnie wpisanych nazwach podpowie nam frazę, którą najprawdopodobniej klient miał na myśli, a której zastosowanie pozwoli nam wyszukać dokumenty związane z tą frazą.
Zmiany w solrconfig.xml
Najważniejszym elementem, który musimy dodać do pliku konfiguracyjnego solrconfig.xml jest komponent wykorzystujący klasę solr.SpellCheckComponent. Spróbujemy wykorzystać najprostszą, standardową konfigurację tego komponentu i przekonamy się, jak jego działanie sprawdzi się w praktyce:
<searchComponent name="spellcheck" class="solr.SpellCheckComponent"> <lst name="spellchecker"> <str name="classname">solr.IndexBasedSpellChecker</str> <str name="spellcheckIndexDir">./spellchecker</str> <str name="field">content</str> <str name="buildOnCommit">true</str> </lst> </searchComponent>
Wyjaśnijmy sobie co znaczą poszczególne atrybuty:
-
-
classname – klasa która jest implementacją naszego mechanizmu podpowiadania poprawnej frazy wyszukiwania. Wykorzystujemy klasę solr.IndexBasedSpellChecker, która jako źródło podpowiedzi wykorzystuje indeks solr.
-
spellcheckIndexDir – katalog, w którym przechowywany będzie indeks mechanizmu popowiedzi.
-
field – nazwa pola zdefiniowanego w pliku schema.xml, wykorzystywanego jako pole źródłowe do generowania indeksu dla mechanizmu podpowiedzi. W naszym przypadku będzie to pole o nazwie „content”, co zostanie uzasadnionej później.
-
buildOnCommit – jeżeli atrybut ten będzie ustawiony na wartośc true, to indeks mechanizmu podpowiedzi zostanie automatycznie wygenerowany przy każdym uaktualnieniu (commit) indeksu solr.
-
Mamy już zdefiniowany komponent, zatem teraz należy go wykorzystać w którymś z handler’ów, aby można było się do niego odwoływać. Najlepiej dodać go do handler’a, którego domyślnie używamy do wyszukiwania dokumentów. W ten sposób będziemy mogli za pomocą tylko jednego żądania otrzymywać wyniki wyszukiwania wraz z podpowiedzią. Przed uaktualnieniem, nasz domyślny handler wyglądał tak:
<requestHandler name="standard" class="solr.SearchHandler" default="true"> <lst name="defaults"> <str name="echoParams">explicit</str> </lst> </requestHandler>
Po zmianie, wygląda tak:
<requestHandler name="standard" class="solr.SearchHandler" default="true"> <lst name="defaults"> <str name="echoParams">explicit</str> <str name="spellcheck">true</str> <str name="spellcheck.collate">true</str> </lst> <arr name="last-components"> <str>spellcheck</str> </arr> </requestHandler>
Jak widać, oprócz naszego komponentu spellcheck, dodaliśmy również dwie domyślne wartość wykorzystywane w zapytaniach:
-
-
spellcheck – ustawienie wartości na true powoduje że dla każdego requestu nastąpi próba wygenerowania podpowiedzi.
-
spellcheck.collate – ustawienie wartości na true powoduje że mechanizm wybiera najlepszą podpowiedź dla każdego wyrazu i konstruuje nowe zapytanie składające się z tych podpowiedzi. Jeżeli mechanizm uzna, że dany wyraz jest poprawny, zostawia go w niezmienionej postaci.
-
Zmiany w schema.xml
Ewentualne zmiany w pliku schema.xml będą polegały na dodaniu pola, wykorzystywanego przez komponent solr.SpellCheckComponent jako źródło danych do generowania indeksu dla mechanizmu podpowiedzi. Pole takie powinno zawierać wszystkie informacje, jakie chcielibyśmy aby było użyte przy tworzeniu indeksu dla mechanizmu podpowiedzi. Typ takiego pola powinien zapewniać odpowiednią tokenizację indeksowanych danych, jak i być pozbawionym wszelkich filtrów używających stemmingu czy lametyzacji, co by mogło niekorzystnie wpłynąć na wyniki podpowiedzi.
Nasza schema posiada już pole spełniające wszystkie te wymaganie, a nazywa się „content”. Dla przypomnienia, jest to pole domyślne, po którym realizowane jest wyszukiwanie przez silnik solr. Przypomnijmy sobie aktualną definicję tego pola, jak i jego typu:
<field name="content" type="text" indexed="true" stored="false" multiValued="true"/>
<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>
Do pola „content” kopiowane są wartości z pól marki, modelu i roku:
<copyField source="make" dest="content"/> <copyField source="model" dest="content"/> <copyField source="year" dest="content"/>
Tworzymy zapytania
Wykorzystamy zapytania z analizy wymagań, które nie zwróciły nam żadnych wyników, dodając parametr spellcheck.q, gdzie wpisujemy tę samą frazę co dla parametru q. W ten sposób, za pomocą jednego zapytania zwrócimy wyniki wyszukiwania wraz z wynikami mechanizmu podpowiedzi:
- ?q=bugati+weyron&spellcheck.q=bugati+weyron
<result name="response" numFound="0" start="0" /> <lst name="spellcheck"> <lst name="suggestions"> <lst name="bugati"> <int name="numFound">1</int> <int name="startOffset">0</int> <int name="endOffset">6</int> <arr name="suggestion"> <str>bugatti</str> </arr> </lst> <lst name="weyron"> <int name="numFound">1</int> <int name="startOffset">7</int> <int name="endOffset">13</int> <arr name="suggestion"> <str>veyron</str> </arr> </lst> <str name="collation">bugatti veyron</str> </lst> </lst>
Mechanizm podpowiedzi poprawił nam wyrazy zapytania na poprawne i dodatkowo funkcjonalność „collation” wygenerowała nam gotowe zapytanie, które możemy wykorzystać w celach zwrócenia nam poprawnych wyników wyszukiwania. Sprawdźmy kolejne przykłady:
- ?q=daewo+laceti&spellcheck.q=?q=daewo+laceti
<result name="response" numFound="0" start="0" /> <lst name="spellcheck"> <lst name="suggestions"> <lst name="daewo"> <int name="numFound">1</int> <int name="startOffset">0</int> <int name="endOffset">5</int> <arr name="suggestion"> <str>daewoo</str> </arr> </lst> <lst name="laceti"> <int name="numFound">1</int> <int name="startOffset">6</int> <int name="endOffset">12</int> <arr name="suggestion"> <str>lacetti</str> </arr> </lst> <str name="collation">daewoo lacetti</str> </lst> </lst>
- ?q=cadilac+brogham&spellcheck.q=cadilac+brogham
<result name="response" numFound="0" start="0" /> <lst name="spellcheck"> <lst name="suggestions"> <lst name="cadilac"> <int name="numFound">1</int> <int name="startOffset">0</int> <int name="endOffset">7</int> <arr name="suggestion"> <str>cadillac</str> </arr> </lst> <lst name="brogham"> <int name="numFound">1</int> <int name="startOffset">8</int> <int name="endOffset">15</int> <arr name="suggestion"> <str>brougham</str> </arr> </lst> <str name="collation">cadillac brougham</str> </lst> </lst>
- ?q=ford+kapri& spellcheck.q=?q=ford+kapri
<result name="response" numFound="0" start="0" /> <lst name="spellcheck"> <lst name="suggestions"> <lst name="kapri"> <int name="numFound">1</int> <int name="startOffset">5</int> <int name="endOffset">10</int> <arr name="suggestion"> <str>capri</str> </arr> </lst> <str name="collation">ford capri</str> </lst> </lst>
- ?q=maseratti+coupe&spellcheck.q=?q=maseratti+coupe
<result name="response" numFound="0" start="0" /> <lst name="spellcheck"> <lst name="suggestions"> <lst name="maseratti"> <int name="numFound">1</int> <int name="startOffset">0</int> <int name="endOffset">9</int> <arr name="suggestion"> <str>maserati</str> </arr> </lst> <str name="collation">maserati coupe</str> </lst> </lst>
Mechanizm spellcheck zadziałał dla naszych przypadków perfekcyjnie, poprawiając błędnie wpisane wyrazy i generując poprawne zapytanie. W dwóch ostatnich przypadkach (4,5) możemy zaobserwować że mechanizm nie wygenerował podpowiedzi dla poprawnie wpisanych wyrazów (4 – ford, 5 – coupe) lecz wykorzystał je do złożenia poprawnego zapytania (collation).
Podsumowanie
Nasz silnik wyszukiwania został wzbogacony o funkcjonalność sprawdzania poprawności wpisanej frazy. Zostało nam czekać na opinie klientów … i być może jakieś uwagi.