НейроАгент

Поиск без учета регистра в EF Core 9 с SQLite

Решение проблемы регистронезависимого поиска для русского языка в EF Core 9 с SQLite. Узнайте эффективные методы настройки пользовательских сортировок и полнотекстового поиска.

Как в EF Core 9 произвести поиск совпадений на русском и без учета регистра?

Использую SQLite.
Если поиск на английском то все хорошо. Но вот на русском поиск совпадений чувствителен к регистру.
То что я пытался и не работает:
Result = await _context.Podcasts
//1) .Where(p => p.Title.StartsWith(value))
//2) .Where(p => p.Title.ToLower().StartsWith(value.ToLower()))
//3) .Where(p => EF.Functions.Like(p.Title, “{value}%”))
//4) .Where(p => EF.Functions.Like(p.Title.ToLower(), “{value.ToLower()}%”))
//5) .FromSqlRaw(“SELECT * FROM Podcasts WHERE LOWER(Title) LIKE LOWER({0})”, “{value}%”)
.ToListAsync();

НейроАгент

Для реализации поиска без учета регистра для русского языка в EF Core 9 с SQLite необходимо использовать специализированные подходы, так как стандартные методы не работают корректно с кириллицей. Основные решения включают настройку пользовательских правил сортировки (collations), использование полнотекстового поиска FTS3/FTS4 или применение функций преобразования на уровне базы данных.

Содержание

Основные проблемы поиска на русском языке

SQLite по умолчанию чувствителен к регистру для всех символов, включая кириллицу. Как указано в исследованиях, стандартная функция COLLATE NOCASE работает корректно только для ASCII-символов, но не для кириллических букв.

Это означает, что ваши попытки использовать стандартные методы:

csharp
// Эти методы не работают для русского языка
.Where(p => p.Title.ToLower().StartsWith(value.ToLower()))
.Where(p => EF.Functions.Like(p.Title, "{value}%"))

не будут давать ожидаемого результата для русского текста. Проблема заключается в том, что SQLite LOWER() функция работает только с ASCII, а кириллические символы требуют специальной обработки.


Способ 1: Настройка пользовательской сортировки (Collation)

Наиболее эффективным решением является создание пользовательской сортировки, которая корректно обрабатывает кириллицу.

Реализация через DbContext

csharp
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
    if (!optionsBuilder.IsConfigured)
    {
        optionsBuilder.UseSqlite($"Data Source=podcasts.db");
        
        // Настройка пользовательской сортировки для SQLite
        var connection = optionsBuilder.UseSqlite().Options.FindExtension<SqliteOptions>()?.Connection;
        if (connection != null)
        {
            connection.Open();
            try
            {
                connection.CreateCollation("RUSSIAN_NOCASE", (x, y) =>
                {
                    if (x == null && y == null) return 0;
                    if (x == null) return -1;
                    if (y == null) return 1;
                    
                    return string.Compare(x, y, StringComparison.OrdinalIgnoreCase);
                });
            }
            finally
            {
                connection.Close();
            }
        }
    }
}

Использование в запросах

csharp
Result = await _context.Podcasts
    .Where(p => EF.Functions.Collate(p.Title, "RUSSIAN_NOCASE").StartsWith(value))
    .ToListAsync();

Этот подход позволяет создать собственное правило сортировки, которое корректно обрабатывает русский язык в соответствии с рекомендациями Microsoft.Data.Sqlite.


Способ 2: Использование FTS3/FTS4 для полнотекстового поиска

Для полнотекстового поиска с поддержкой русского языка рекомендуется использовать виртуальные таблицы FTS3 или FTS4.

Создание виртуальной таблицы

csharp
// Создание виртуальной таблицы FTS4
using var command = _context.Database.GetDbConnection().CreateCommand();
command.CommandText = @"
    CREATE VIRTUAL TABLE IF NOT EXISTS Podcasts_fts 
    USING fts4(Title, content='Podcasts', tokenize='unicode61');
";
command.ExecuteNonQuery();

// Заполнение таблицы данными
command.CommandText = @"
    INSERT INTO Podcasts_fts(rowid, Title) 
    SELECT rowid, Title FROM Podcasts WHERE rowid NOT IN (SELECT rowid FROM Podcasts_fts);
";
command.ExecuteNonQuery();

Поиск через FTS

csharp
Result = await _context.Podcasts
    .FromSqlRaw("SELECT p.* FROM Podcasts p JOIN Podcasts_fts f ON p.rowid = f.rowid WHERE f.Title MATCH {0}", value)
    .ToListAsync();

Этот метод особенно эффективен для сложных поисковых операций, как указано в Stack Overflow обсуждении.


Способ 3: Глобальная настройка сортировки в EF Core

Можно настроить глобальную сортировку для всех строковых свойств в модели.

Конфигурация в OnModelCreating

csharp
protected override void ConfigureConventions(ModelConfigurationBuilder configurationBuilder)
{
    configurationBuilder.Properties<string>().UseCollation("NOCASE");
}

Однако, как отмечено в GitHub issue, этот подход может не работать корректно для всех случаев, особенно для LIKE операций. Для русского языка потребуется дополнительная настройка.


Способ 4: Клиентская фильтрация

Для небольших наборов данных можно использовать фильтрацию на стороне приложения.

csharp
Result = await _context.Podcasts
    .AsNoTracking()
    .ToListAsync();

// Фильтрация в памяти
Result = Result.Where(p => 
    CultureInfo.CurrentCulture.CompareInfo.IndexOf(
        p.Title, 
        value, 
        CompareOptions.IgnoreCase | CompareOptions.IgnoreNonSpace
    ) >= 0
).ToList();

Этот метод прост в реализации, но неэффективен для больших объемов данных, так как все данные загружаются в память.


Примеры реализации

Комбинированный подход с LINQ

csharp
public async Task<List<Podcast>> SearchPodcastsAsync(string value)
{
    // Сначала выполняем поиск с учетом регистра через базу данных
    var dbResults = await _context.Podcasts
        .Where(p => p.Title.Contains(value))
        .Take(1000) // Ограничение для предотвращения загрузки всех данных
        .ToListAsync();
    
    // Затем фильтруем на стороне приложения без учета регистра
    return dbResults.Where(p => 
        CultureInfo.CurrentCulture.CompareInfo.IndexOf(
            p.Title, 
            value, 
            CompareOptions.IgnoreCase | CompareOptions.IgnoreNonSpace
        ) >= 0
    ).ToList();
}

Использование EF.Functions.Collate

csharp
Result = await _context.Podcasts
    .Where(p => EF.Functions.Collate(p.Title, "NOCASE").StartsWith(value))
    .ToListAsync();

Пример с пользовательской сортировкой

csharp
// Настройка в DbContext
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    modelBuilder.Entity<Podcast>()
        .Property(p => p.Title)
        .HasCollation("RUSSIAN_NOCASE");
}

// Использование в запросах
Result = await _context.Podcasts
    .Where(p => p.Title.StartsWith(value))
    .ToListAsync();

Рекомендации по выбору подхода

Подход Преимущества Недостатки Рекомендации
Пользовательская сортировка Высокая производительность, встроенная поддержка EF Core Требует дополнительной настройки Лучший выбор для большинства случаев
FTS3/FTS4 Отличная производительность поиска, поддержка сложных запросов Требует отдельной таблицы, сложнее в настройке Идеально для каталогов и больших объемов данных
Глобальная сортировка Простота настройки Может не работать для всех операций Подходит для простых случаев
Клиентская фильтрация Простота реализации Низкая производительность на больших данных Только для небольших наборов данных

Для большинства приложений с использованием SQLite и русского языка рекомендуется пользовательская сортировка (Collation), как описано в первом способе. Это обеспечивает оптимальный баланс между производительностью и простотой реализации.


Заключение

  1. Основная проблема - стандартные методы EF Core и SQLite не поддерживают корректный поиск без учета регистра для русского языка

  2. Наиболее эффективное решение - создать пользовательскую сортировку RUSSIAN_NOCASE через CreateCollation в OnConfiguring

  3. Альтернативные подходы включают использование FTS3/FTS4 для полнотекстового поиска или комбинирование базы данных и клиентской фильтрации

  4. Для оптимальной производительности рекомендуется настраивать сортировку на уровне базы данных, а не загружать все данные в память

  5. Тестирование обязательно проводить с реальными данными на русском языке для проверки корректности работы выбранного подхода

Для получения дополнительной информации всегда консультируйтесь с официальной документацией Microsoft.Data.Sqlite и EF Core Collations.

Источники

  1. Collations and case sensitivity - EF Core | Microsoft Learn
  2. SQLite case insensitive search for Russian characters - Stack Overflow
  3. Another implementation of case-insensitive search for Cyrillic characters in SQLite - Sudo Null
  4. 5 ways to implement case-insensitive search in SQLite with full Unicode support - ShallowDepth
  5. Collation - Microsoft.Data.Sqlite | Microsoft Learn
  6. Setting global collation for SQLite doesn’t set collation for individual columns - GitHub
  7. UTF8 case insensitive like operator in sqlite - GitHub
  8. Entity Framework core - Contains is case sensitive or case insensitive? - Stack Overflow
  9. Entity Framework Core. SQLite — COLLATE NOCASE - Medium
  10. EF Core Advanced Topics - Collations and Case Sensitivity - RiT Tutorial