linq entity-framework full-text-search ef-code-first

linq - Marco de entidad, primer código y búsqueda de texto completo



entity-framework full-text-search (5)

Me doy cuenta de que se han formulado muchas preguntas relacionadas con la búsqueda de texto completo y Entity Framework, pero espero que esta pregunta sea un poco diferente.

Estoy utilizando Entity Framework, Code First y necesito hacer una búsqueda de texto completo. Cuando necesito realizar la búsqueda de texto completo, normalmente tendré otros criterios / restricciones también, como omitir las primeras 500 filas o filtrar en otra columna, etc.

Veo que esto se ha manejado utilizando funciones con valores de tabla: consulte http://sqlblogcasts.com/blogs/simons/archive/2008/12/18/LINQ-to-SQL---Enabling-Fulltext-searching.aspx . Y esta parece ser la idea correcta.

Desafortunadamente, las funciones con valores de tabla no son compatibles hasta Entity Framework 5.0 (e incluso entonces, creo que no son compatibles con Code First).

Mi verdadera pregunta es cuáles son las sugerencias para la mejor forma de manejar esto, tanto para Entity Framework 4.3 como para Entity Framework 5.0. Pero para ser específico:

  1. Además del SQL dinámico (a través de System.Data.Entity.DbSet.SqlQuery , por ejemplo), ¿hay alguna opción disponible para Entity Framework 4.3?

  2. Si actualizo a Entity Framework 5.0, ¿hay alguna forma de que pueda usar primero las funciones con valores de tabla?

Gracias, Eric


Como los otros chicos mencionaron, yo diría que comiences a usar Lucene.NET

Lucene tiene una curva de aprendizaje bastante alta, pero encontré un envoltorio llamado " SimpleLucene ", que se puede encontrar en CodePlex

Permítanme citar un par de bloques de código del blog para mostrarles lo fácil que es usarlos. Empecé a usarlo, pero lo entendí muy rápido.

Primero, obtenga algunas entidades de su repositorio, o en su caso, use Entity Framework

public class Repository { public IList<Product> Products { get { return new List<Product> { 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"}, }; } } }

Lo siguiente que quieres hacer es crear una definición de índice

public class ProductIndexDefinition : IIndexDefinition<Product> { 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()); } }

y crea un índice de búsqueda para él.

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

Entonces, ahora tiene un índice de búsqueda. Lo único que queda por hacer es ... ¡buscar! Puedes hacer cosas bastante increíbles, pero puede ser tan fácil como esto: (para obtener más ejemplos, consulta el blog o la documentación en codeplex)

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


Con los interceptores introducidos en EF6, puede marcar la búsqueda de texto completo en linq y luego reemplazarlo en dbcommand como se describe en 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<int> interceptionContext) { } public void NonQueryExecuted(DbCommand command, DbCommandInterceptionContext<int> interceptionContext) { } public void ReaderExecuting(DbCommand command, DbCommandInterceptionContext<DbDataReader> interceptionContext) { RewriteFullTextQuery(command); } public void ReaderExecuted(DbCommand command, DbCommandInterceptionContext<DbDataReader> interceptionContext) { } public void ScalarExecuting(DbCommand command, DbCommandInterceptionContext<object> interceptionContext) { RewriteFullTextQuery(command); } public void ScalarExecuted(DbCommand command, DbCommandInterceptionContext<object> 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<T>(this T source, params T[] list) { return (list as IList<T>).Contains(source); } }

Por ejemplo, si tiene Nota de clase con campo indexado FTS NoteText:

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

y el mapa EF para ello

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

y contexto para ello:

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

puede tener una sintaxis bastante simple para la consulta 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(); } } }

Eso generará SQL como

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)

Tenga en cuenta que debe usar la variable local y no puede mover el contenedor FTS dentro de la expresión como

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


Descubrí que la forma más fácil de implementar esto es configurar y configurar la búsqueda de texto completo en SQL Server y luego usar un procedimiento almacenado. Pase sus argumentos a SQL, permita que el DB haga su trabajo y devuelva un objeto complejo o asigne los resultados a una entidad. No necesariamente tiene que tener SQL dinámico, pero puede ser óptimo. Por ejemplo, si necesita paginación, puede pasar PageNumber y PageSize en cada solicitud sin la necesidad de SQL dinámico. Sin embargo, si la cantidad de argumentos fluctúa por consulta, será la solución óptima.


El ejemplo aquí http://www.entityframework.info/Home/FullTextSearch no es una solución completa. Deberá analizar cómo funciona la búsqueda de texto completo. Imagine que tiene un campo de búsqueda y el usuario escribe 2 palabras para buscar. El código anterior arrojará una excepción. Primero debe realizar el procesamiento previo en la frase de búsqueda para pasarla a la consulta utilizando AND u OR lógico.

por ejemplo, su frase de búsqueda es "blah blah2", entonces necesita convertir esto en:

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

La solución completa sería:

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); }

este método usa AND operador lógico. Puede pasar eso como argumento y usar el método para operadores AND u OR.

Debe evitar los caracteres no alfanuméricos, de lo contrario arrojaría una excepción cuando un usuario ingrese caracteres alfanuméricos y no tenga validada el nivel del modelo del sitio del servidor.


Recientemente tuve un requisito similar y terminé escribiendo una extensión IQueryable específicamente para el acceso de índice de texto completo de Microsoft, aquí está disponible IQueryableFreeTextExtensions