Entity Framework, Code First и полнотекстовый поиск
Я понимаю, что было задано много вопросов, касающихся полнотекстового поиска и Entity Framework, но я надеюсь, что этот вопрос немного другой.
Я использую Entity Framework, Code First и мне нужно выполнить полнотекстовый поиск. Когда мне нужно выполнить полнотекстовый поиск, у меня обычно есть и другие критерии / ограничения - например, пропустить первые 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?Если я обновлюсь до Entity Framework 5.0, могу ли я сначала использовать функции с табличными значениями в коде?
Спасибо эрик
5 ответов
Используя перехватчики, представленные в 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);
}
}
Например, если у вас есть класс Note с индексированным FTS полем NoteText:
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
Позвольте мне процитировать пару кодовых блоков из блога, чтобы показать вам, как легко им пользоваться. Я только начал использовать его, но быстро освоился.
Во-первых, получите некоторые сущности из вашего хранилища или, в вашем случае, используйте 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"},
};
}
}
}
Следующее, что вы хотите сделать, это создать определение индекса
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 слова для поиска. Приведенный выше код вызовет исключение. Сначала необходимо выполнить предварительную обработку поисковой фразы, чтобы передать ее в запрос с помощью логического И или ИЛИ.
Например, ваша поисковая фраза - "бла-бла-2", тогда вам нужно преобразовать это в:
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. Вы можете передать это в качестве аргумента и использовать метод для операторов И или ИЛИ.
Вы должны экранировать не алфавитно-цифровые символы, иначе будет выдано исключение, когда пользователь вводит буквенно-цифровые символы, а у вас нет проверки уровня модели сайта сервера.
Недавно у меня было похожее требование, и в итоге я написал расширение IQueryable специально для доступа к полнотекстовым индексам Microsoft, его можно найти здесь IQueryableFreeTextExtensions