1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

プリザンターの検索機能を詳しくみてみる[第2回:FullText列の生成編]

1
Last updated at Posted at 2026-09-09

はじめに

前回は、プリザンターの検索が Items.FullText という1本の文字列を土台にしていることを確認しました。

今回はその Items.FullText が具体的に何をどう連結して作られているのかを読んでいきます。
「更新したのに検索に引っかからない」という現象の答えは、ほとんどこの回にあります。

先に結論だけ書いておくと、検索に載らない原因は次の3つに集約されます。

  1. その項目が編集画面に配置されていない
  2. その項目の全文検索の設定が なし のままになっている
  3. 設定を変えたが既存レコードのインデックスを再構築していない

順番に見ていきましょう。

バージョン 1.5.7.0 を対象にしています

model.FullText() を読む

Items.FullText の中身を組み立てているのは、各モデルの FullText() メソッドです。
IssueModel を例に見ていきます。

Implem.Pleasanter/Models/Issues/IssueModel.cs
public string FullText(
    Context context,
    SiteSettings ss,
    bool backgroundTask = false,
    bool onCreating = false)
{
    if (!Parameters.Search.CreateIndexes && !backgroundTask) return null;
    if (AccessStatus == Databases.AccessStatuses.NotFound) return null;
    var fullText = new System.Text.StringBuilder();
    if (ss.FullTextIncludeBreadcrumb == true)
    {
        // パンくずを連結
    }
    if (ss.FullTextIncludeSiteId == true)
    {
        fullText.Append($" {ss.SiteId}");
    }
    if (ss.FullTextIncludeSiteTitle == true)
    {
        fullText.Append($" {ss.Title}");
    }
    // 以下、項目ごとの連結が続く

StringBuilder に空白区切りで値を積んでいくだけの、素朴な作りです。
組み立ての順番は次のようになっています。

先頭に付く3つの任意情報

パラメータ 既定 連結される内容
FullTextIncludeBreadcrumb false 親サイトをたどったパンくず
FullTextIncludeSiteId false サイト ID の数値
FullTextIncludeSiteTitle true サイトのタイトル

FullTextIncludeSiteTitle だけが既定で有効です。
そのため横断検索でサイト名を入れると、そのサイトのレコードが全部ヒットします。
意図した挙動ではあるのですが、検索結果がノイズだらけになる場合はここを疑ってみてください。

なお、これらの既定値は Search.json から SiteSettings に流れ込んだうえで、
サイトごとに上書きできる形になっています。

Implem.Pleasanter/Libraries/Settings/SiteSettings.cs
FullTextIncludeBreadcrumb = FullTextIncludeBreadcrumb ?? Parameters.Search.FullTextIncludeBreadcrumb;

末尾に付く送信メール

見落としがちですが、送信メールの内容もインデックスに入ります。

Implem.Pleasanter/Libraries/Extensions/FullTextExtensions.cs
if (ss.FullTextNumberOfMails > 0)
{
    new OutgoingMailCollection(
        context: context,
        where: Rds.OutgoingMailsWhere()
            .ReferenceType(referenceType)
            .ReferenceId(referenceId),
        orderBy: Rds.OutgoingMailsOrderBy()
            .OutgoingMailId(orderType: SqlOrderBy.Types.desc),
        top: ss.FullTextNumberOfMails.ToInt())
            .ForEach(o =>
            {
                // From / To / Cc / Bcc / Title / Body を連結
            });
}

宛先やメール本文まで含まれるので、
メール送信機能を使っているサイトでは Items.FullText がかなり大きくなります。
不要なら FullTextNumberOfMails を 0 にすれば連結処理そのものがスキップされます。

onCreating が true のとき(レコード作成時)は、この処理は呼ばれません。
作成時点ではまだメールが存在しないためです。

最後の正規化が地味に効いている

FullText() の戻り値を作る最終処理を見てみましょう。ここが面白いところです。

Implem.Pleasanter/Models/Issues/IssueModel.cs
return fullText
    .ToString()
    .Replace(" ", " ")
    .Replace("\r", " ")
    .Replace("\n", " ")
    .Split(' ')
    .Select(o => o.Trim())
    .Where(o => o != string.Empty)
    .Distinct()
    .Join(" ");

全角スペースと改行を半角スペースに変換したうえで分割し、
空要素を除いて Distinct() してから再結合しています。

つまり Items.FullText は、元の文章そのものではなく、重複を除いた語の集合です。

入力 Items.FullText に入る値
対応 完了 対応 完了 対応 完了
改行を<br>含む本文 改行を 含む本文

ここから2つのことが言えます。

  • 同じ単語が何度出てきても1回しか格納されないので、出現回数による重み付けはできません
  • 空白や改行で区切られた位置は保持されないので、改行をまたぐフレーズ検索は期待どおりに動きません

日本語は単語をスペースで区切らないため、日本語の本文はほぼそのまま1語として格納されます。
「本文の途中の単語で検索できるかどうか」は、このあと第3回で見る RDBMS 側の分割方式に委ねられています。

落とし穴1:編集画面に配置された項目だけが対象

項目の値を連結する部分は、こういうループになっています。

Implem.Pleasanter/Models/Issues/IssueModel.cs
ss.GetEditorColumnNames(
    context: context,
    columnOnly: true)
        .Select(columnName => ss.GetColumn(
            context: context,
            columnName: columnName))
        .ForEach(column =>
        {
            switch (column.ColumnName)
            {
                case "Title":
                    Title.FullText(
                        context: context,
                        column: column,
                        fullText: fullText);
                    break;
                // 以下、項目ごとに続く
            }
        });

対象は GetEditorColumnNames() が返す列、つまり編集画面に配置されている項目だけです。

ここが最初の落とし穴です。

一覧には表示しているけれど編集画面には配置していない項目は、Items.FullText に入りません。
つまり検索できません。

「一覧に出ているのに検索でヒットしない」という相談は、たいていこれが原因です。
検索したい項目は、たとえ画面上で使わないとしても編集画面に配置したうえで非表示にするのが正解です。
配置から外してしまうと、インデックスの対象から消えてしまいます。

落とし穴2:拡張項目の全文検索は既定で無効

次の落とし穴はもっと分かりにくいものです。
項目ごとの全文検索の扱いは Column.FullTextType で決まります。

Implem.Pleasanter/Libraries/Settings/Column.cs
public enum FullTextTypes : int
{
    None = 0,
    DisplayName = 1,
    Value = 2,
    ValueAndDisplayName = 3
}

この値が設定されていないときは、列定義の既定値が使われます。

Implem.Pleasanter/Libraries/Settings/SiteSettings.cs
column.FullTextType = column.FullTextType ?? (Column.FullTextTypes)columnDefinition.FullTextType;

そこで列定義のファイルを確認してみると、こうなっています。

App_Data/Definitions/Definition_Column/_BaseItems_Body.json
{
  "Id": "_BaseItems_Body",
  "ColumnName": "Body",
  "FullTextType": "1"
}

Title や Body、Manager、Owner、Status などの標準項目には FullTextType が明示されています。
ところが ClassA〜ClassZ、NumA〜、DescriptionA〜、AttachmentsA〜 といった
拡張項目の列定義には FullTextType の記載がありません。

記載がなければ既定値の 0、つまり None になります。

項目 既定の FullTextType
Title / Body DisplayName(1)
Manager / Owner / Status DisplayName(1)
ClassA〜ClassZ None(0)
NumA〜NumZ None(0)
DescriptionA〜DescriptionZ None(0)
AttachmentsA〜AttachmentsZ None(0)

拡張項目は、サイトの設定で明示的に指定しない限り全文検索の対象になりません。

Enterprise Edition の項目拡張で Class001〜Class999 のような項目を増やした場合も、
既定の FullTextType は同じく None です。

設定場所は「サイトの設定」→「項目」→ 対象項目の詳細 →「全文検索」です。
選択肢は ColumnUtilities.FullTextTypeOptions() で作られており、
なし / 表示名 / 値 / 値と表示名 の4つです。

Implem.Pleasanter/Libraries/Settings/ColumnUtilities.cs
public static Dictionary<string, string> FullTextTypeOptions(Context context)
{
    return new Dictionary<string, string>
    {
        { Column.FullTextTypes.None.ToInt().ToString(), Displays.None(context: context) },
        { Column.FullTextTypes.DisplayName.ToInt().ToString(), Displays.DisplayName(context: context) },
        { Column.FullTextTypes.Value.ToInt().ToString(), Displays.Value(context: context) },
        { Column.FullTextTypes.ValueAndDisplayName.ToInt().ToString(), Displays.ValueAndDisplayName(context: context) }
    };
}

FullTextTypes の4種類をどう選ぶか

表示名 と 値 の違いは、項目の型によって意味が変わります。
FullTextExtensions.cs の型ごとの実装を見ると判断できます。

ユーザ項目・分類項目

Implem.Pleasanter/Libraries/Extensions/FullTextExtensions.cs
case Column.FullTextTypes.DisplayName:
    fullText
        .Append(" ")
        .Append(user.Anonymous()
            ? string.Empty
            : user.Name);
    break;
case Column.FullTextTypes.Value:
    fullText
        .Append(" ")
        .Append(user.Anonymous()
            ? string.Empty
            : user.Id.ToString());
    break;

表示名 ならユーザ名、値 ならユーザ ID が入ります。
氏名で検索したいなら 表示名 が必須です。値 を選ぶと数値の ID しか入りません。

分類項目も同様で、選択肢の表示値とコード値のどちらを入れるかが変わります。
コードでも名称でも引っかけたい場合は 値と表示名 を選びます。

添付ファイル項目

添付ファイル項目も FullTextType を持っています。

Implem.Pleasanter/Libraries/Extensions/FullTextExtensions.cs
public static void FullText(
    this Attachments self,
    Context context,
    Column column,
    StringBuilder fullText)
{
    if (self != null)
    {
        switch (column?.FullTextType)
        {
            case Column.FullTextTypes.DisplayName:
                self?.ForEach(o =>
                    fullText
                        .Append(" ")
                        .Append(o.Name));
                break;
            case Column.FullTextTypes.Value:
                self?.ForEach(o =>
                    fullText
                        .Append(" ")
                        .Append(o.Guid));
                break;
            case Column.FullTextTypes.ValueAndDisplayName:
                self?.ForEach(o =>
                    fullText
                        .Append(" ")
                        .Append(o.Guid)
                        .Append(" ")
                        .Append(o.Name));
                break;
        }
    }
}

ここに入るのは ファイル名(表示名)か GUID(値) です。
ファイルの中身は入りません。中身の検索はまったく別の仕組みなので、第6回で扱います。

添付ファイル項目も拡張項目なので既定は None です。
つまり既定ではファイル名でも検索できません。
表示名 にしておくと、保存先や RDBMS の種類を問わずファイル名で検索できるようになります。

コメント

コメントは投稿者名と本文の両方が入ります。

Implem.Pleasanter/Libraries/Extensions/FullTextExtensions.cs
self.ForEach(o =>
    fullText
        .Append(" ")
        .Append(SiteInfo.UserName(
            context: context,
            userId: o.Creator))
        .Append(" ")
        .Append(o.Body));

コメント本文が検索対象に入るのは便利ですが、
やりとりが長いレコードでは Items.FullText が膨らむ要因になります。

落とし穴3:設定変更は既存レコードに遡及しない

FullTextType を変更しても、既存レコードの Items.FullText は変わりません。
生成されるのは次に更新されたときです。

そこで使うのが「検索インデックスの再構築」です。実装は2モードあります。

Implem.Pleasanter/Libraries/Search/Indexes.cs
where: siteId > 0
    ? Rds.ItemsWhere().SiteId(siteId)
    : Rds.ItemsWhere().Add(raw: new List<string>()
    {
        "\"Items\".\"SearchIndexCreatedTime\" is null",
        "\"Items\".\"SearchIndexCreatedTime\"<>\"Items\".\"UpdatedTime\""
    }.Join(" or ")),
top: siteId > 0
    ? 0
    : Parameters.BackgroundTask.CreateSearchIndexLot))

モード 対象 件数上限
siteId > 0(サイト指定) そのサイトの全レコード なし
siteId <= 0(バックグラウンド) 生成時刻が未設定または UpdatedTime と不一致 CreateSearchIndexLot 件

サイトの設定から実行するボタンは前者、バックグラウンドタスクは後者です。
サイト指定は上限なしで全件を回すので、
レコード数が多いサイトでは実行時間に注意してください。

もうひとつ、再構築の前に権限のハッシュを全サイト分埋めている箇所があります。

Implem.Pleasanter/Libraries/Search/Indexes.cs
context.PermissionHash = Rds.ExecuteTable(
    context: context,
    statements: Rds.SelectSites(
        column: Rds.SitesColumn().SiteId()))
            .AsEnumerable()
            .ToDictionary(
                o => o.Long("SiteId"),
                o => Permissions.Types.Read);

インデックスの生成にはリンク先の表示名の解決などが必要で、
そのために権限チェックを通す必要があります。
「インデックス生成のためだけに全サイト読み取り可として扱う」という割り切った実装です。

検索できない項目を検索できるようにする手順

ここまでの内容をまとめると、手順は3ステップになります。

  1. 対象項目を編集画面に配置する(使わない項目なら非表示にする)
  2. 項目の詳細で 「全文検索」を 表示名 以上に設定 する
  3. サイトの設定から 「検索インデックスの再構築」を実行 する

3番目を忘れると「新しく更新したレコードだけ検索できる」という中途半端な状態になります。

まとめ

今回の要点です。

  • Items.FullText は StringBuilder に値を空白区切りで積み、最後に Distinct() して作られる語の集合です
  • 対象になるのは GetEditorColumnNames() が返す列、つまり編集画面に配置された項目だけです
  • 拡張項目の FullTextType の既定は None なので、明示設定しない限り検索対象になりません
  • ユーザ項目や分類項目では、表示名 と 値 のどちらを選ぶかで検索できるキーワードが変わります
  • 添付ファイル項目に入るのはファイル名か GUIDで、ファイルの中身は入りません
  • 設定変更は既存レコードに遡及しないため、再構築が必要です

次回は、こうして作られた Items.FullText に対して
RDBMS ごとにまったく違う SQL が生成されるところを見ていきます。

1
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?