Azure Search. Jak stworzyć zaawansowaną wyszukiwarkę w kilku krokach (cz.2)
W poprzedniej części artykułu opisałem przykładowy scenariusz: chcieliśmy stworzyć przeglądarkę, która będzie umożliwiała podpowiadanie, wyszukiwanie po kilku polach, filtrowanie, wypisywanie wyników nawet, gdy użytkownik popełni dany błąd. Mogłaby też przeszukiwać pliki tekstowe i zdjęcia. Pisząc to samodzielnie zajęłoby to nam bardzo dużo czasu, dlatego skorzystaliśmy z Azure i usługi Search. W drugiej części artykułu omówimy scenariusz, w którym tworzymy wyszukiwarkę dla strony internetowej z wiadomościami, w której bazie są tytuły artykułów, oraz treści tych artykułów trzymane są na Blobie.
Kacper Świsłocki. .NET Developer w białostockiej firmie Elastic Cloud Solutions. Młody programista, początkujący prelegent. Występował m.in. podczas wydarzenia Śląska Grupa Microsoft Meetup, gdzie opowiadał o tym, jak stworzyć zaawansowaną wyszukiwarkę za pomocą Azure Search.
Tworzymy projekt w .Necie – aplikację konsolową, której zadaniem będzie konfiguracja naszej usługi. Aby móc odwoływać się do naszej usługi potrzebujemy jej nazwy, oraz klucza administratora. Musimy również zainstalować SDK – Microsoft.Azure.Search do naszego projektu. (Poniżej znajdują się tylko kody najważniejszych funkcji. Pełny kod będzie znajdować się na repozytorium: https://github.com/swislockikacper/AzureSearchConfiguration).
Tworzymy dwa źródła danych. Jedno do informacji znajdujących się w bazie, drugie do plików znajdujących się na blobie.
private void CreateContentDataSource() { Console.WriteLine("Creating data source..."); var dataSource = DataSource.AzureSql( name: contentDataSourceName, sqlConnectionString: dbConnectionString, tableOrViewName: contentTable); try { searchClient.DataSources.CreateOrUpdate(dataSource); Console.WriteLine("Done"); serviceWorks = true; } catch { Console.WriteLine("Something went wrong"); serviceWorks = false; } } private void CreateBlobDataSource() { Console.WriteLine("Creating data source (blob)..."); var dataSource = DataSource.AzureBlobStorage( name: filesDataSourceName, storageConnectionString: blobConnectionString, containerName: $"articles"); try { searchClient.DataSources.CreateOrUpdate(dataSource); Console.WriteLine("Done"); serviceWorks = true; } catch (Exception e) { Console.WriteLine("Something went wrong"); serviceWorks = false; } }
Tworzymy jeden indeks, który będzie zawierał dane zarówno z bazy danych, jak i z Bloba. Definiujemy w nawiasach, jakiego typu są to pola oraz co możemy z nimi zrobić, tak jak było to przy tworzeniu indeksu na portalu.
class ArticleIndex { [Key] [IsFilterable, IsSearchable, IsRetrievable(true)] public string Title { get; set; } [IsRetrievable(true)] public string Id { get; set; } [IsFilterable, IsSearchable, IsRetrievable(true)] public string Content { get; set; } }
Będą to wszystkie dane z tabeli bazy danych oraz pole content (zawartość pliku tekstowego) z Bloba. Ponadto należy połączyć te dane jakimś polem, które będzie zawierała tabela oraz pliki z Bloba. Wykorzystamy do tego tytuł artykułu, który jest zapisywany w naszej bazie.
Najważniejszą częścią jest dodanie meta danych do bloba, również z tytułem artykułu, aby poinformować usługę, do którego indeksu ma trafić zawartość pliku. Następnie należy zrzutować dodawaną meta daną na odpowiednie pole w indeksie, co zaimplementowano poniżej.
Dane z bazy oraz przykład meta danych znajdujące się na Blobie.
Ponadto do indeksu możemy dodać suggester, oraz scoring profile.
private List<Suggester> CreateSuggester() => new List<Suggester> { new Suggester(suggesterName, new[] { "Title" }) }; private List<ScoringProfile> CreateScoringProfile() { var weights = new Dictionary<string, double> { { "Title", 6 }, { "Content", 3 } }; return new List<ScoringProfile> { new ScoringProfile(scoringProfileName, new TextWeights(weights)) }; }
Tworzenie indeksu
private void CreateIndex() { Console.WriteLine("Creating index..."); var indexExists = searchClient.Indexes.Exists(indexName); if (indexExists) searchClient.Indexes.Delete(indexName); var index = new Index( name: indexName, fields: FieldBuilder.BuildForType<ArticleIndex>(), scoringProfiles: CreateScoringProfile(), suggesters: CreateSuggester()); try { searchClient.Indexes.Create(index); Console.WriteLine("Done"); serviceWorks = true; } catch (Exception e) { Console.WriteLine("Something went wrong"); serviceWorks = false; } }
Musimy teraz stworzyć dwa indeksery, które będą ściągać dane z naszych źródeł. Musimy jednak pamiętać o zmapowaniu pól title z meta danych boba, oraz zawartości pliku. Wykona to poniższa metoda.
private List<FieldMapping> AddFieldsToMappingInBlob() => new List<FieldMapping> { new FieldMapping("title", "Title"), new FieldMapping("content", "Content") }; private void CreateBlobIndexer() { Console.WriteLine("Creating indexer (blob)..."); var indexerExists = searchClient.Indexers.Exists(filesIndexerName); if (indexerExists) searchClient.Indexers.Delete(filesIndexerName); var indexer = new Indexer( name: filesIndexerName, dataSourceName: filesDataSourceName, targetIndexName: indexName, schedule: new IndexingSchedule(TimeSpan.FromDays(1)), fieldMappings: AddFieldsToMappingInBlob() ); try { searchClient.Indexers.Create(indexer); searchClient.Indexers.Run(filesIndexerName); Console.WriteLine("Done"); serviceWorks = true; } catch (Exception e) { Console.WriteLine("Something went wrong"); serviceWorks = false; } } private void CreateContentIndexer() { Console.WriteLine("Creating indexer..."); var indexerExists = searchClient.Indexers.Exists(contentIndexerName); if (indexerExists) searchClient.Indexers.Delete(contentIndexerName); var indexer = new Indexer( name: contentIndexerName, dataSourceName: contentDataSourceName, targetIndexName: indexName, schedule: new IndexingSchedule(TimeSpan.FromDays(1))); try { searchClient.Indexers.Create(indexer); searchClient.Indexers.Run(contentIndexerName); System.Console.WriteLine("Done"); serviceWorks = true; } catch (Exception e) { Console.WriteLine("Something went wrong"); serviceWorks = false; } }
Odpalamy naszą aplikację. Jeżeli wszystko poszło bez błędów, możemy pisać zapytania.
W tym przypadku również napiszemy konsolówkę. Kod całego programu znajduje się w repozytorium pod adresem: https://github.com/swislockikacper/AzureSearchQueryExample.
Funkcja, która implementuje zapytanie, wygląda następująco:
private DocumentSearchResult Search(string query) { var searchParameters = new SearchParameters() { IncludeTotalResultCount = true, ScoringProfile = "scoring-profile", QueryType = QueryType.Full }; return indexClient.Documents.Search(query, searchParameters); }
Po otrzymaniu wyników, należy je zdeserializować.
private IEnumerable<Article> DeserializeResults(DocumentSearchResult response) { var results = new List<Article>(); foreach (var result in response.Results) { results.Add(new Article { Id = result.Document["Id"].ToString(), Title = result.Document["Title"].ToString(), Content = result.Document["Content"].ToString() }); } return results; }
Podsumowanie
Azure Search jest bardzo rozbudowanym i pożytecznym narzędziem — co ważne jest cały czas rozwijany. Nie jest także skomplikowany, wszystko można stworzyć z poziomu kodu, lub też wyklinać. W wielu przypadkach może ułatwić życie. Nie przedstawiałem tutaj samego działania podpowiadania oraz innych ciekawych funkcjonalności, jednak korzystanie z nich jest równie proste, co samo wyszukiwanie.