web-dev-qa-db-fra.com

Entity Framework, Code First et recherche en texte intégral

Je me rends compte que beaucoup de questions ont été posées concernant la recherche en texte intégral et Entity Framework, mais j'espère que cette question est un peu différente.

J'utilise Entity Framework, Code First et dois faire une recherche en texte intégral. Lorsque je dois effectuer la recherche en texte intégral, j'aurai généralement d'autres critères/restrictions - comme ignorer les 500 premières lignes, ou filtrer sur une autre colonne, etc.

Je vois que cela a été géré à l'aide de fonctions de valeur de table - voir http://sqlblogcasts.com/blogs/simons/archive/2008/12/18/LINQ-to-SQL---Enabling-Fulltext-searching .aspx . Et cela semble être la bonne idée.

Malheureusement, les fonctions de valeur de table ne sont pas prises en charge avant Entity Framework 5.0 (et même alors, je crois, elles ne sont pas prises en charge pour Code First).

Ma vraie question est quelles sont les suggestions pour la meilleure façon de gérer cela, à la fois pour Entity Framework 4.3 et Entity Framework 5.0. Mais pour être précis:

  1. Autre que SQL dynamique (via System.Data.Entity.DbSet.SqlQuery, par exemple), existe-t-il des options pour Entity Framework 4.3?

  2. Si je mets à niveau vers Entity Framework 5.0, existe-t-il un moyen d'utiliser d'abord les fonctions table avec du code?

Merci Eric

52
Eric

En utilisant les intercepteurs introduits dans EF6, vous pouvez marquer la recherche de texte intégral dans linq puis la remplacer dans dbcommand comme décrit dans 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);
    }
}

Par exemple, si vous avez une classe Note avec un champ indexé FTS NoteText:

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

et carte EF pour cela

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

et son contexte:

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

vous pouvez avoir une syntaxe assez simple pour la requête 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();
        }
    }
}

Cela va générer du SQL comme

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)   

Veuillez noter que vous devez utiliser une variable locale et ne pouvez pas déplacer le wrapper FTS à l'intérieur d'une expression comme

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

J'ai constaté que la façon la plus simple de mettre en œuvre cela est d'installer et de configurer la recherche en texte intégral dans SQL Server, puis d'utiliser une procédure stockée. Passez vos arguments à SQL, laissez la base de données faire son travail et renvoyez un objet complexe ou mappez les résultats à une entité. Vous n'avez pas nécessairement besoin d'avoir du SQL dynamique, mais il peut être optimal. Par exemple, si vous avez besoin de pagination, vous pouvez passer PageNumber et PageSize à chaque demande sans avoir besoin de SQL dynamique. Cependant, si le nombre d'arguments fluctue par requête, ce sera la solution optimale.

17
Matt

Comme les autres gars l'ont mentionné, je dirais commencer à utiliser Lucene.NET

Lucene a une courbe d'apprentissage assez élevée, mais j'ai trouvé un wrapper pour cela appelé " SimpleLucene ", qui peut être trouvé sur CodePlex

Permettez-moi de citer quelques blocs de code du blog pour vous montrer à quel point il est facile à utiliser. Je viens juste de commencer à l'utiliser, mais j'ai compris très rapidement.

Tout d'abord, récupérez certaines entités de votre référentiel ou, dans votre cas, utilisez 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"},
            };
        }
    }
}

La prochaine chose que vous voulez faire est de créer une définition d'index

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

et créer un index de recherche pour cela.

var writer = new DirectoryIndexWriter(
    new DirectoryInfo(@"c:\index"), true);

var service = new IndexService();
service.IndexEntities(writer, Repository().Products, ProductIndexDefinition());

Donc, vous avez maintenant un index de recherche. La seule chose qui reste à faire est .., en cherchant! Vous pouvez faire des choses assez étonnantes, mais cela peut être aussi simple que cela: (pour de plus grands exemples, voir le blog ou la documentation sur 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);
3
s.meijer

L'exemple ici http://www.entityframework.info/Home/FullTextSearch n'est pas une solution complète. Vous devrez chercher à comprendre comment fonctionne la recherche en texte intégral. Imaginez que vous ayez un champ de recherche et que l'utilisateur tape 2 mots pour lancer la recherche. Le code ci-dessus lèvera une exception. Vous devez d'abord effectuer un prétraitement sur l'expression de recherche pour la transmettre à la requête à l'aide d'un ET logique ou d'un OU.

par exemple, votre phrase de recherche est "bla bla2", vous devez la convertir en:

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

La solution complète serait:

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

cette méthode utilise l'opérateur logique AND. Vous pouvez passer cela comme argument et utiliser la méthode pour les opérateurs AND ou OR.

Vous devez échapper les caractères non alphanumériques, sinon cela lèverait une exception lorsqu'un utilisateur entre des caractères alphanumériques et que vous n'avez aucune validation de niveau de modèle de site de serveur en place.

2
akd

J'ai récemment eu une exigence similaire et j'ai fini par écrire une extension IQueryable spécifiquement pour l'accès aux index de texte intégral Microsoft, son disponible ici IQueryableFreeTextExtensions

1
Robert Ginsburg