我意识到人们提出了很多与全文搜索和实体框架相关的问题,但我希望这个问题有点不同。
我正在使用实体框架,代码优先,需要进行全文搜索。 当我需要执行全文搜索时,我通常还会有其他条件/限制 - 例如跳过前 500 行,或过滤另一列等。
我发现这是使用表值函数处理的 - 请参阅http://sqlblogcasts.com/blogs/simons/archive/2008/12/18/LINQ-to-SQL---Enabling-Fulltext-searching.aspx 。 这似乎是正确的想法。
不幸的是,直到 Entity Framework 5.0 才支持表值函数(即使如此,我相信 Code First 也不支持它们)。
我真正的问题是,对于 Entity Framework 4.3 和 Entity Framework 5.0,处理此问题的最佳方法有哪些建议。 但具体来说:
除了动态 SQL(例如,通过
System.Data.Entity.DbSet.SqlQuery
)之外,还有适用于 Entity Framework 4.3 的其他选项吗?如果我升级到实体框架 5.0,有没有办法可以先通过代码使用表值函数?
谢谢, 艾瑞克
使用 EF6 中引入的拦截器,您可以在 linq 中标记全文搜索,然后在 dbcommand 中替换它,如 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);
}
}
例如,如果您有带有 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")));
我发现实现这一点的最简单方法是在 SQL Server 中设置和配置全文搜索,然后使用存储过程。 将参数传递给 SQL,允许数据库完成其工作并返回复杂对象或将结果映射到实体。 您不一定必须拥有动态 SQL,但它可能是最佳的。 例如,如果您需要分页,您可以在每个请求中传入 PageNumber 和 PageSize,而不需要动态 SQL。 但是,如果每个查询的参数数量发生波动,这将是最佳解决方案。
正如其他人提到的,我会说开始使用 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);
此处的示例 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 运算符。
您必须转义非字母数字字符,否则当用户输入字母数字字符并且您没有适当的服务器站点模型级别验证时,它会引发异常。
我最近有类似的需求,最终专门为 Microsoft 全文索引访问编写了一个 IQueryable 扩展,可在此处获取 IQueryableFreeTextExtensions
如果您使用的是SqlServerfulltext功能,您可以对其进行配置并使用EF默认功能。
使用原始查询
进行配置 //invoke this function after applying your migrations
public static async Task ApplyFullTextSearchIndex(this ApplicationDbContext context)
{
//Default catalogs
var catalogSql = @"
IF NOT EXISTS (SELECT * FROM sys.fulltext_catalogs WHERE name = 'ftCatalog')
BEGIN
CREATE FULLTEXT CATALOG ftCatalog AS DEFAULT;
END";
await context.Database.ExecuteSqlRawAsync(catalogSql);
//Default catalogs
var sql = @"
IF NOT EXISTS (SELECT * FROM sys.fulltext_indexes WHERE object_id = OBJECT_ID('dbo.Contents'))
BEGIN
CREATE FULLTEXT INDEX ON Mytable(mycolumn1, mycolumn2)
KEY INDEX my_index
WITH CHANGE_TRACKING AUTO;
END";
await context.Database.ExecuteSqlRawAsync(sql);
}
您还可以使用迁移来配置它
public partial class AddFullTextIndex : Migration
{
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.Sql("CREATE FULLTEXT CATALOG ftCatalog AS DEFAULT;", suppressTransaction: true);
migrationBuilder.Sql(
"CREATE FULLTEXT INDEX ON YourTableName(yourcolumns) KEY INDEX YourPrimaryKeyIndex;",
suppressTransaction: true);
}
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.Sql("DROP FULLTEXT INDEX ON YourTableName;", suppressTransaction: true);
migrationBuilder.Sql("DROP FULLTEXT CATALOG ftCatalog;", suppressTransaction: true);
}
}
配置完成后,您可以将其与 Ef 功能一起使用
query.Where(x => EF.Functions.FreeText(x.Title, search)