52 votes

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

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

Je suis en utilisant Entity Framework Code First et le besoin de faire une recherche plein texte. Quand j'en ai besoin pour effectuer la recherche en texte intégral, je vais généralement avoir d'autres critères/restrictions ainsi - comme ignorer les 500 premières lignes, ou un filtre sur une autre colonne, etc.

Je vois que cela a été gérée à l'aide de fonctions à 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 à valeur de table ne sont pas pris en charge jusqu'à Entity Framework 5.0 (et même alors, je crois, ils ne sont pas pris en charge pour le Premier Code).

Ma vraie question est de savoir quelles sont les suggestions sur 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) Autres que SQL dynamique (via le Système.Les données.De l'entité.DbSet.SqlQuery, par exemple), il y a toutes les options disponibles pour l'Entité Cadre 4.3?

(2) Si j'ai de la mise à niveau de l'Entité Cadre 5.0, est-il une manière que je peux utiliser des fonctions à valeur de table avec le premier code?

Merci, Eric

51voto

Ben Points 975

À l'aide de séparateurs introduit dans EF6, vous pouvez marquer la recherche plein texte dans linq et puis le 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 de la classe de Note avec FTS-champ indexé NoteText:

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

et EF carte pour elle

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

et 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 assez d'une syntaxe simple pour FTS de requête:

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

Que va générer le 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 peut se déplacer FTS wrapper à l'intérieur de l'expression comme

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

17voto

Matt Points 586

J'ai trouvé que la façon la plus simple à mettre en œuvre c'est pour installer et configurer plein de recherche de texte dans SQL Server, puis utiliser une procédure stockée. Passer vos arguments pour SQL, permettre à la DB pour faire son travail et retourne un objet complexe ou de la carte les résultats d'une entité. Vous n'avez pas nécessairement besoin d'avoir le SQL dynamique, mais il peut être optimal. Par exemple, si vous avez besoin de pagination, vous pourriez passer dans PageNumber et PageSize à chaque requête sans la nécessité pour le SQL dynamique. Toutefois, si le nombre d'arguments fluctue par requête, il sera la solution optimale.

3voto

s.meijer Points 522

Comme les autres gars mentionné, je dirais de commencer à utiliser Lucene.NET

Lucene a une assez haute de la courbe d'apprentissage, mais j'ai trouvé un wrapper pour qu'il appelait "SimpleLucene", qui peut être trouvé sur CodePlex

Permettez-moi de citer un couple de codeblocks, le blog pour vous montrer comment il est facile à utiliser. Je viens de commencer à l'utiliser, mais il a obtenu le coup de lui très vite.

Tout d'abord, obtenir certaines entités à partir de votre référentiel, ou dans votre cas, l'utilisation d'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 un index-définition

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

Oui, vous avez maintenant une recherche-mesure de l'indice. La seule chose restant à faire est de.., de la recherche! Vous pouvez faire des choses assez étonnantes, mais il peut être aussi facile que cela: (pour plus d'exemples, voir le blog ou de 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);

1voto

Robert Ginsburg Points 59

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, elle est disponible ici IQueryableFreeTextExtensions

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X