Skip to content

Feature/azure search Help #303

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 12 commits into
base: dev
Choose a base branch
from
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using CoreWiki.Core.Domain;
using CoreWiki.Data.Abstractions.Interfaces;

namespace CoreWiki.Application.Articles.Search
{
/// <summary>
/// Proxy pattern: Using a middleman to extend functionality of original class but not changing it.
/// Ex caching, when you dont want/need to muddle the original class with logic for another resposibility,
/// Or here when we want to extend our app with a searchengine, wich needs indexing. The repository dont need to know we do indexing somewere else
/// </summary>
public class ArticleRepositorySearchIndexingProxy : IArticleRepository
{
private readonly ISearchProvider<Article> _searchProvider;
private readonly IArticleRepository _repository;

public ArticleRepositorySearchIndexingProxy(ISearchProvider<Article> searchProvider, Func<int, IArticleRepository> repository)
{
_searchProvider = searchProvider;
_repository = repository(1);
}

public Task<Article> CreateArticleAndHistory(Article article)
{
_searchProvider.IndexElementsAsync(article);
return _repository.CreateArticleAndHistory(article);
}

public Task<Article> Delete(string slug)
{
return _repository.Delete(slug);
}

public void Dispose()
{
_repository.Dispose();
}

// GetFrom Search
public Task<bool> Exists(int id)
{
return _repository.Exists(id);
}

public Task<Article> GetArticleById(int articleId)
{
return _repository.GetArticleById(articleId);
}

public Task<Article> GetArticleBySlug(string articleSlug)
{
return _repository.GetArticleBySlug(articleSlug);
}

// TODO: Search should not be a part of repository
public (IEnumerable<Article> articles, int totalFound) GetArticlesForSearchQuery(string filteredQuery, int offset, int resultsPerPage)
{
return _repository.GetArticlesForSearchQuery(filteredQuery, offset, resultsPerPage);
}

public Task<Article> GetArticleWithHistoriesBySlug(string articleSlug)
{
return _repository.GetArticleWithHistoriesBySlug(articleSlug);
}

// TODO: get from search
public Task<List<Article>> GetLatestArticles(int numOfArticlesToGet)
{
return _repository.GetLatestArticles(numOfArticlesToGet);
}

//TODO: update search?
public Task IncrementViewCount(string slug)
{
return _repository.IncrementViewCount(slug);
}

//TODO: Topic from Search
public Task<bool> IsTopicAvailable(string articleSlug, int articleId)
{
return _repository.IsTopicAvailable(articleSlug, articleId);
}

public Task Update(Article article)
{
_searchProvider.IndexElementsAsync(article);
return _repository.Update(article);
}
}
}
5 changes: 2 additions & 3 deletions CoreWiki.Application/Articles/Search/IArticlesSearchEngine.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
using System.Threading.Tasks;
using CoreWiki.Application.Articles.Search.Dto;
using CoreWiki.Core.Domain;
using CoreWiki.Application.Articles.Search.Dto;
using System.Threading.Tasks;

namespace CoreWiki.Application.Articles.Search
{
Expand Down
12 changes: 12 additions & 0 deletions CoreWiki.Application/Articles/Search/ISearchProvider.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
using System.Collections.Generic;
using System.Threading.Tasks;

namespace CoreWiki.Application.Articles.Search
{
public interface ISearchProvider<T> where T : class
{
Task<(IEnumerable<T> results, long total)> SearchAsync(string Query, int pageNumber, int resultsPerPage);

Task<int> IndexElementsAsync(params T[] items);
}
}
41 changes: 29 additions & 12 deletions CoreWiki.Application/Articles/Search/Impl/ArticlesDbSearchEngine.cs
Original file line number Diff line number Diff line change
@@ -1,41 +1,58 @@
using AutoMapper;
using CoreWiki.Application.Articles.Search.Dto;
using CoreWiki.Core.Domain;
using CoreWiki.Data.Abstractions.Interfaces;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;

namespace CoreWiki.Application.Articles.Search.Impl
{
public class ArticlesDbSearchEngine : IArticlesSearchEngine
{

private readonly IArticleRepository _articleRepo;
private readonly ISearchProvider<Article> _searchProvider;
private readonly IMapper _mapper;

public ArticlesDbSearchEngine(IArticleRepository articleRepo, IMapper mapper)
public ArticlesDbSearchEngine(ISearchProvider<Article> searchProvider, IMapper mapper)
{
_articleRepo = articleRepo;
_searchProvider = searchProvider;
_mapper = mapper;
}

public async Task<SearchResultDto<ArticleSearchDto>> SearchAsync(string query, int pageNumber, int resultsPerPage)
{
var filteredQuery = query.Trim();
var offset = (pageNumber - 1) * resultsPerPage;
var (articles, totalFound) = await _searchProvider.SearchAsync(filteredQuery, pageNumber, resultsPerPage).ConfigureAwait(false);

// TODO maybe make this searchproviders problem
var total = int.TryParse(totalFound.ToString(), out var inttotal);
if (!total)
{
inttotal = int.MaxValue;
}

var (articles, totalFound) = _articleRepo.GetArticlesForSearchQuery(filteredQuery, offset, resultsPerPage);
return _mapper.CreateArticleResultDTO(filteredQuery, articles, pageNumber, resultsPerPage, inttotal);
}
}

internal static class SearchResultFactory
{
internal static SearchResultDto<ArticleSearchDto> CreateArticleResultDTO(this IMapper mapper, string query, IEnumerable<Article> articles, int currenPage, int resultsPerPage, int totalResults)
{
var results = new List<Article>();
if (articles?.Any() == true)
{
results = articles.ToList();
}
var result = new SearchResult<Article>
{
Query = filteredQuery,
Results = articles.ToList(),
CurrentPage = pageNumber,
Query = query,
Results = results,
CurrentPage = currenPage,
ResultsPerPage = resultsPerPage,
TotalResults = totalFound
TotalResults = totalResults
};

return _mapper.Map<SearchResultDto<ArticleSearchDto>>(result);
return mapper.Map<SearchResultDto<ArticleSearchDto>>(result);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using CoreWiki.Core.Domain;
using CoreWiki.Data.Abstractions.Interfaces;
using Microsoft.Extensions.Logging;

namespace CoreWiki.Application.Articles.Search.Impl
{
/// <summary>
/// Adapter pattern: When using local DB, convert Concrete Articlesearch to Generic ISearchProvider<T>
/// </summary>
/// <typeparam name="T"></typeparam>
public class LocalDbArticleSearchProviderAdapter<T> : ISearchProvider<T> where T : Article
{
private readonly ILogger _logger;
private readonly IArticleRepository _articleRepo;

public LocalDbArticleSearchProviderAdapter(ILogger<LocalDbArticleSearchProviderAdapter<T>> logger, Func<int, IArticleRepository> articleRepo)
{
_logger = logger;
_articleRepo = articleRepo(1);
}

public Task<int> IndexElementsAsync(params T[] items)
{
// For LocalDB DB itself is responsible for "Indexing"
return Task.Run(() => items.Length);
}

public async Task<(IEnumerable<T> results, long total)> SearchAsync(string Query, int pageNumber, int resultsPerPage)
{
var offset = (pageNumber - 1) * resultsPerPage;
var (articles, totalFound) = _articleRepo.GetArticlesForSearchQuery(Query, offset, resultsPerPage);

var supportedType = articles.GetType().GetGenericArguments()[0];
if (typeof(T) == supportedType)
{
var tlist = articles.Cast<T>();
return (results: tlist, total: totalFound);
}

_logger.LogWarning($"{nameof(SearchAsync)}: Only supports search for {nameof(supportedType)} but asked for {typeof(T).FullName}");
return (Enumerable.Empty<T>(), 0);
}
}
}
Original file line number Diff line number Diff line change
@@ -1,18 +1,19 @@
using System.Threading;
using System.Threading.Tasks;
using CoreWiki.Application.Articles.Search.Dto;
using CoreWiki.Application.Articles.Search.Dto;
using MediatR;
using System.Threading;
using System.Threading.Tasks;

namespace CoreWiki.Application.Articles.Search.Queries
{
class SearchArticlesHandler: IRequestHandler<SearchArticlesQuery, SearchResultDto<ArticleSearchDto>>
internal class SearchArticlesHandler : IRequestHandler<SearchArticlesQuery, SearchResultDto<ArticleSearchDto>>
{
private readonly IArticlesSearchEngine _articlesSearchEngine;

public SearchArticlesHandler(IArticlesSearchEngine articlesSearchEngine)
{
_articlesSearchEngine = articlesSearchEngine;
}

public Task<SearchResultDto<ArticleSearchDto>> Handle(SearchArticlesQuery request, CancellationToken cancellationToken)
{
return _articlesSearchEngine.SearchAsync(request.Query, request.PageNumber, request.ResultsPerPage);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
namespace CoreWiki.Application.Articles.Search
{
public class SearchProviderSettings
{
public string Az_ApiGateway { get; set; }
public string Az_ReadApiKey { get; set; }
public string Az_WriteApiKey { get; set; }
}
}
2 changes: 2 additions & 0 deletions CoreWiki.Application/CoreWiki.Application.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,13 @@

<ItemGroup>
<PackageReference Include="MediatR" Version="5.1.0" />
<PackageReference Include="Microsoft.Azure.Search" Version="5.0.2" />
<PackageReference Include="Microsoft.Extensions.Logging" Version="2.1.1" />
</ItemGroup>

<ItemGroup>
<ProjectReference Include="..\CoreWiki.Data.Abstractions\CoreWiki.Data.Abstractions.csproj" />
<ProjectReference Include="..\CoreWiki.Data\CoreWiki.Data.EntityFramework.csproj" />
</ItemGroup>

</Project>
77 changes: 77 additions & 0 deletions CoreWiki.Azure/Areas/AzureSearch/ArticlesAzureSearcSearchEngine.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using CoreWiki.Application.Articles.Search;
using Microsoft.Azure.Search;
using Microsoft.Azure.Search.Models;
using Microsoft.Extensions.Logging;

namespace CoreWiki.Azure.Areas.AzureSearch
{
/// <summary>
/// Tutorial here: https://github.com/Azure-Samples/search-dotnet-getting-started/blob/master/DotNetHowTo/DotNetHowTo/Program.cs
/// </summary>
/// <typeparam name="T"></typeparam>
public class AzureSearchProvider<T> : ISearchProvider<T> where T : class
{
private readonly ILogger _logger;
private readonly IAzureSearchClient _searchClient;
private readonly ISearchIndexClient _myclient;

public AzureSearchProvider(ILogger<AzureSearchProvider<T>> logger, IAzureSearchClient searchClient)
{
_logger = logger;
_searchClient = searchClient;
_myclient = _searchClient.GetSearchClient<T>();
}

public async Task<int> IndexElementsAsync(params T[] items)
{
var action = items.Select(IndexAction.MergeOrUpload);
var job = new IndexBatch<T>(action);

try
{
var myclient = _searchClient.CreateServiceClient<T>();
var res = await _myclient.Documents.IndexAsync<T>(job).ConfigureAwait(false);
return res.Results.Count;
}
catch (IndexBatchException e)
{
// Sometimes when your Search service is under load, indexing will fail for some of the documents in
// the batch. Depending on your application, you can take compensating actions like delaying and
// retrying. For this simple demo, we just log the failed document keys and continue.

var failedElements = e.IndexingResults.Where(r => !r.Succeeded).Select(r => r.Key);
_logger.LogError(e, "Failed to index some of the documents", failedElements);
return items.Length - failedElements.Count();
}
}

public async Task<(IEnumerable<T> results, long total)> SearchAsync(string Query, int pageNumber, int resultsPerPage)
{
var offset = (pageNumber - 1) * resultsPerPage;
var parameters = new SearchParameters()
{
IncludeTotalResultCount = true,
Top = resultsPerPage,
Skip = offset,
};
try
{
var res = await _myclient.Documents.SearchAsync(Query, parameters).ConfigureAwait(false);

var total = res.Count.GetValueOrDefault();
var list = res.Results;
//TODO: map results

return (results: null, total: total);
}
catch (System.Exception e)
{
_logger.LogCritical(e, $"{nameof(SearchAsync)} Search failed horribly, you should check it out");
return (results: null, total: 0);
}
}
}
}
Loading