Implementacja wyszukiwania w aplikacji serverless, czyli jak wykorzystać Azure Functions i Azure Search?

Azure oferuje nam wiele różnych często bardzo zaawansowanych usług oraz możliwość „żaglowania” nimi praktycznie w nieograniczony sposób. Jest to bardzo duży benefit w sytuacji gdy musimy zaimplementować tytułowe wyszukiwanie w aplikacji serverless. Ale po kolei…

W aplikacji jaką rozwijamy w Altkom Software & Consulting pojawiło się wymaganie wyszukiwania danych po dowolnych atrybutach encji np. Tytuł, Miejsce itp. Początkowo rozważałem wykorzystanie jakiegoś mechanizmu budowanego „from scratch” ze względu na koszty. Jednak przeglądając ofertę usług Azure trafiłem na Azure Search w skrócie jest to usługa pozwalająca indeksować praktycznie dowolne źródła danych które są dostępne w naszej subskrypcji Azure. Idąc dalej indeksowanie tych danych pozwala na późniejsze ich przeszukiwanie oraz zwrócenie wyników w aplikacji klienckiej.

Serverless

Wybraliśmy usługę Azure Functions głównie ze względu na minimalizację kosztów tworzonego rozwiązania. W związku z tym czy Azure Search nie będzie za drogim rozwiązaniem?

Cennik tej usługi nie wygląda zbyt zachęcająco przynajmniej na pierwszy rzut oka…

WarstwaFreeBasicS1S2S3L1L2
Cena$0~$73~$245~$1000~$2000~$3000~$6000

Warto zaznaczyć, że warstwy L1 oraz L2 są dostępne jako Preview. Wracając do kosztów dla startup który jeszcze nie przynosi „dużych” zysków opłacanie co miesiąc rachunku w postaci > $70 może być dużym kosztem na tle całego rozwiązania. Stąd też wybór Azure Functions w których płacimy tylko za faktyczne wykorzystanie. Drogi Microsoft czekam na Azure Search w wersji serverless.

Obejściem tego problemu okazuje się wykorzystanie warstwy Free oczywiście przy założeniach, że ponosimy ryzyko braku SLA, oraz ograniczenia jakie nakłada ta warstwa. Nasze rozwiązanie nie posiada jeszcze bardzo dużego wolumenu danych przy czym czyste indeksowane dane zamykały się w okolicach 1-2MB.

A więc wprowadzenie mamy już za nami czas na mięso…

Jak to wszystko poustawiać i połączyć?

Zacznijmy od konfiguracji usługi Azure Search w tym celu logujemy się do Portalu Azure i szukamy naszej usługi wpisując w wyszukiwaniu lub też znajdując usługę ręcznie.

Wyszukiwanie usługi Azure Search

W kolejnym kroku musimy skonfigurować parę parametrów np. Pricing tier, oraz adres pod jakim możemy się komunikować z naszą usługą.

Tworzenie usługi Azure Search

Jeżeli wybierzemy bardziej zaawansowaną warstwę możemy ustawić również opcje dotyczące skalowania naszej usługi lub też replikacji danych.

Ustawianie skalowania oraz replikacji w usłudze Azure Search

Te ustawienia pozwalają nam zwiększyć szybkość oraz niezawodność działania naszej aplikacji/systemu. W naszym przypadku jesteśmy w stanie ponieść takie ryzyko i dodać je do naszej listy „długu technologicznego”.

W kolejnym kroku przechodzimy do podsumowania oraz utworzenia naszej usługi. Warto również zaznaczyć, że w tym kroku możemy pobrać ARM template który możemy wykorzystać do automatyzacji budowania naszego środowiska.

Podsumowanie tworzenia usługi Azure Search

Konfiguracja usługi Azure Search

A więc mamy już przygotowaną usługę należy teraz dodać do niej jakieś dane które będą podlegać indeksacji. W tym celu w górnym pasku klikamy „Import data”.

Główne menu usługi Azure Search

W kolejnym kroku po rozwinięciu listy „Existing data source” widzimy źródła danych z jakich możemy korzystać w ramach Azure Search.

Źródła danych dostępne w Azure Search

Co w sytuacji gdy korzystamy np. z MySQL na Azure? Rozwiązanie jakie przychodzi mi na szybko do głowy to replikacja potrzebnych danych np. do Azure Table Storage i odwoływanie się tylko po detale do MySQL.

W kolejnych krokach konfigurujemy różne parametry zależnie od wybranego połączenia w naszym wypadku było to Azure Table Storage. Naszym źródłem jest tabela „events” w naszym Azure Storage.

Źródło danych w Azure Search

Kolejnym krokiem jest konfiguracja indeksu oraz indexer-a który będzie uruchamiany cyklicznie na naszym źródle.

Konfiguracja indeksu wygląda następująco.

Tworzenie indeksu w Azure Search

Pozwala ona nam na stworzenie oprócz takich oczywistych rzeczy jak nazwa indeksu również wybór klucza oraz ustawienie parametrów poszczególnych kolumn.

Nasz przykładowy indeks prezentował się następująco.

Rozmiar przykładowego indeksu
Struktura przykładowego indeksu

Jak widzicie można ustawić sporo parametrów dotyczących struktury naszego indeksu oraz późniejszego wyszukiwania danych lub też pobierania szczegółów z innego źródła danych.

Kolejnym ważnym elementem Azure Search jest index-er który nie jest niczym innym jak procesem działającym w tle (demonem) który jest uruchamiany cyklicznie na naszym źródle danych. Poniżej przykład konfiguracji takiego demona.

Konfiguracja indexer-a

Kolejnymi elementami jakimi możemy sterować w usłudze Azure Search jest wyznaczanie „wag” dla poszczególnych danych w naszym indeksie. Tym elementem nie będziemy się zajmować w tym artykule ponieważ wykracza on poza jego zakres.

Jak to połączyć z Azure Functions?

Jako, że w naszym rozwiązaniu korzystamy z Azure Functions to na nich też się głównie skupię aczkolwiek połączenie z innymi komponentami tworzonymi w .NET lub .NET Core będzie wyglądało analogicznie.

W wypadku Azure Functions moglibyśmy skusić się na stworzenie własnego Binding do Azure Search ale znacznie prościej i elastyczniej jest skorzystać z gotowego SDK. Biblioteki SDK są dostępne w postaci paczek NuGet które wystarczy po prostu dodać do naszego projektu.

Przykładowa funkcja może wyglądać następująco. Poniżej opiszę poszczególne elementy specyficzne dla Azure Search.

    public class LookupEventsFunction
    {
        private static string SearchServiceName = Environment.GetEnvironmentVariable("SearchServiceName");
        private static string ApiKey = Environment.GetEnvironmentVariable("SearchServiceApiKey");

        private static ISearchIndexClient _searchIndexClient;
        private const string IndexName = "eventstable-index";

        public LookupEventsFunction()
        {
            _searchIndexClient = CreateSearchIndexClient();
        }

        [FunctionName("LookupEventsFunction")]
        public async Task<IActionResult> Run(
            [HttpTrigger(AuthorizationLevel.Function, "get", Route = "events/search={searchPhrase}")] HttpRequest req,
            ExecutionContext context,
            string searchPhrase,
            ILogger log)
        {
            log.LogInformation($"Start trigger {nameof(context.FunctionName)}");

            var parameters = new SearchParameters()
            {
                Select = new[] { "RowKey", "Title", "Address", "Place", "Country" }
            };
            var resultObject = await _searchIndexClient.Documents.SearchAsync(searchPhrase, parameters);
            if (resultObject.Results.Any())
                return new OkObjectResult(resultObject.Results);
            else
                return new BadRequestResult();
        }

        private SearchIndexClient CreateSearchIndexClient()
        {
            return new SearchIndexClient(SearchServiceName, IndexName, new SearchCredentials(ApiKey));
        }
    }
Environment.GetEnvironmentVariable("SearchServiceName");
Environment.GetEnvironmentVariable("SearchServiceApiKey");

Są to elementy pobierające z konfiguracji nazwę naszego serwisu oraz jego API key. Oba te elementy możemy znaleźć w „Portalu Azure” pierwszy element możemy znaleźć w menu „Properties” drugi natomiast w „Keys” gdzie należy najlepiej dodać nowy klucz.

Nazwa usługi
API key

Idąc dalej poniższy element odpowiada za utworzenie klienta do naszej usługi.

return new SearchIndexClient(SearchServiceName, IndexName, new SearchCredentials(ApiKey));

Samo wywołanie usługi i pobranie z niej danych możemy załatwić 3-4 linijkami kodu

var parameters = new SearchParameters() 
{
  Select = new[] { "RowKey", "Title", "Address", "Place", "Country" }};
var resultObject = await _searchIndexClient.Documents.SearchAsync(searchPhrase, parameters);

Zabrakło jeszcze jednej ważnej rzeczy w tym wszystkim pakietu NuGet Microsoft.Azure.Search oraz odpowiednich namespace.

using Microsoft.Azure.Search;
using Microsoft.Azure.Search.Models;

Podsumowanie

Jak widzicie powiązanie Azure Functions z Azure Search i zbudowanie funkcjonalności wyszukiwania jest bardzo prostym zadaniem które można szybko zrealizować. Wiadomo bardziej eleganckim podejściem byłoby stworzenie własnego Binding ale tam gdzie czas jest najważniejszym wyznacznikiem trzeba iść na kompromisy.

Cały kod źródłowy tego przykładu jest dostępny na GitHub. Mile widziane Pull Request może dowiem się czegoś ciekawego przy okazji 😊.

Jak zawsze zapraszam do komentowania zawsze jest to jakaś nauka dla mnie i dla was. Również wspólnymi siłami możemy dojść do jakiś ciekawych wniosków 🤯.

A wy jak podchodzicie do budowy wyszukiwania w waszych aplikacjach/systemach?


Chcesz być na bierząco z najnowszymi zmianami na blog i newsami ze świata mobilnego, oraz chmury?

Zapisz się na mój newsletter zajmie to tylko parę sekund.

Share