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 *

We use cookies to personalise content and ads, to provide social media features and to analyse our traffic. We also share information about your use of our site with our social media, advertising and analytics partners. View more
Cookies settings
Accept
Privacy & Cookie policy
Privacy & Cookies policy
Cookie name Active
Save settings
Cookies settings