Il motore di ricerca interno ha lo scopo di permettere la ricerca tra i âcontenutiâ del framework. Tuttavia, il concetto di âcontenutoâ deve essere definito in modo chiaro per garantire una corretta implementazione. Questo documento fornisce unâanalisi dettagliata dello sviluppo del motore di ricerca, dalla definizione dei contenuti fino allâimplementazione tecnica.
Prima di implementare il motore di ricerca, è necessario stabilire cosa si intende per âcontenutoâ. Una soluzione efficace è lâuso di Custom Attributes per marcare le classi e le proprietĂ che devono essere indicizzate dal motore di ricerca. Inoltre, una tabella di categorie può essere utilizzata per gestire le proprietĂ da indicizzare.
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Property, AllowMultiple = false)]
public class SearchableAttribute : Attribute
{
public string Category { get; }
public bool IsBodyPart { get; }
public SearchableAttribute(string category, bool isBodyPart = false)
{
Category = category;
IsBodyPart = isBodyPart;
}
}
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Property, AllowMultiple = false)]
public class SearchableKeyAttribute : Attribute
{
public int Index { get; }
public SearchableKeyAttribute(int index)
{
Index = index;
}
}
[Searchable("Article")]
public class Article
{
[SearchableKey(0)]
public int Id { get; set; }
[SearchableKey(1)]
public int CategoryKey { get; set; }
[Searchable("Title")]
public string Title { get; set; }
[Searchable("Body", true)]
public string Introduction { get; set; }
[Searchable("Body", true)]
public string MainContent { get; set; }
[Searchable("Body", true)]
public string Conclusion { get; set; }
}
public class Category
{
public int Id { get; set; }
public string Name { get; set; }
public List<string> IndexableProperties { get; set; }
}
public static class UrlGenerator
{
public static string GenerateUrl<T>(T entity, Category category)
{
var keyProperties = typeof(T).GetProperties()
.Where(prop => prop.GetCustomAttribute<SearchableKeyAttribute>() != null)
.OrderBy(prop => prop.GetCustomAttribute<SearchableKeyAttribute>()?.Index ?? 0)
.Select(prop => prop.GetValue(entity)?.ToString())
.Where(value => !string.IsNullOrEmpty(value))
.ToArray();
return keyProperties.Length > 0 ? $"/{category.Name.ToLower()}/{string.Join("/", keyProperties)}" : "#";
}
}
PoichĂŠ il âbodyâ può essere suddiviso in piĂš proprietĂ , il motore di ricerca deve:
IsBodyPart = true per creare unâunica stringa indicizzabile.public static class SearchHelper
{
public static string AggregateBody<T>(T content)
{
var bodyParts = typeof(T).GetProperties()
.Where(prop => prop.GetCustomAttribute<SearchableAttribute>()?.IsBodyPart == true)
.Select(prop => prop.GetValue(content)?.ToString())
.Where(value => !string.IsNullOrEmpty(value));
return string.Join(" ", bodyParts);
}
}
public interface ISearchProvider
{
Task<IEnumerable<SearchResult>> SearchAsync(string query, SearchOptions options);
}
public class SearchResult
{
public string Category { get; set; }
public string Title { get; set; }
public string Body { get; set; }
public string Url { get; set; }
public double Score { get; set; }
}
public class ElasticSearchProvider : ISearchProvider
{
private readonly ElasticClient _client;
private readonly List<Category> _categories;
public ElasticSearchProvider(ElasticClient client, List<Category> categories)
{
_client = client;
_categories = categories;
}
public async Task<IEnumerable<SearchResult>> SearchAsync(string query, SearchOptions options)
{
var response = await _client.SearchAsync<SearchResult>(s => s
.Query(q => q.QueryString(d => d.Query(query)))
);
return response.Documents.Select(d =>
{
var category = _categories.FirstOrDefault(c => c.Name == d.Category);
return new SearchResult
{
Title = d.Title,
Body = d.Introduction,
Url = UrlGenerator.GenerateUrl(d, category),
Score = response.MaxScore ?? 1.0
};
});
}
}
public class SqlSearchProvider : ISearchProvider
{
private readonly DbContext _context;
private readonly List<Category> _categories;
public SqlSearchProvider(DbContext context, List<Category> categories)
{
_context = context;
_categories = categories;
}
public async Task<IEnumerable<SearchResult>> SearchAsync(string query, SearchOptions options)
{
var results = await _context.Contents
.Where(c => EF.Functions.Contains(c.Title, query) || EF.Functions.Contains(c.Body, query))
.ToListAsync();
foreach (var record in results)
{
if (record.Title.Contains(query))
record.Score += 2;
if (record.Body.Contains(query))
record.Score += 1;
}
return results.Select(record =>
{
var category = _categories.FirstOrDefault(c => c.Name == record.Category);
return new SearchResult
{
Title = record.Title,
Body = record.Body,
Url = UrlGenerator.GenerateUrl(record, category),
Score = record.Score
};
});
}
}
Per mantenere gli indici aggiornati, possiamo creare unâinterfaccia IIndexUpdater e implementare classi specifiche per Elasticsearch e SQL Server.
public interface IIndexUpdater
{
Task UpdateIndexAsync<T>(T entity);
}
public class ElasticIndexUpdater : IIndexUpdater
{
private readonly ElasticClient _client;
public ElasticIndexUpdater(ElasticClient client)
{
_client = client;
}
public async Task UpdateIndexAsync<T>(T entity)
{
await _client.IndexDocumentAsync(entity);
}
}
public class SqlIndexUpdater : IIndexUpdater
{
private readonly DbContext _context;
public SqlIndexUpdater(DbContext context)
{
_context = context;
}
public async Task UpdateIndexAsync<T>(T entity)
{
_context.Update(entity);
await _context.SaveChangesAsync();
}
}
Quando un nuovo contenuto viene aggiunto o modificato, possiamo chiamare il metodo UpdateIndexAsync per aggiornare gli indici.
public class ContentService
{
private readonly IIndexUpdater _indexUpdater;
public ContentService(IIndexUpdater indexUpdater)
{
_indexUpdater = indexUpdater;
}
public async Task AddOrUpdateContentAsync<T>(T content)
{
// Aggiungi o aggiorna il contenuto nel database
// ...existing code...
// Aggiorna l'indice
await _indexUpdater.UpdateIndexAsync(content);
}
}
Il motore di ricerca supporta sia SQL che Elasticsearch attraverso provider configurabili. Attraverso la gestione dei custom attribute sulle classi e lâuso di una tabella di categorie, è possibile definire quali oggetti sono âcontenutiâ e il link al dettaglio di tali contenuti. Inoltre, lâaggiornamento degli indici è gestito tramite lâinterfaccia IIndexUpdater e le sue implementazioni specifiche per Elasticsearch e SQL Server.