Skip to content

Commit

Permalink
Model ile filtreleme özelliği eklendi.
Browse files Browse the repository at this point in the history
  • Loading branch information
barisakdas committed Apr 9, 2024
1 parent d01c091 commit 91b3c04
Show file tree
Hide file tree
Showing 11 changed files with 340 additions and 3 deletions.
20 changes: 20 additions & 0 deletions Elasticsearch.Api/Controllers/BookController.cs
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,26 @@ public async Task<IActionResult> GetAllAsync()
return this.FromResult(result);
}

[HttpGet("getfilter_with_searchtext")]
public async Task<IActionResult> GetFilterAsync(string searchText)
{
// Servis üzerinden veriyi alıyoruz. Alınan bu veri bize Result<T> şeklinde döneceği için bunun yapılandırmasına ihtiyacımız var.
var result = await _service.GetFilterAsync(searchText);

// Gelen verideki Result yapısının durumunu kontrol ederek ve ona uygun geri dönüş tipini (IActionResult) seçerek işlemi sonlandırıyoruz.
return this.FromResult(result);
}

[HttpPost("getfilter_with_model")]
public async Task<IActionResult> GetFilterAsync(SearchBookModel model)
{
// Servis üzerinden veriyi alıyoruz. Alınan bu veri bize Result<T> şeklinde döneceği için bunun yapılandırmasına ihtiyacımız var.
var result = await _service.GetFilterAsync(model);

// Gelen verideki Result yapısının durumunu kontrol ederek ve ona uygun geri dönüş tipini (IActionResult) seçerek işlemi sonlandırıyoruz.
return this.FromResult(result);
}

[HttpGet("getbyid")]
public async Task<IActionResult> GetByIdAsync(string id)
{
Expand Down
7 changes: 6 additions & 1 deletion Elasticsearch.Application/ServiceInterfaces/IBookService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,14 @@ public interface IBookService
/// <summary>Veri tabanında aktif olarak bulunan verilerin gerekli işlemleri yapılarak ön yüze toplu şekilde gönderilmesini sağlayan metot.</summary>
Task<BaseResult<List<BookDto>>> GetAllAsync();

/// <summary>Parametre olarak alınan veriyi hem Title hemde Abstact alanında Full Text search ve Term query ile arayarak getiren metot.</summary>
/// <summary>Ekrandan gelen parametre ile Makalelerin `Title` ve `Abstract` alanlarında full text search araması yapmak istiyoruz.
/// Bunun için repositorymize özel olarak bir sorgu hazırladık. Bu sorgu makaleleri ararken elasticsearch veritabanının Full Text Search özelliğini kullanacak.
/// Arka planda(repository içinde) komplex ve birleşik bir sorgu yazacağız.</summary>
Task<BaseResult<List<BookDto>>> GetFilterAsync(string searchText);

/// <summary>Ekrandan gelen parametreler ile Makalelerin ilgili tüm alanlarında filtreleme yapmamızı sağlayacak olan metot..</summary>
Task<BaseResult<List<BookDto>>> GetFilterAsync(SearchBookModel model);

/// <summary>Veri tabanında aktif olarak bulunan ve ilgili id ye ait veriyi getiren sağlayan metot.</summary>
Task<BaseResult<BookDto>> GetByIdAsync(string id);

Expand Down
38 changes: 38 additions & 0 deletions Elasticsearch.Application/Services/BookService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,44 @@ public async Task<BaseResult<List<BookDto>>> GetAllAsync()
return new SuccessfullResult<List<BookDto>>(response);
}

/// <summary>Ekrandan gelen parametre ile Makalelerin `Title` ve `Abstract` alanlarında full text search araması yapmak istiyoruz.
/// Bunun için repositorymize özel olarak bir sorgu hazırladık. Bu sorgu makaleleri ararken elasticsearch veritabanının Full Text Search özelliğini kullanacak.
/// Arka planda(repository içinde) komplex ve birleşik bir sorgu yazacağız.</summary>
public async Task<BaseResult<List<BookDto>>> GetFilterAsync(string searchText)
{
// İlk olarak dışarıdan gelen modelin boş olup olmadığına bakıyoruz.
// Böylece gelen model boşşsa herhangi bir işlem yapmadan hızlıca metodu kırabiliriz.
if (string.IsNullOrWhiteSpace(searchText))
return new BadRequestResult<List<BookDto>>("Gelen id boş geçilemez!");

// Elasticsearch üzerindeki veriyi alıyoruz.
var (data, message) = await _repository.GetFilterAsync(IndexName, searchText);
if (data is null)
return new BadRequestResult<List<BookDto>>($"Veri alınırken hata ile karşılaşıldı: Hata: {message}");

var response = _mapper.Map<List<BookDto>>(data);

return new SuccessfullResult<List<BookDto>>(response);
}

/// <summary>Ekrandan gelen parametreler ile Makalelerin ilgili tüm alanlarında filtreleme yapmamızı sağlayacak olan metot..</summary>
public async Task<BaseResult<List<BookDto>>> GetFilterAsync(SearchBookModel model)
{
// İlk olarak dışarıdan gelen modelin boş olup olmadığına bakıyoruz.
// Böylece gelen model boşşsa herhangi bir işlem yapmadan hızlıca metodu kırabiliriz.
if (model is null)
return new BadRequestResult<List<BookDto>>("Gelen id boş geçilemez!");

// Elasticsearch üzerindeki veriyi alıyoruz.
var (data, message) = await _repository.GetFilterAsync(IndexName, model);
if (data is null)
return new BadRequestResult<List<BookDto>>($"Veri alınırken hata ile karşılaşıldı: Hata: {message}");

var response = _mapper.Map<List<BookDto>>(data);

return new SuccessfullResult<List<BookDto>>(response);
}

/// <summary>Veri tabanında aktif olarak bulunan verilerin gerekli işlemleri yapılarak ön yüze gönderilmesini sağlayan metot.</summary>
public async Task<BaseResult<BookDto>> GetByIdAsync(string id)
{
Expand Down
14 changes: 14 additions & 0 deletions Elasticsearch.Core/Models/BookModels.cs
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,20 @@ public record UpdateBookModel
public List<string> Categories { get; set; } = new();
}

/// <summary>Filtre alanında seçilecek özelliklere göre arama işlemini yapmamızı sağlayacak model.</summary>
public record SearchBookModel
{
public string? Title { get; set; } = null!;
public string? Abstract { get; set; } = null!;
public double? MinPrice { get; set; }
public uint? MinStock { get; set; }

[DataType(DataType.Date)]
public DateTime? PublishDateStart { get; set; }
public int Page { get; set; } = 1;
public int PageSize { get; set; } = 10;
}

// <note-tr>
// Burada bir sınıfın oluşturulma ve güncellenmesi dışında kullanılacak tüm talep modellerinin tek bir .cs dosyasının içerisinde
// oluşturulması yöntemini kullanıyoruz. Böylece .cs fazlalaşmamış oluyor ve klasör/dosya kalabalığının da önüne geçmiş oluyoruz.
Expand Down
1 change: 1 addition & 0 deletions Elasticsearch.Infrastructure/Global/CommonNamespaces.cs
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@

global using Elasticsearch.Core.Entities;
global using Elasticsearch.Core.Entities.BaseEntites;
global using Elasticsearch.Core.Models;
global using Elasticsearch.Infrastructure.Repositories.BaseRepositories;
global using Elasticsearch.Infrastructure.RepositoryInterfaces;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ public class BaseRepository<T> : RepositoryInterfaces.BaseRepositoryInterfaces.I
// sorgu oluşturma için Fluent API ve belge indeksleme gibi ortak görevler için yardımcılar sunar1.
// .NET 8’in kendine has özellikleri arasında performans iyileştirmeleri, çöp toplama ve çekirdek ile uzantı kitaplıklarına yönelik geliştirmeler bulunmaktadır.
// Ayrıca, mobil uygulamalar ve yeni kaynak oluşturucular için com birlikte çalışma ve yapılandırma bağlaması gibi yeni özellikler içerir
private readonly ElasticsearchClient _client;
internal readonly ElasticsearchClient _client;

public BaseRepository(ElasticsearchClient client)
=> _client = client;
Expand Down
147 changes: 147 additions & 0 deletions Elasticsearch.Infrastructure/Repositories/BookRepository.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,4 +5,151 @@ public class BookRepository : BaseRepository<Book>, IBookRepository
public BookRepository(ElasticsearchClient client) : base(client)
{
}

/// <summary>Ekrandan gelen parametre ile Makalelerin `Title` ve `Abstract` alanlarında full text search araması yapmak istiyoruz.
/// Bunun için repositorymize özel olarak bir sorgu hazırladık. Bu sorgu makaleleri ararken elasticsearch veritabanının Full Text Search özelliğini kullanacak.</summary>
public async Task<(IEnumerable<Book> data, string message)> GetFilterAsync(string indexName, string searchText)
{
// Not: Bu sorguda Title/Abstract alanında FullText search yerine MatchBoolPrefix kullanmak daha doğru bir yaklaşım olacaktır.
// Çünkü; kullanıcı Title/Abstract içerisindeki tam kelimeyi yazmıyor olabilir.
// Örneğin: makalemizin başlığı `Elasticsearch ve .Net8 Yenilikleri` olsun. Bu durumda kullanıcı aramaya `yenilik` ya da `elastic` yazarsa bu makalenin gelmesini istiyoruz.
// Eğer ki burada Match kullanırsak bu metot arka planda direkt olarak gelen kelimelerle eş bir kelime arayacağından hiç bir veri getirmez.
// Bu yüzden bu sorguda Title/Abstract alanı için `MatchBoolPrefix` metodunu kullanacağız.

// Should içerisinde bir Match ifadesi yazıp hemen arkadasından başka bir sorguyu nokta koyarak yazarsak elasticsearch bunu `and` bağlacı ile bağlar.
// Bunun önüne geçmek için Should metodunu birden fazla parametre ile beslemeliyiz.
// Örneğin: ......Should(s => s.Match().Match())... => Bu durumda iki match metodu `and` bağlacına alınır. Bunun yerinne şu şekilde oluşturursak:
// ......Should(s => s.Match(), s => s.Match()).... => Bu iki match metodu `or` metodu ile bağlanacaktır. Burada bağlaçların yönetimi veriyi doğru alabilmek adına önemlidir.
var result = await _client
.SearchAsync<Book>(
s => s.Index(indexName)
.Query(q => q.Bool(
b => b.Should(
s => s.MatchBoolPrefix(
m => m.Field(
f => f.Abstract)
.Query(searchText)),
s => s.MatchBoolPrefix(
m => m.Field(
f => f.Title)
.Query(searchText))))));

if (result is null || !result.IsValidResponse)
{
result.TryGetOriginalException(out Exception exception);
return (Enumerable.Empty<Book>().ToList(), exception.Message);
}

// Not: Gelen veri kendi içerisinde id özelliğini taşımıyor. Bu id verinin `Hits` alanının içerisinde.
// Hist alanının içerisindeki veriyi source içerisine taşırsak documents alanı da source içerisinden beslendiği için verilerimizin id özelliğini almış oluruz.
foreach (var hit in result.Hits)
hit.Source.Id = hit.Id;

return (result.Documents.ToList(), string.Empty);
}

/// <summary>Belirtilen arama modeline göre kitapları filtreleyerek getirir.</summary>
/// <param name="indexName">Elasticsearch index adı.</param>
/// <param name="model">Arama kriterlerini içeren model.</param>
/// <returns>Filtrelenmiş kitapların listesi ve işlem mesajı içeren bir tuple döner.</returns>
public async Task<(IEnumerable<Book> data, string message)> GetFilterAsync(string indexName, SearchBookModel model)
{
// Sorgu filtrelerimizi tutacak bir liste başlatıyoruz.
List<Action<QueryDescriptor<Book>>> listQuery = new();

// Eğer model nesnesi boş ise, tüm kitapları getirecek bir sorgu ekliyoruz.
if (model is null)
{
listQuery.Add(
g => g.MatchAll(
new MatchAllQuery()));

// Filtre uygulama fonksiyonumuzu çağırıyoruz.
return await ApplyFilter(indexName, listQuery, model.Page, model.PageSize);
}

// Modelde başlık varsa, başlığa göre bir filtre ekliyoruz.
// Fuziness:2 dememizin sebebi 2 harfte hata yapmasına müsade etmek.
if (!string.IsNullOrWhiteSpace(model.Title))
listQuery.Add(
q => q.MatchBoolPrefix(
m => m.Field(f => f.Title)
.Query(model.Title)
.Fuzziness(new Fuzziness(2))));

// Modelde özet varsa, özete göre bir filtre ekliyoruz.
// Fuziness:2 dememizin sebebi 2 harfte hata yapmasına müsade etmek.
if (!string.IsNullOrWhiteSpace(model.Abstract))
listQuery.Add(
q => q.MatchBoolPrefix(
m => m.Field(f => f.Abstract)
.Query(model.Abstract)
.Fuzziness(new Fuzziness(2))));

// Modelde minimum fiyat belirtilmişse, fiyata göre bir filtre ekliyoruz.
if (model.MinPrice is not null & model.MinPrice is not default(double))
listQuery.Add(
q => q.Bool(
b => b.Filter(
f => f.Range(
r => r.NumberRange(
nr => nr.Field(f => f.Price)
.Gte(model.MinPrice))))));

// Modelde minimum stok belirtilmişse, stoğa göre bir filtre ekliyoruz.
if (model.MinStock is not null & model.MinStock is not default(uint))
listQuery.Add(
q => q.Bool(
b => b.Filter(
f => f.Range(
r => r.NumberRange(
nr => nr.Field(f => f.Stock)
.Gte(model.MinStock))))));

// Modelde yayın tarihi başlangıcı belirtilmişse, tarihe göre bir filtre ekliyoruz.
if (model.PublishDateStart is not null & model.PublishDateStart != DateTime.MinValue)
listQuery.Add(
q => q.Range(
r => r.DateRange(
dr => dr.Field(f => f.PublishDate)
.Lte(model.PublishDateStart))));

// Filtre uygulama fonksiyonumuzu çağırıyoruz.
return await ApplyFilter(indexName, listQuery, model.Page, model.PageSize);
}

/// <summary>Belirtilen sorgu filtrelerini uygulayarak kitapları getirir.</summary>
/// <param name="indexName">Elasticsearch index adı.</param>
/// <param name="listQuery">Uygulanacak sorgu filtrelerinin listesi.</param>
/// <param name="page">Sayfa numarası.</param>
/// <param name="pageSize">Bir sayfada gösterilecek öğe sayısı.</param>
/// <returns>Filtrelenmiş kitapların listesi ve işlem mesajı içeren bir tuple döner.</returns>
private async Task<(IEnumerable<Book> data, string message)> ApplyFilter(string indexName, List<Action<QueryDescriptor<Book>>> listQuery, int page = 1, int pageSize = 10)
{
// Sayfalama için başlangıç noktasını hesaplıyoruz.
var pageFrom = (page - 1) * pageSize;

// Elasticsearch istemcisini kullanarak asenkron bir arama işlemi gerçekleştiriyoruz.
var result = await _client.SearchAsync<Book>(
s => s.Index(indexName)
.Size(pageSize)
.From(pageFrom)
.Query(
q => q.Bool(
b => b.Must(listQuery.ToArray()))));

// Eğer sonuç null ise veya geçerli bir yanıt değilse, hata mesajı ile boş bir liste dönüyoruz.
if (result is null || !result.IsValidResponse)
{
result.TryGetOriginalException(out Exception exception);
return (Enumerable.Empty<Book>().ToList(), exception.Message);
}

// Her bir sonucun ID'sini kaynağa ekliyoruz.
foreach (var hit in result.Hits)
hit.Source.Id = hit.Id;

// Sonuçları ve boş bir mesajı döndürüyoruz.
return (data: result.Documents.AsEnumerable(), message: string.Empty);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,13 @@

public interface IBookRepository : BaseRepositoryInterfaces.IRepository<Book>
{
/// <summary>Ekrandan gelen parametre ile Makalelerin `Title` ve `Abstract` alanlarında full text search araması yapmak istiyoruz.
/// Bunun için repositorymize özel olarak bir sorgu hazırladık. Bu sorgu makaleleri ararken elasticsearch veritabanının Full Text Search özelliğini kullanacak.</summary>
Task<(IEnumerable<Book> data, string message)> GetFilterAsync(string indexName, string searchText);

/// <summary>Belirtilen arama modeline göre kitapları filtreleyerek getirir.</summary>
/// <param name="indexName">Elasticsearch index adı.</param>
/// <param name="model">Arama kriterlerini içeren model.</param>
/// <returns>Filtrelenmiş kitapların listesi ve işlem mesajı içeren bir tuple döner.</returns>
Task<(IEnumerable<Book> data, string message)> GetFilterAsync(string indexName, SearchBookModel model);
}
Loading

0 comments on commit 91b3c04

Please sign in to comment.