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
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
Next Next commit
Added AzureSearch v0.5
  • Loading branch information
ArgTang committed Sep 23, 2018
commit b3b4c936d56fc9f4528ad362ad384e95d08bea8d
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(bool clearIndex = false, 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,85 @@
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.Azure.Search;
using Microsoft.Azure.Search.Models;
using Microsoft.Extensions.Logging;

namespace CoreWiki.Application.Articles.Search.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(bool clearIndex = false, params T[] items)
{
if (clearIndex)
{
DeleteCurrentItems();
}

var action = items.Select(IndexAction.MergeOrUpload);
var job = new IndexBatch<T>(action);

try
{
var res = await _searchClient.CreateServiceClient<T>().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();
}
}

private void DeleteCurrentItems()
{
// TODO
}

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);
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
using Microsoft.Azure.Search;
using Microsoft.Azure.Search.Models;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Options;

namespace CoreWiki.Application.Articles.Search.AzureSearch
{
internal class AzureSearchClient : IAzureSearchClient
{
private readonly IOptionsSnapshot<SearchProviderSettings> _config;
private readonly IConfiguration _configuration;

private string searchServiceName => _config.Value.Az_ApiGateway; //_configuration["SearchProvider:Az_ApiGateway"];
private string adminApiKey => _config.Value.Az_ReadApiKey; //_configuration["SearchProvider:Az_ReadApiKey"];
private string queryApiKey => _config.Value.Az_WriteApiKey; //_configuration["SearchProvider:Az_WriteApiKey"];

public AzureSearchClient(IOptionsSnapshot<SearchProviderSettings> config, IConfiguration configuration)
{
_config = config;
_configuration = configuration;
}

public ISearchIndexClient CreateServiceClient<T>()
{
var index = typeof(T).FullName;
var serviceClient = new SearchServiceClient(searchServiceName, new SearchCredentials(adminApiKey));
return GetOrCreateIndex<T>(serviceClient);
}

private ISearchIndexClient GetOrCreateIndex<T>(SearchServiceClient serviceClient)
{
var index = typeof(T).FullName;
if (serviceClient.Indexes.Exists(index))
{
return serviceClient.Indexes.GetClient(index);
}

var definition = new Index()
{
Name = index,
Fields = FieldBuilder.BuildForType<T>()
};

serviceClient.Indexes.Create(definition);

return serviceClient.Indexes.GetClient(index);
}

public ISearchIndexClient GetSearchClient<T>()
{
var indexClient = new SearchIndexClient(searchServiceName, typeof(T).FullName, new SearchCredentials(queryApiKey));
return indexClient;
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
using Microsoft.Azure.Search;

namespace CoreWiki.Application.Articles.Search.AzureSearch
{
public interface IAzureSearchClient
{
/// <summary>
/// This client can be used for search
/// </summary>
/// <typeparam name="T"></typeparam>
/// <returns></returns>
ISearchIndexClient GetSearchClient<T>();

/// <summary>
/// This client can be used to Index elements
/// </summary>
/// <typeparam name="T"></typeparam>
/// <returns></returns>
ISearchIndexClient CreateServiceClient<T>();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
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>
/// When using local DB convert Generic search to Concrete Articlesearch
/// </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, IArticleRepository articleRepo)
{
_logger = logger;
_articleRepo = articleRepo;
}

public async Task<int> IndexElementsAsync(bool clearIndex = false, params T[] items)
{
// For LocalDB DB itself is responsible for "Indexing"
return await Task.Run(() => 0);
}

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; }
}
}
28 changes: 28 additions & 0 deletions CoreWiki.Application/Articles/Search/SetupSearchprovider.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
using CoreWiki.Application.Articles.Search.AzureSearch;
using CoreWiki.Application.Articles.Search.Impl;
using CoreWiki.Core.Domain;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;

namespace CoreWiki.Application.Articles.Search
{
public static class SetupSearchprovider
{
public static IServiceCollection ConfigureSearchProvider(this IServiceCollection services, IConfiguration configuration)
{
switch (configuration["SearchProvider"])
{
case "Az":
services.AddTransient(typeof(ISearchProvider<>), typeof(AzureSearchProvider<>));
services.AddTransient<IAzureSearchClient, AzureSearchClient>();
break;
default:
services.AddTransient<ISearchProvider<Article>, LocalDbArticleSearchProviderAdapter<Article>>();
break;
}

// db repos
return services;
}
}
}
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>
2 changes: 0 additions & 2 deletions CoreWiki/Configuration/Settings/AppSettings.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,10 @@ namespace CoreWiki.Configuration.Settings
{
public class AppSettings
{

public Uri Url { get; set; }
public Connectionstrings ConnectionStrings { get; set; }
public Comments Comments { get; set; }
public EmailNotifications EmailNotifications { get; set; }
public CspSettings CspSettings { get; set; }

}
}
Loading