Znacie ten typ ludzi, którzy z całych sił próbują naprawić popełniony błąd i chcą zjeść pączka średnio raz na 3 dni? To nie ja. Ja wole jeść pączki częściej. A co do poprawy – obiecałem, że będzie więcej artykułów dotyczące Azure? No i to kolejny artykuł o usłudze Azure Search.
Kto by pomyślał że powstanie z tego taka mała seria artykułów? Na pewno nie ja. Bardzo dziękuje za odzew dotyczący tych wpisów. Motywuje mnie to do dalszej, intensywnej pracy na blogiem.
W poprzednich artykułach skupiłem się na działaniu samej usługi i możliwościach oferowanych przez portal Azure. Powiedzmy sobie szczerze – prawdziwych programistów jak wy nie interesuje co można “wyklinać” w jakimś tam GUI, prawda? My potrzebujemy kodu, mięcha! Dlatego chciałbym zabrać was w małą wycieczkę po Azure Search .NET SDK.
Dodanie SDK do projektu .NET Core
Nie chcąc mieszać wam w głowach nadal pozostaniemy w kontekście książek. Napiszemy prostą aplikację konsolową, która będzie dodawać i przeszukiwać dokumenty w index-ie. Wykorzystamy do tego paczkę Microsoft.Azure.Search
. Zapewne wiecie jak, za pomocą dotnet CLI, dodać ją do projektu, jednak jakby ktoś z was zapomniał to wstawiam gotowy kod 🙂
$ dotnet add package Microsoft.Azure.Search -v 5.0.0 $ dotnet restore
Wersja biblioteki jest na sztywno, ponieważ z wersją 5.0.3 miałem problemy na swoim komputerze. Rzucało mi dziwnym błędem związanym z klientem HTTP. Zakładam, że dość szybko zostanie to naprawione, na ten moment muszę was przed tym ostrzec 😉
Konfiguracja Azure Search .NET SDK
Jak to zazwyczaj bywa z usługami w chmurach musimy mieć jakąś konfigurację pozwalającą na odpytywanie się usługi. Nie inaczej jest z Azure Search. Tutaj potrzebujemy trzech rzeczy:
- Nazwę indeksu, do którego mają być kierowane zapytania
- Nazwę usługi Search, do której mają być kierowane zapytania
- Klucz admin-a, który będzie pozwał na dostęp do usługi
Jako, że piszę aplikację w C# uruchamianą po stronie serwer-a możemy użyć klucza admina. W przypadku chęci tworzenia indeksów, aby dodawać nowe dokumenty, nawet musimy. Jeżeli zachowamy zasady bezpieczeństwa to wszystko powinno być proste i bez kłopotów. W końcu nie będzie przechowywać tak ważnego klucza po stronie aplikacji klienckiej.
Osobiście zawsze lubię zamykać takie obiekty konfiguracyjne w osobne klasy. Wtedy mogę w prosty sposób w ramach development-u wpisać na sztywno wartości. A zarazem jak aplikacja trafia na produkcję to bardzo łatwo za pomocą jakiegoś systemu CD uzupełnić tą klasę produkcyjnymi wartościami.
No to stwórzmy konfigurację. Nazwijmy ją tak tajemniczo AzureSearchConfig
. Zakładając. że nasza konfiguracja jest niezmiennikiem, możemy ją napisać w poniższy sposób.
public class AzureSearchConfig { public string IndexName { get; } public string SearchName { get; } public string SearchKey { get; } public AzureSearchConfig(string indexName, string searchName, string searchKey) { IndexName = indexName; SearchName = searchName; SearchKey = searchKey; } }
Encja i dokument
Mamy już konfigurację. Teraz potrzebujemy klas, które będą odpowiadały naszym obiektom domenowym i dokumentom w kolekcji Azure Search. Tak jak pisałem wcześniej, nadal pozostaniemy w kontekście książek, więc nasza przykładowa encja mogłaby wyglądać następująco:
public class Author { public string First { get; set; } public string Last { get; set; } } public class Book { public string Id { get; set; } public string Title { get; set; } public string Description { get; set; } public IEnumerable<string> Tags { get; set; } public Author Author { get; set; } }
Nie ma tu nic odkrywczego. Jest jedno utrudnienie, o którym pisałem już w artykule Azure Search Spłaszczanie Struktur Danych. Mamy zagnieżdżony obiekt, z którym usługa Azure Search jeszcze sobie nie poradzi. Dlatego potrzebujemy stworzyć obiekt DTO, który będzie miał spłaszczone pole Author
.
public class BookDTO { public string Id { get; set; } public string Title { get; set; } public string Description { get; set; } public IEnumerable<string> Tags { get; set; } public string Author_First { get; set; } public string Author_Last { get; set; } }
Potrzebujemy jeszcze mechanizmu mapowania pomiędzy tymi dwoma klasami. W prawdziwym projekcie prawdopodobnie użyłbym biblioteki AutoMapper, jednak tutaj wprowadziłoby to więcej zamieszania niż pożytku. Na szybko możemy napisać następującego helper-a.
public static class Mapper { public static BookDTO BookToBookDTO(Book book) { return new BookDTO { Id = book.Id, Title = book.Title, Description = book.Description, Tags = book.Tags, Author_First = book.Author.First, Author_Last = book.Author.Last }; } public static Book BookDTOToBook(BookDTO bookDto) { return new Book { Id = bookDto.Id, Title = bookDto.Title, Description = bookDto.Description, Tags = bookDto.Tags, Author = new Author { First = bookDto.Author_First, Last = bookDto.Author_Last } }; } }
Jeszcze tylko nieśmiertelne InMemoryRepository
i cały kod związany z naszymi encjami mamy ogarnięty.
public class BookRepository { private IEnumerable<Book> _list = new List<Book> { new Book { Id = Guid.NewGuid().ToString(), Title = "Ogniem i mieczem", Description = "Lorem lipsum dolor", Tags = new List<string> { "dobre bo polskie", "ogniem", "mieczem" }, Author = new Author { First = "Henryk", Last = "Sienkiewicz" } }, new Book { Id = Guid.NewGuid().ToString(), Title = "Krzyżacy", Description = "Lorem lorem sit amend", Tags = new List<string> { "test", "krzyzacy" }, Author = new Author { First = "Henryk", Last = "Sienkiewicz" } }, new Book { Id = Guid.NewGuid().ToString(), Title = "Pan Tadeusz", Description = "Lorem lorem", Tags = new List<string> { "test", "pan tadeusz" }, Author = new Author { First = "Adam", Last = "Mickiewicz" } } }; public IEnumerable<Book> GetAll() { return _list; } }
Stworzenie serwisu do komunikacji z Azure Search
Mamy nasz obiekt konfiguracyjny i wszystkie potrzebne encję. Czas przejść do stworzenia serwisu, który będzie nam enkapsulował komunikację z usługą Azure Search. Super! W końcu będzie więcej mięcha 😉 Robimy to, aby zachować wywołania do Azure Search-a w jednym miejscu aplikacji.
Nadamy naszemu serwisowi tajemniczą nazwę AzureSearchService
. Jako zależność w konstruktorze przekażemy mu obiekt konfiguracyjny.
public class AzureSearchService { private readonly AzureSearchConfig _config; public AzureSearchService(AzureSearchConfig config) { _config = config; } }
Pora przejść do wykorzystania Azure Serach .NET SDK. Tworzymy dwa obiekty: klienta serwisu i klienta index-u. Jaka jest różnica pomiędzy tymi obiektami? Za pomocą klienta serwisu możemy zarządzać indeksami, zaś za pomocą klienta index-u możemy zarządzać dokumentami w kolekcji.
Obie instancję przypiszmy do klasy jako propertis-y.
public class AzureSearchService { private readonly AzureSearchConfig _config; private ISearchServiceClient _searchServiceClient; private ISearchIndexClient _searchIndexClient; public AzureSearchService(AzureSearchConfig config) { _config = config; _searchServiceClient = new SearchServiceClient(_config.SearchName, new SearchCredentials(_config.SearchKey)); _searchIndexClient = _searchServiceClient.Indexes.GetClient(_config.IndexName); } }
Uzupełnianie serwisu metodami
Mam nadzieje, że wszystko do tego momentu jest czytelne i zrozumiałe(na pewno suuuper ciekawe i standardowo gratuluje wszystkim, którzy mają siłę i chęci 🙂 Idąc dalej – potrzebujemy jeszcze trzech metod:
- Tworzenia indeksu,
- Dodawania dokumentów do indeksu,
- Przeszukiwanie indeksu
Na szczęście Azure Search .NET SDK posiada przyjemne w użytkowaniu API. Wystarczy kilka linijek by wszystko działało. W pierwszej kolejności zajmijmy się tworzeniem indeksu z poziomu kodu. Tworzy się go za pomocą klasy IndexModel zawartej w paczce Microsoft.Azure.Search
. Unikając tworzenia silnych wiązań pomiędzy serwisem a indexem, przyjmijmy taki obiekt w parametrach wywołania tej metody. Samą klasę IndexModel omówimy sobie w dalszej części artykułu. Dodatkowo, rzecz jasna, musimy jeszcze sprawdzić czy podany index nie jest już utworzony w usłudze. Cały kod wygląda następująco:
public void CreateIndex(Index indexModel) { if (_searchServiceClient.Indexes.Exists(_config.IndexName)) { return; } _searchServiceClient.Indexes.Create(indexModel); }
Przyszła pora by przejść do generycznej metody, która pozwoli na dodawanie dokumentów do index-u. Z pomocą Azure Search .NET SDK możemy dodać naraz aż do 1000 dokumentów. Oczywiście, im więcej będzie dokumentów w jednym batch-u, tym dłużej będzie trwało zindeksowanie wszystkich elementów.
public void Add<T>(IEnumerable<T> documents) where T : class { _searchIndexClient.Documents.Index(IndexBatch.MergeOrUpload(documents)); }
Cała metoda prezentuje się jak powyżej. Wszystkie dokumenty są wgrywane. Jeżeli podany dokument był już w kolekcji, zostaje nadpisany dzięki wykorzystaniu metody MergeOrUpload
. Chcecie poczytać więcej na ten temat? Polecam mój artykuł na temat Azure Search REST API, gdzie znajdziecie opisane wszystkie typy zapytań.
Pozostaje nam trzecia, i na obecną chwilę ostatnia, metoda do napisania serwisu – przeszukiwanie kolekcji. Jak się pewnie domyślacie Azure Search pozwala nam parametryzować wyszukiwanie za pomocą kilku propertis-ów takich jak OrderBy
, Skip
, Top
. Metoda będzie przyjmować, oprócz samej szukanej frazy query
,obiekt klasy SearchParameters
. O obiekcie, tak samo jak w przypadku IndexModel,
napiszę więcej w późniejszej części artykułu.
public DocumentSearchResult<T> Search<T>(string query, SearchParameters sp) where T : class { return _searchIndexClient.Documents.Search<T>(query, sp); }
Oczywiście ta metoda będzie zwracać wyniki 🙂 Wszystko co usługa nam zwróci Azure Search .NET SDK zamyka nam w klasie DocumentSearchResult
.
Wykorzystanie Serwisu
Tworzenie Index-u
Koniec przygotowań! Zacznijmy korzystać z nowo stworzonego serwisu. Stwórzmy prosty index odzwierciadlający nasz obiekt bookDto. Będziemy potrzebowali wspomnianej wcześniej klasy IndexModel
.
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("Title", DataType.String) { IsRetrievable = true, IsSearchable = true, IsSortable = true }, new Field("Description", DataType.String) { IsRetrievable = true, IsSearchable = true, IsSortable = true }, new Field("Author_First", DataType.String) { IsRetrievable = true, IsSearchable = true, IsSortable = true }, new Field("Author_Last", DataType.String) { IsRetrievable = true, IsSearchable = true, IsSortable = true }, new Field("Tags", DataType.Collection(DataType.String)) { IsRetrievable = true, IsSearchable = true, IsFilterable = true, IsFacetable = true } } }; service.CreateIndex(indexModel);
Najprostsza implementacja składa się z dwóch elementów: nazwy index-u i listy pól, jakie ma zawierać. Oczywiście sam IndexModel
ma więcej możliwych ustawień, ale to temat na kolejne artykuły. Każde pole składa się z nazwy, typu danych i flag, którymi definiujemy ustawienia dla Pola. Potrzebujesz więcej informacji na temat flag? Zapraszam do przeczytania mojego artykułu Azure Search Wyszukiwarka w Mgnieniu Oka, gdzie jest to skrupulatnie opisane. Na koniec przykładu wywołujemy stworzony wcześniej serwis, który utworzy nam nowy index w usłudze.
Dodawanie dokumentów do kolekcji
Pamiętacie jeszcze te wszystkie encję, dto, repozytoria stworzone na początku artykułu? Wykorzystajmy to, by dodać dokumenty do naszej kolekcji. Wystarczy pobrać wszystkie elementy z naszego repozytorium, zrobić mapowanie Book -> BookDTO i możemy je zindeksować!
var documents = new BookRepository().GetAll().Select(x => Mapper.BookToBookDTO(x)).ToList(); service.Add(documents);
Szybki podgląd w portalu Azure czy to zadziałało:
Działa! Super 🙂
Przeszukiwanie kolekcji dokumentów
Skoro mamy już świeżo dodane dokumenty do naszego index-u, możemy przejść do przeszukiwania. W tym celu wykorzystamy ostatnią metodę zdefiniowaną w naszym serwisie. Pobranie wszystkich elementów wymaga zapytania:
var response = service.Search<BookDTO>("*", new SearchParameters()); IEnumerable<Book> books = response.Results.Select(x => Mapper.BookDTOToBook(x.Document)).ToList();
Oczywiście, po pobraniu elementów, musimy je przemapować na nasz obiekt domenowy. W tym przypadku się to trochę komplikuje, ponieważ Azure Search dostarcza nam obiekt BookDTO, opakowany w ich klasy takie jak DocumentSearchResult
i SearchResult
. Nadal można to ogarnąć za pomocą naszego Mapper-a i jednej linijki LINQ. Abyście mogli zobaczyć jak to wygląda na żywo, załączam wam screen z Rider-a.
W tym zapytaniu pojawiła się po raz kolejny klasa SearchParameters
, o której jeszcze nie powiedziałem ani słowa. Służy do definiowania parametrów wyszukiwania i jest nam dostarczona przez paczkę Microsoft.Azure.Search
. Pozwala nam nie tylko na przeszukiwanie, kategoryzację czy filtrowanie wyników ale też, za pomocą metod Skip
i Top,
na paginację wyników.
Jeżeli, dla przykładu, potrzebowalibyśmy wyszukać wszystkie książki oznaczone tagiem test
i posortować je po imieniu autora, to moglibyśmy skonstruować następujące zapytanie:
var response = service.Search<BookDTO>("*", new SearchParameters { OrderBy = new List<string> { "Author_First asc" }, Filter = "Tags/any(t: t eq 'test')" });
Wygląda na skomplikowane? Chyba nie 🙂 A jeśli nawet – nie bójcie się, przejdziemy przez te parametry w kolejnych artykułach na temat tej usługi 🙂
Podsumowanie
Nie wiem dlaczego te artykułu na temat Azure Search-a, tak bardzo mi się rozrastają. Wyszło jak wyszło 😛 Mam nadzieje, że nie macie mi za złe, chęć przekazania wiedzy silniejsza od logiki. Za tydzień możecie się spodziewać kolejnego artykułu o Azure 🙂 Jeżeli chcecie być zawsze na bieżąco – zapiszcie się na newsletter. Można to uczynić na dole strony. Tak, wiem że ten formularz nie wygląda zbyt zachęcająco, niedługo to się zmieni. Maile są ładniejsze, zapewniam 🙂
Jak zwykle dzięki za przeczytanie artykułu.
Do Następnego!
Cześć!
PS. Spodobał Ci się ten artykuł?