SQL Server EF Core プロバイダーでの Full-Text 検索

SQL Server には、単純な パターンを超えた高度なテキスト検索を可能にするLIKEが用意されています。 フルテキスト検索では、言語マッチング、変曲形式、近接検索、加重ランク付けがサポートされます。

EF Core の SQL Server プロバイダーは、フルテキスト検索 述語 (フィルター処理用) と テーブル値関数 (ランク付けによるフィルター処理) の両方をサポートしています。

フルテキスト検索を使用する前に、データベースに フルテキスト カタログ を作成し、検索する列に フルテキスト インデックス を作成する必要があります。

注

移行でのフルテキスト カタログとインデックス管理は、EF Core 11 で導入されました。

フルテキスト カタログとインデックスは、EF モデルで直接構成できます。 移行を追加すると、EF はカタログとインデックスを作成 (または変更) するための適切な SQL を生成します。

まず、モデルでフルテキスト カタログを定義し、次にエンティティ型にフルテキスト インデックスを構成します。

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    modelBuilder.HasFullTextCatalog("ftCatalog");

    modelBuilder.Entity<Article>()
        .HasFullTextIndex(a => a.Contents)
        .UseKeyIndex("PK_Articles")
        .UseCatalog("ftCatalog");
}

UseKeyIndex() メソッドは、テーブルのフルテキスト キー (通常は主キー インデックス) として使用される、null 非許容の一意の単一列インデックスを指定します。 UseCatalog() は、フルテキスト インデックスを特定のカタログに割り当てます。

複数の列と、列ごとの言語や変更の追跡などの追加オプションを構成することもできます。

modelBuilder.Entity<Article>()
    .HasFullTextIndex(a => new { a.Title, a.Contents })
    .UseKeyIndex("PK_Articles")
    .UseCatalog("ftCatalog")
    .HasChangeTracking(FullTextChangeTracking.Manual)
    .UseLanguage("Title", "English")
    .UseLanguage("Contents", "French");

フルテキスト カタログは、既定のカタログとして設定でき、アクセントの区別を考慮することも可能です。

modelBuilder.HasFullTextCatalog("ftCatalog")
    .IsDefault()
    .IsAccentSensitive(false);

詳細については、 SQL Server のフルテキスト検索に関するドキュメントを参照してください。

フルテキスト述語

EF Core では、 FREETEXT() 述語と CONTAINS() 述語がサポートされています。これは、結果をフィルター処理するために Where() 句で使用されます。

FREETEXT()

FREETEXT() は、単語の意味に基づいて、動詞の時制や名詞の複数形など、さまざまな形態の単語を検索しながら、より緩やかな一致を行います。

var articles = await context.Articles
    .Where(a => EF.Functions.FreeText(a.Contents, "veggies"))
    .ToListAsync();

これは次のようになります。

SELECT [a].[Id], [a].[Title], [a].[Contents]
FROM [Articles] AS [a]
WHERE FREETEXT([a].[Contents], N'veggies')

必要に応じて、言語用語を指定できます。

var articles = await context.Articles
    .Where(a => EF.Functions.FreeText(a.Contents, "veggies", "English"))
    .ToListAsync();

CONTAINS() (指定した要素や値を含むかを確認する関数)

CONTAINS() では、より正確な一致が実行され、プレフィックス用語、近接検索、加重用語など、より高度な検索条件がサポートされます。

// Simple search
var articles = await context.Articles
    .Where(a => EF.Functions.Contains(a.Contents, "veggies"))
    .ToListAsync();

// Prefix search (words starting with "vegg")
var articles = await context.Articles
    .Where(a => EF.Functions.Contains(a.Contents, "\"vegg*\""))
    .ToListAsync();

// Phrase search
var articles = await context.Articles
    .Where(a => EF.Functions.Contains(a.Contents, "\"fresh vegetables\""))
    .ToListAsync();

これは次のようになります。

SELECT [a].[Id], [a].[Title], [a].[Contents]
FROM [Articles] AS [a]
WHERE CONTAINS([a].[Contents], N'veggies')

CONTAINS()クエリ構文の詳細については、SQL Server CONTAINS のドキュメントを参照してください。

フルテキスト テーブル値関数

注

フルテキスト テーブル値関数は、EF Core 11 で導入されています。

上記の述語はフィルター処理に役立ちますが、ランク付け情報は提供されません。 SQL Server のテーブル値関数 FREETEXTTABLE() および CONTAINSTABLE() は、一致する行と、各行が検索クエリとどの程度一致するかを示すランク付けスコアの両方を返します。

FreeTextTable()

FreeTextTable() は、 FreeText()のテーブル値関数バージョンです。 エンティティとランク付け値の両方を含む FullTextSearchResult<TEntity>が返されます。

var results = await context.Articles
    .Join(
        context.Articles.FreeTextTable<Article, int>("veggies", topN: 10),
        a => a.Id,
        ftt => ftt.Key,
        (a, ftt) => new { Article = a, ftt.Rank })
    .OrderByDescending(r => r.Rank)
    .ToListAsync();

foreach (var result in results)
{
    Console.WriteLine($"Article {result.Article.Id} with rank {result.Rank}");
}

ジェネリック型パラメーターを指定する必要があることに注意してください。 Article は、検索対象のエンティティの種類に対応します。ここで、 int はインデックスの作成時に指定されたフルテキスト検索キーであり、 FREETEXTTABLE()によって返されます。

上記では、フルテキスト検索に登録されているすべての列が自動的に検索され、上位 10 件の一致が返されます。 検索する特定の列を指定することもできます。

var results = await context.Articles
    .Join(
        context.Articles.FreeTextTable<Article, int>("veggies", a => a.Contents),
        a => a.Id,
        ftt => ftt.Key,
        (a, ftt) => new { Article = a, ftt.Rank })
    .OrderByDescending(r => r.Rank)
    .ToListAsync();

...または複数の列:

var results = await context.Articles
    .FreeTextTable("veggies", a => new { a.Title, a.Contents })
    .Select(r => new { Article = r.Value, Rank = r.Rank })
    .OrderByDescending(r => r.Rank)
    .ToListAsync();

ContainsTable()

ContainsTable() はテーブル値関数バージョンの Contains()であり、同じ高度な検索構文をサポートすると同時に、ランク付け情報も提供します。

var results = await context.Articles
    .Join(
        context.Articles.ContainsTable<Article, int>( "veggies OR fruits"),
        a => a.Id,
        ftt => ftt.Key,
        (a, ftt) => new { Article = a, ftt.Rank })
    .OrderByDescending(r => r.Rank)
    .ToListAsync();

結果を制限する

両方のテーブル値関数は、結果の数を制限するために topN パラメーターをサポートしています。

var results = await context.Articles
    .FreeTextTable("veggies", a => a.Contents, topN: 10)
    .Select(r => new { Article = r.Value, Rank = r.Rank })
    .OrderByDescending(r => r.Rank)
    .ToListAsync();

言語の指定

両方のテーブル値関数では、言語一致のための言語用語の指定がサポートされています。

var results = await context.Articles
    .FreeTextTable("veggies", a => a.Contents, languageTerm: "English")
    .Select(r => new { Article = r.Value, Rank = r.Rank })
    .ToListAsync();

述語とテーブル値関数のどちらを使用するか

特徴 述語 (FreeText()、 Contains()) テーブル値関数 (FreeTextTable()、 ContainsTable())
ランキングを提供します ❌ いいえ ✅ はい
大規模な結果セットのパフォーマンス フィルター処理に適しています 順位付けと並べ替えに適しています
他のエンティティとの結合 結合を使用する 組み込みエンティティの結果
Where()句で使用する ✅ はい ❌ いいえ (ソースとして使用)

フルテキスト検索条件に基づいて結果をフィルター処理する必要がある場合は、述語を使用します。 ランク付け情報が必要な場合、テーブル値関数を使用して関連性に基づいて結果を並べたり、ユーザーに関連性スコアを表示したりします。