实体框架,代码优先和全文搜索

时间:2012-07-23 16:09:10

标签: linq entity-framework full-text-search ef-code-first

我意识到有很多关于全文搜索和实体框架的问题,但我希望这个问题有点不同。

我正在使用Entity Framework,Code First,需要进行全文搜索。当我需要执行全文搜索时,我通常还会有其他条件/限制 - 例如跳过前500行,或过滤其他列等等。

我看到这是使用表值函数处理的 - 请参阅http://sqlblogcasts.com/blogs/simons/archive/2008/12/18/LINQ-to-SQL---Enabling-Fulltext-searching.aspx。这似乎是正确的想法。

不幸的是,在实体框架5.0之前不支持表值函数(我认为,即使这样,Code First也不支持它们)。

我真正的问题是,对于Entity Framework 4.3和Entity Framework 5.0,有哪些最佳方法可以解决这个问题。但具体来说:

  1. 除了动态SQL(例如,通过System.Data.Entity.DbSet.SqlQuery),是否有可用于Entity Framework 4.3的选项?

  2. 如果我升级到Entity Framework 5.0,有没有办法可以首先使用表值函数和代码?

  3. 谢谢, 埃里克

5 个答案:

答案 0 :(得分:51)

使用EF6中引入的拦截器,您可以在linq中标记全文搜索,然后按照http://www.entityframework.info/Home/FullTextSearch中的描述在dbcommand中替换它:

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

例如,如果您有使用FTS索引字段NoteText:

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

和EF映射

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

及其背景:

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

您可以使用非常简单的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();
        }
    }
}

这将生成类似

的SQL
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)   

请注意,您应该使用局部变量,并且不能在表达式中移动FTS包装器,如

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

答案 1 :(得分:17)

我发现最简单的实现方法是在SQL Server中设置和配置全文搜索,然后使用存储过程。将您的参数传递给SQL,允许DB完成其工作并返回复杂对象或将结果映射到实体。您不一定必须拥有动态SQL,但它可能是最佳的。例如,如果您需要分页,则可以在每个请求中传递 PageNumber PageSize ,而无需动态SQL。但是,如果每个查询的参数数量波动,那么它将是最佳解决方案。

答案 2 :(得分:3)

正如其他人提到的,我会说开始使用Lucene.NET

Lucene的学习曲线相当高,但我找到了一个名为“SimpleLucene”的包装器,可以在CodePlex上找到

让我引用博客中的几个代码块来向您展示它的易用性。我刚开始使用它,但很快就掌握了它。

首先,从您的存储库中获取一些实体,或者在您的情况下,使用实体框架

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"},
            };
        }
    }
}

您要做的下一件事是创建索引定义

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

并为其创建搜索索引。

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

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

所以,你现在有一个可搜索的索引。剩下要做的唯一事情是..,搜索!你可以做一些非常棒的事情,但它可以像这样简单:(更多例子见博客或关于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 :(得分:2)

这里的示例http://www.entityframework.info/Home/FullTextSearch不是完整的解决方案。您需要了解全文搜索的工作原理。想象一下,你有一个搜索字段,用户输入2个单词来搜索。上面的代码将引发异常。您需要首先对搜索短语进行预处理,然后使用逻辑AND或OR将其传递给查询。

例如,您的搜索短语是“blah blah2”,那么您需要将其转换为:

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

完整的解决方案是:

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

此方法使用AND逻辑运算符。您可以将其作为参数传递,并将该方法用于AND或OR运算符。

您必须转义非字母数字字符,否则当用户输入字母数字字符并且您没有服务器站点模型级别验证时它会抛出异常。

答案 4 :(得分:1)

我最近有类似的要求,最后专门为Microsoft全文索引访问编写了一个IQueryable扩展,可在此处IQueryableFreeTextExtensions