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.
Podobne artykuły
Krytyczne spojrzenie na kod jest kluczowy dla jego skutecznej analizy. Jak analizować systemy legacy
Sieci neuronowe. PyTorch i praktyczny projekt od początku do końca
Od czego zacząć swoją przygodę w branży IT? Rozmowa z Jackiem Hrynczyszynem, Java Developerem
Kim jest Software Architect? Obowiązki, specjalizacje, kariera
Co nowego w Javie? Przegląd zmian, które przyniosło JDK 20
Efektywne zarządzanie Protocoll Buffers z “Buf”. Wszystko, co powinieneś wiedzieć
Czy Scala to wciąż dobry język dla programistów w 2023 roku?



