Jakiś czas temu obiecałem napisanie kilku słów o wykorzystaniu synonimów w usłudze Azure Search. To temat na prawdę ciekawy, ponieważ, poprzez wykorzystanie tego mechanizmu, możemy znacząco poprawić wyniki wyszukiwania w naszym serwisie. Dlatego złap teraz za kubek ciepłego kakałka i zapraszam Cie do lektury.

Czym jest synonim?

Zacznijmy od encyklopedycznej definicji słowa:

Według Wikipedii:

Synonim (gr. synōnymos ‘równoimienny’) – wyraz lub dłuższe określenie równoważne znaczeniowo innemu, lub na tyle zbliżone, że można nim zastąpić to drugie w odpowiednim kontekście (auto – samochód). Synonimia może dotyczyć konstrukcji składniowych (mówić wiersz – mówić wierszem), form morfologicznych (profesorowie – profesorzy) i leksemów.

Natomiast Słownik Języka Polskiego opisuje go jako:

1.«każdy z pary wyrazów mających takie samo znaczenie»
2.«symbol lub odpowiednik czegoś»
W skrócie, synonim jest to inne określenie na tą samą rzecz. Tak przynajmniej ja bym podsumował ;).
Ale jakie to może mieć zastosowanie w kwestii wyników wyszukiwania? Zaglądając do dokumentacji usługi Azure Search znajdujemy całkiem fajny przykład. Przyjmijmy, że tworzymy wyszukiwarkę wszystkich państw świata. Nasz użytkownik spróbuje wyszukać Stany Zjednoczone. Teraz pytanie: co trzeba wpisać w wyszukiwarkę: US, USA, United States of America, U.S. ? To wszystko synonimy, dlatego wpisane powinny zwrócić ten sam wynik wyszukiwania. Niestety, w większości wyszukiwarek jest jak na maturze z Języka Polskiego – nie wstrzelisz się w klucz, to po ptakach.

Zastosowanie synonimów w usłudze Azure Search

Liczę na to, że wstęp dość klarownie wytłumaczył, do czego potrzebne nam synonimy. Do implementacji wykorzystamy usługę Azure Search, która udostępnia nam to tego mechanizm.

W ramach dzisiejszego przykładu będziemy tworzyć nową domenę, dotyczącą piłki nożnej. Na naszym serwisie będzie dostępna wyszukiwarka zawodników. W takim wypadku uproszczona encja zawodnika wygląda następująco:

public class Player
{
    public string Id { get; set; } = Guid.NewGuid().ToString();
    public string FirstName { get; set; }
    public string LastName { get; set; }
    public string Position { get; set; }
}

Otrzymujemy taki mały worek na dane. Jakie możemy zastosować synonimy? Spróbujmy zaimplementować dwa przypadki:

  1. Kiedy użytkownik będzie chciał wyszukać frazę “orzeł polski” to powinien otrzymać Roberta Lewandowskiego jako wynik
  2. Kiedy użytkownik będzie chciał wyszukać frazę “defenders” to powinien otrzymać listę wszystkich obrońców dostępnych w bazie danych

Oczywiście nie przechowujemy danych jak “alias” czy “uogólniona pozycja”, dlatego potrzebny nam jest mechanizm synonimów.

Do implementacji wykorzystamy gotowe klasy z mojego poprzedniego artykułu. Jeżeli ich nie pamiętasz (ja bym raczej nie pamiętał, więc luz, rozumiem), bądź go nie czytałaś / czytałeś to zapraszam tutaj.

Budujemy kolekcję zawodników

Przyszedł czas by zbudować kolekcję zawodników. Nie dorabiając bez sensu pracy ograniczmy się do 4.

var players = new List<Player>
{
    new Player { Position = "CF", FirstName = "Robert", LastName = "Lewandowski" },
    new Player { Position = "GK", FirstName = "Wojciech", LastName = "Szczęsny"},
    new Player { Position = "CB", FirstName = "Kamil", LastName = "Glik"},
    new Player { Position = "RB", FirstName = "Łukasz", LastName = "Piszczek"}
};

Definicja Synonimów

Mając naszą encję i kolekcję zawodników przechodzimy do zdefiniowania synonimów. Używamy klasy SynonymMap dostępnej w paczce Microsoft.Azure.Search.

W instancji tej klasy musimy zdefiniować 3 pola

  • Name -> Nazwę, która posłuży jako identyfikator mapy synonimów
  • Format -> Rodzaj formatu zapisywania synonimów. Obecnie wspierany jest ten z Apache Solr
  • Synonyms -> Zapis synonimów w wybranym powyżej formacie

Format zapisu synonimów Solr – Szybki Start

Według specyfikacji Apache Solr mamy dwa rodzaje zapisu synonimów: równoważny (equivalent) i sprecyzowany (explicit).

W zapisie równoważnym podajemy słowa po przecinku np.:

Zwierzak, Pies, Kot

Dla szukanej frazy “zwierzak”, otrzymamy zapytanie Zwierzak OR Pies OR Kot. Działa tak samo dla każdego słowa w tej konfiguracji.

W zapisie sprecyzowanym stosujemy notację strzałki “=>”. Wyrażenie znalezione po lewej stronie zostanie nadpisane wyrażeniem po prawej stronie np.

Pies, Szczeniak => dogo

Czyli dla poszukiwanej frazy “Pies” otrzymamy wynik dla frazy “dogo”. W tej notacji wszystko działa zgodnie z kierunkiem strzałki, tzw. nie osiągniemy zastąpienia słowa “Szczeniak” przez słowo “Pies”.

Definicja map synonimów w kodzie

Do utworzenia dwóch wcześniej wspomnianych przez mnie map synonim wykorzystamy następujący kod:

var eagleSynonyms = new SynonymMap
{
    Name = "eagle",
    Format = "solr",
    Synonyms = "orzeł polski, Lewandowski"
};

var defendersSynonyms = new SynonymMap
{
    Name = "defenders",
    Format = "solr",
    Synonyms = "defenders => CB, LB, RB"
};

Dla naszego “orła polskiego” zastosowaliśmy notację równoważną, ponieważ nie chcemy mieć rozróżnienia czy ktoś wyszukuje “orzeł polski” czy “Lewandowski”.  Natomiast dla obrońców zastosowaliśmy notację sprecyzowaną. Wyszukiwanie poprzez frazę”defenders” powinno nam wyświetlić wszystkich obrońców,  jednak dla wyszukiwania np. “LB” zależy nam na bardziej sprecyzowanym wyniku.

Dodanie map synonimów do klienta

Mam nadzieję, że na razie wszystko wygląda zrozumiale. Jeśli nie – kurcze, mogłem to wyjaśnić inaczej. W przeciwnym wypadku – ciesze się, że dajecie radę. Oby tak dalej! Czas dodać zdefiniowane powyżej mapy synonimów do naszej usługi. Musimy rozbudować nasz serwis. Nie jakoś przesadnie. W zasadzie wystarczy jedna metoda:

public void AddSynonymsMaps(IEnumerable<SynonymMap> maps)
{
    foreach (var synonymMap in maps)
    {
        _searchServiceClient.SynonymMaps.CreateOrUpdate(synonymMap);
    }
}

Wywołujemy ją w następujący sposób:

service.AddSynonymsMaps(new [] { eagleSynonyms, defendersSynonyms });

Synonimy są już dostępne do zdefiniowania w Indeksie.

Utworzenie Indeksu

Podczas tworzenia indeksu możemy podać mapy synonimów dla każdego pola z osobna.

var indexModel = new Index
{
    Name = config.IndexName,
    Fields = new List<Field>
    {
        new Field("Id", DataType.String)
        {
            IsKey = true, IsRetrievable = true, IsSearchable = true, IsSortable = true
        },
        new Field("FirstName", DataType.String)
        {
            IsSearchable = true, IsRetrievable = true
        },
        new Field("LastName", DataType.String)
        {
            IsSearchable = true, IsRetrievable = true,
            SynonymMaps = new [] { "eagle" }
        },
        new Field("Position", DataType.String)
        {
            IsSearchable = true, IsRetrievable = true,
            SynonymMaps = new [] { "defenders" }
        }
    }                
};

Wykorzystujemy do tego identifikator, którego użyliśmy w polu Name klasy SynonymMap. Pozwala nam to na niezależność działania synonimów w kontekście tylko jednej kolumny.

Przeszukiwania

To by było na tyle jeżeli chodzi o implementacje. Wystarczy przetestować poprawność działania.

Kiedy spróbujemy wyszukać obrońców to otrzymujemy zawodników z pozycji CB i RB. Super! Działa poprawnie.

Przy frazie “orzeł polski” musimy wykorzystać drobny hak. Ponieważ jest to wyrażenie składające z dwóch słów trzeba je zamknąć w cudzysłów. W innym wypadku Azure Search będzie szukać osobno “orzeł” i “polski”, co nie da nam oczekiwanego wyniku.

Podsumowanie

To by było na tyle, jeżeli chodzi o wykorzystanie synonimów w usłudze Azure Search. Oczywiście wszystkie poruszone poruszone przykłady da się wykonać używając zwykłych request-ów przez REST API, jednak mi osobiście przyjemniej się pracuje na kodzie.

Jest to ostatni post w tym roku, dlatego wszystkim wam życzę w 2019 jak najmniej bugów, ciekawych projektów i zawsze ciepłego kakałka gdy tylko będziecie mieli na nie ochotę.

No i jak zwykle: dzięki za dotrwanie do końca artykułu 🙂

Do Następnego!

Cześć