Entity Framework, Code First e Full Text Search

Mi rendo conto che sono state poste molte domande relative alla ricerca a testo integrale e ad Entity Framework, ma spero che questa domanda sia un po ‘diversa.

Sto usando Entity Framework, Code First e ho bisogno di fare una ricerca testuale. Quando ho bisogno di eseguire la ricerca di testo completo, in genere avrò anche altri criteri / restrizioni – come saltare le prime 500 righe o filtrare su un’altra colonna, ecc.

Vedo che questo è stato gestito utilizzando le funzioni con valori di tabella – vedi http://sqlblogcasts.com/blogs/simons/archive/2008/12/18/LINQ-to-SQL—Enabling-Fulltext-searching.aspx . E questa sembra l’idea giusta.

Sfortunatamente, le funzioni con valori di tabella non sono supportate fino a Entity Framework 5.0 (e anche allora, credo, non sono supportate per Code First).

La mia vera domanda è quali sono i suggerimenti per il modo migliore per gestire questo, sia per Entity Framework 4.3 che per Entity Framework 5.0. Ma per essere specifici:

  1. Oltre all’SQL dinamico (tramite System.Data.Entity.DbSet.SqlQuery , ad esempio), sono disponibili opzioni per Entity Framework 4.3?

  2. Se eseguo l’aggiornamento a Entity Framework 5.0, esiste un modo per utilizzare prima le funzioni con valori di tabella con il codice?

Grazie, Eric

Utilizzando gli intercettori introdotti in EF6, è ansible contrassegnare la ricerca di testo completo in linq e quindi sostituirla in dbcommand come descritto in http://www.entityframework.info/Home/FullTextSearch :

 public class FtsInterceptor : IDbCommandInterceptor { private const string FullTextPrefix = "-FTSPREFIX-"; public static string Fts(string search) { return string.Format("({0}{1})", FullTextPrefix, search); } public void NonQueryExecuting(DbCommand command, DbCommandInterceptionContext interceptionContext) { } public void NonQueryExecuted(DbCommand command, DbCommandInterceptionContext interceptionContext) { } public void ReaderExecuting(DbCommand command, DbCommandInterceptionContext interceptionContext) { RewriteFullTextQuery(command); } public void ReaderExecuted(DbCommand command, DbCommandInterceptionContext interceptionContext) { } public void ScalarExecuting(DbCommand command, DbCommandInterceptionContext interceptionContext) { RewriteFullTextQuery(command); } public void ScalarExecuted(DbCommand command, DbCommandInterceptionContext interceptionContext) { } public static void RewriteFullTextQuery(DbCommand cmd) { string text = cmd.CommandText; for (int i = 0; i < cmd.Parameters.Count; i++) { DbParameter parameter = cmd.Parameters[i]; if (parameter.DbType.In(DbType.String, DbType.AnsiString, DbType.StringFixedLength, DbType.AnsiStringFixedLength)) { if (parameter.Value == DBNull.Value) continue; var value = (string)parameter.Value; if (value.IndexOf(FullTextPrefix) >= 0) { parameter.Size = 4096; parameter.DbType = DbType.AnsiStringFixedLength; value = value.Replace(FullTextPrefix, ""); // remove prefix we added n linq query value = value.Substring(1, value.Length - 2); // remove %% escaping by linq translator from string.Contains to sql LIKE parameter.Value = value; cmd.CommandText = Regex.Replace(text, string.Format( @"\[(\w*)\].\[(\w*)\]\s*LIKE\s*@{0}\s?(?:ESCAPE N?'~')", parameter.ParameterName), string.Format(@"contains([$1].[$2], @{0})", parameter.ParameterName)); if (text == cmd.CommandText) throw new Exception("FTS was not replaced on: " + text); text = cmd.CommandText; } } } } } static class LanguageExtensions { public static bool In(this T source, params T[] list) { return (list as IList).Contains(source); } } 

Ad esempio, se si dispone di una nota di class con campo indicizzato FTS NoteText:

 public class Note { public int NoteId { get; set; } public string NoteText { get; set; } } 

e mappa EF per questo

 public class NoteMap : EntityTypeConfiguration { public NoteMap() { // Primary Key HasKey(t => t.NoteId); } } 

e contesto per questo:

 public class MyContext : DbContext { static MyContext() { DbInterception.Add(new FtsInterceptor()); } public MyContext(string nameOrConnectionString) : base(nameOrConnectionString) { } public DbSet Notes { get; set; } protected override void OnModelCreating(DbModelBuilder modelBuilder) { modelBuilder.Configurations.Add(new NoteMap()); } } 

puoi avere una syntax abbastanza semplice per la query FTS:

 class Program { static void Main(string[] args) { var s = FtsInterceptor.Fts("john"); using (var db = new MyContext("CONNSTRING")) { var q = db.Notes.Where(n => n.NoteText.Contains(s)); var result = q.Take(10).ToList(); } } } 

Questo genererà SQL come

 exec sp_executesql N'SELECT TOP (10) [Extent1].[NoteId] AS [NoteId], [Extent1].[NoteText] AS [NoteText] FROM [NS].[NOTES] AS [Extent1] WHERE contains([Extent1].[NoteText], @p__linq__0)',N'@p__linq__0 char(4096)',@p__linq__0='(john) 

Si noti che è necessario utilizzare la variabile locale e non è ansible spostare il wrapper FTS all’interno di espressioni come

 var q = db.Notes.Where(n => n.NoteText.Contains(FtsInterceptor.Fts("john"))); 

Ho trovato che il modo più semplice per implementare questo è quello di configurare e configurare la ricerca full-text in SQL Server e quindi utilizzare una stored procedure. Passa i tuoi argomenti a SQL, consenti al DB di fare il suo lavoro e restituire un object complesso o mappare i risultati a un’ quadro. Non è necessario avere SQL dinamico, ma potrebbe essere ottimale. Ad esempio, se è necessario il paging, è ansible passare in PageNumber e PageSize su ogni richiesta senza la necessità di SQL dinamico. Tuttavia, se il numero di argomenti fluttua per query, sarà la soluzione ottimale.

Recentemente ho avuto un requisito simile e ho finito per scrivere un’estensione IQueryable specificamente per l’accesso all’indice di testo completo di Microsoft, è disponibile qui IQueryableFreeTextExtensions

Come menzionato dagli altri ragazzi, direi di iniziare a usare Lucene.NET

Lucene ha una curva di apprendimento piuttosto elevata, ma ho trovato un wrapper chiamato ” SimpleLucene “, che può essere trovato su CodePlex

Lasciami citare un paio di blocchi di codice dal blog per mostrarti quanto è facile da usare. Ho appena iniziato a usarlo, ma ho capito molto velocemente.

Per prima cosa, prendi alcune entity framework dal tuo repository o, nel tuo caso, usa Entity Framework

 public class Repository { public IList Products { get { return new List { new Product { Id = 1, Name = "Football" }, new Product { Id = 2, Name = "Coffee Cup"}, new Product { Id = 3, Name = "Nike Trainers"}, new Product { Id = 4, Name = "Apple iPod Nano"}, new Product { Id = 5, Name = "Asus eeePC"}, }; } } } 

La prossima cosa che vuoi fare è creare una definizione di indice

 public class ProductIndexDefinition : IIndexDefinition { public Document Convert(Product p) { var document = new Document(); document.Add(new Field("id", p.Id.ToString(), Field.Store.YES, Field.Index.NOT_ANALYZED)); document.Add(new Field("name", p.Name, Field.Store.YES, Field.Index.ANALYZED)); return document; } public Term GetIndex(Product p) { return new Term("id", p.Id.ToString()); } } 

e creare un indice di ricerca per questo.

 var writer = new DirectoryIndexWriter( new DirectoryInfo(@"c:\index"), true); var service = new IndexService(); service.IndexEntities(writer, Repository().Products, ProductIndexDefinition()); 

Quindi, ora hai un indice di ricerca. L’unica cosa che rimane da fare è … cercare! Puoi fare cose incredibili, ma può essere facile come questo: (per maggiori esempi vedi il blog o la documentazione su codeplex)

 var searcher = new DirectoryIndexSearcher( new DirectoryInfo(@"c:\index"), true); var query = new TermQuery(new Term("name", "Football")); var searchService = new SearchService(); Func converter = (doc) => { return new ProductSearchResult { Id = int.Parse(doc.GetValues("id")[0]), Name = doc.GetValues("name")[0] }; }; IList results = searchService.SearchIndex(searcher, query, converter); 

L’esempio qui http://www.entityframework.info/Home/FullTextSearch non è una soluzione completa. Dovrai cercare di capire come funziona la ricerca di testo completo. Immagina di avere un campo di ricerca e l’utente digita 2 parole per colpire la ricerca. Il codice precedente genererà un’eccezione. È necessario eseguire prima la pre-elaborazione della frase di ricerca per passarla alla query utilizzando AND o OR logico.

per esempio la tua frase di ricerca è “blah blah2”, quindi devi convertirlo in:

 var searchTerm = @"\"blah\" AND/OR \"blah2\" "; 

La soluzione completa sarebbe:

  value = Regex.Replace(value, @"\s+", " "); //replace multiplespaces value = Regex.Replace(value, @"[^a-zA-Z0-9 -]", "").Trim();//remove non-alphanumeric characters and trim spaces if (value.Any(Char.IsWhiteSpace)) { value = PreProcessSearchKey(value); } public static string PreProcessSearchKey(string searchKey) { var splitedKeyWords = searchKey.Split(null); //split from whitespaces // string[] addDoubleQuotes = new string[splitedKeyWords.Length]; for (int j = 0; j < splitedKeyWords.Length; j++) { splitedKeyWords[j] = $"\"{splitedKeyWords[j]}\""; } return string.Join(" AND ", splitedKeyWords); } 

questo metodo utilizza l'operatore logico AND. È ansible passare tale argomento come argomento e utilizzare il metodo per entrambi gli operatori AND o OR.

È necessario sfuggire a caratteri alfanumerici altrimenti altrimenti si genera un'eccezione quando un utente immette caratteri alfanumerici e non è presente alcuna convalida del livello del modello del sito server.