Wiele wody upłynęło w Wiśle odkąd ostatni, prawdziwy post pojawił się na solr.pl. Takie z krwi i kości, opisujący jakiś problem i sugerujący rozwiązanie. Mówią, że lepiej późno, niż wcale, więc nadszedł ten dzień, kiedy znów coś publikujemy 🙂 Wracamy dzisiaj do tematu autocomplete opartej o suggestery, faceting lub n-gramy. Ta sama funkcjonalność, różne podejścia, różne metody realizacji.
Dzisiaj przyjrzymy się bardziej rozbudowanej funkcjonalonści autocomplete – takiej, która jest w stanie poradzić sobie ze znakami specjalnymi lub inaczej, znakami niewystępującymi w standardowym zbiorze ASCII.
Autocomplete ze znakami specjalnymi
Załóżmy, że nasze dokumenty, na podstawie których chcemy generować wyniki autocomplete, mają tylko dwa pola – identyfikator i pole name. W polu name możemy napotkać znaki specyficzne dla danego języka, np. takie jak ż, ć lub ę w języku polskim. Co byśmy chcieli to umożliwić generowanie autocomplete dla osób, które nie posiadają klawiatury umożliwiającej wpisanie takich znaków, badź locale, które to umożliwiają w systemie. Zakładamy, iż mamy zaindeksowane następujące dokumenty:
{"id":1, "name":"Pośrednictwo nieruchomości"} {"id":2, "name":"Posadowienie budynków"} {"id":3, "name":"Posocznica"}
Same nazwy nie są znaczące, ważne jest co chcemy osiągnąć. Chcielibyśmy dostać wszystkie trzy dokumenty wpisując pos lub poś. Czy jest to możliwe? Tak i za chwilę zobaczymy, jak to zrobić.
Przygotowanie konfiguracji kolekcji
Zacznijmy od pliku konfiguracyjnego schema.xml i definicji pól i ich typów. Kompletnie zignorujemy w tym wypadku wyszukiwanie pełno tekstowe i skoncentrujemy się tylko i wyłącznie na funkcjonalności autocomplete. Dodatkowo zakładamy, iż chcemy zwrócić zawsze całą wartość pola name, jeżeli tylko trafimy w jakikolwiek term w polu name. Definicja naszych pól w pliku schema.xml wygląda następująco:
<field name="id" type="int" indexed="true" stored="true" required="true" multiValued="false" /> <field name="name" type="string" indexed="false" stored="true" multiValued="false" /> <field name="name_ac" type="text_ac" indexed="true" stored="false" multiValued="false" />
Mamy więc pole id, które jest typu int oraz pole name, które używane jest tylko w celu wyświetlania danych. Pole name_ac to te, z za pomocą którego będziemy generować podpowiedzi autocomplete. Aby ręcznie nie wypełniać pola name_ac skorzystamy z tzw. copyField, który przez analizą skopiuje dane z jednego pola do drugiego (umieszczamy to także w pliku schema.xml):
<copyField source="name" dest="name_ac" />
Typ pola name_ac oparty zostanie o mechanizm ngram, czyli przyrostowe generowanie coraz dłuższego przedrostka wartości występującej w tym polu. Do usunięcia znaków pochodzących spoza standardowej tablicy ASCII skorzystamy z filtra solr.ASCIIFoldingFilterFactory. Oczywiście, filtra potrzebujemy zarówno podczas indeksowania, jak i podczas analizy zapytań. Zatem definicja typu text_ac wyglądać będzie następująco:
<fieldType name="text_ac" class="solr.TextField" positionIncrementGap="100"> <analyzer type="index"> <tokenizer class="solr.KeywordTokenizerFactory"/> <filter class="solr.LowerCaseFilterFactory"/> <filter class="solr.ASCIIFoldingFilterFactory"/> <filter class="solr.EdgeNGramFilterFactory" minGramSize="1" maxGramSize="100" /> </analyzer> <analyzer type="query"> <tokenizer class="solr.KeywordTokenizerFactory"/> <filter class="solr.LowerCaseFilterFactory"/> <filter class="solr.ASCIIFoldingFilterFactory"/> </analyzer> </fieldType>
Jak widać jedyną różnicą pomiędzy analizą podczas indeksowania, a tą podczas zapytania jest wykorzystanie filtra solr.EdgeNGramFilterFactory w trakcie indeksowania. Powoduje on przyrostowe generowanie przedrostka i umieszczenie tych wartości w danym polu. Filtra tego nie potrzebujemy podczas zadawania zapytań.
Czas testowania
Aby przetestować to co zrobiliśmy uruchomimy Solr w wersji SolrCloud z wbudowanym ZooKeeperem za pomocą następującego polecenia:
$ bin/solr start -c
Następnie prześlemy konfigurację zawierającą wszystkie nasze zmiany do ZooKeepera używając następującego polecenia:
$ bin/solr zk upconfig -z localhost:9983 -n autocomplete -d /home/config/autocomplete/conf
Jedyne o czym należy pamiętać, aby powyższe polecenie zadziałało to stworzenie konfiguracji (lub jej pobranie z naszego konta Github – konfiguracja), a następnie umieszczenie jej w odpowiednim katalogu.
Następnie tworzymy kolekcję za pomocą bardzo prostego polecenia:
$ curl 'localhost:8983/solr/admin/collections?action=CREATE&name=autocomplete&numShards=1&replicationFactor=1&collection.configName=autocomplete'
Po poprawnym wykonaniu polecenia, możemy zaindeksować testowe dokumenty z początku wpisu w następujący sposób:
$ curl "http://localhost:8983/solr/autocomplete/update?commit=true" -H 'Content-type:application/json' -d '[ {"id":1, "name":"Pośrednictwo nieruchomości"}, {"id":2, "name":"Posadowienie budynków"}, {"id":3, "name":"Posocznica"} ]'
Zadajmy więc dwa zapytania, jedno z literą ś, a drugie z literą s i porównajmy rezultaty. Pierwsze zapytanie wygląda następująco:
http://localhost:8983/solr/autocomplete/select?q.op=AND&defType=edismax&qf=name_ac&fl=id,name&q=pos
Drugie wygląda natomiast tak:
http://localhost:8983/solr/autocomplete/select?q.op=AND&defType=edismax&qf=name_ac&fl=id,name&q=pos
W obu przypadkach korzystamy z parsera Extended DisMax, spójnika logicznego AND (parametr q.op) i ustawiamy pole po którym chcemy szukać na name_ac za pomocą parameteru qf. Dodatkowo mówimy Solr, iż chcemy aby zwrócone zostały tylko pola id oraz name za pomocą parametru fl.
W przypadku obu zapytań wyniki są jednakowe i wyglądają następująco:
<?xml version="1.0" encoding="UTF-8"?> <response> <lst name="responseHeader"> <bool name="zkConnected">true</bool> <int name="status">0</int> <int name="QTime">0</int> <lst name="params"> <str name="q">poś</str> <str name="defType">edismax</str> <str name="qf">name_ac</str> <str name="fl">id,name</str> <str name="q.op">AND</str> </lst> </lst> <result name="response" numFound="3" start="0"> <doc> <int name="id">1</int> <str name="name">Pośrednictwo nieruchomości</str></doc> <doc> <int name="id">2</int> <str name="name">Posadowienie budynków</str></doc> <doc> <int name="id">3</int> <str name="name">Posocznica</str></doc> </result> </response>
Jak widać opisana metoda działa 🙂