Autocomplete i znaki specjalne

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 🙂

Dodaj komentarz

Twój adres e-mail nie zostanie opublikowany. Wymagane pola są oznaczone *