0
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

プリザンターの検索機能を詳しくみてみる[第1回:全体像とデータフロー編]

0
Posted at

はじめに

プリザンターを使っていて、こんな経験はありませんか。

  • 更新したはずのレコードが検索に引っかからない
  • ヘッダの検索ボックスでは見つかるのに、一覧の検索ボックスでは見つからない
  • データが増えてきたら一覧の検索が急に遅くなった

これらはすべて「プリザンターの検索が内部でどう動いているか」を知ると説明がつきます。

この連載では、検索ボックスに入れた文字が最終的にどんな SQL になって返ってくるのかを、
入口から出口まで通しで追っていきます。全7回の予定です。

回 内容
第1回(本記事) 全体像とデータフロー
第2回 Items.FullText 列がどう作られるか
第3回 RDBMS 別の全文検索 SQL
第4回 一覧の検索と絞り込み
第5回 横断検索とその他の検索
第6回 添付ファイル全文検索の仕組みと限界
第7回 内部実装から導く実践編

初回は地図を描くところから始めましょう。

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

検索の入口は4つある

まず押さえておきたいのは、プリザンターの「検索」はひとつではないということです。
UI 上で検索と呼ばれているものは、実装としては4系統に分かれています。

入口 UI 上の場所 実装の入口 最終的に見ている列
横断検索 ヘッダの検索ボックス Indexes.Search / SearchJson Items.FullText
一覧の検索 一覧上部の検索ボックス View.Search → SetSearchWhere Items.FullText / Items.Title
項目ごとの絞り込み フィルタ列 View.ColumnFilterHash 各テーブルの実カラム
選択肢の検索 リンク項目のドロップダウン SearchDropDown リンク先の Items.Title

「ヘッダの検索では出るのに一覧の検索では出ない」という現象は、
そもそも見ている列も条件式も違うために起こります。
この連載では第4回で一覧の検索、第5回で横断検索とドロップダウン検索を扱います。

検索の土台は Items.FullText 列

上の表を見ると、横断検索と一覧の検索はどちらも Items.FullText という列を見ています。
プリザンターの検索を理解する鍵はここにあります。

プリザンターのレコード実体は、テーブルの種類によって
Issues・Results・Wikis・Sites・Dashboards に分かれて格納されています。
ところが検索は、それらのテーブルを直接見にいきません。
かわりに Items テーブルの FullText 列へ、検索用のテキストを1本の文字列として展開して持たせています。

いわゆる非正規化です。なぜこうしているのかというと、理由がいくつも重なっています。

  • 拡張項目が横に長い(ClassA〜ClassZ、NumA〜NumZ など)ため、
    実テーブルを対象にすると検索条件が列数分だけ膨れ上がる
    (Enterprise Edition の項目拡張を使っている環境ではさらに増える)
  • 参照タイプが5種類あるため、テーブルごとに別の SQL を書くことになる
  • 横断検索では、異種テーブルのレコードを1回のクエリでまとめて舐めたい

Items に文字列を1本持たせておけば、テーブルの種類を問わず同じ条件式で検索できます。
横断検索が「テーブルをまたいで一気に探せる」のは、この設計のおかげです。

Items テーブルの検索関連列

列 役割
ReferenceId レコードの ID(IssueId / ResultId など)
ReferenceType 参照タイプ(Issues / Results / Wikis / Sites / Dashboards)
SiteId 所属サイト
Title レコードのタイトル
FullText 検索用に展開したテキスト
SearchIndexCreatedTime FullText を生成した時刻

SearchIndexCreatedTime は地味ですが重要な列です。
これを UpdatedTime と比較することで「インデックスが古いレコード」を判定しています。

インデックスが作られる3つの経路

Items.FullText は自動で最新になるわけではなく、3つの経路で生成されます。

経路 契機 制御するパラメータ
同期生成 レコードの作成・更新時 Search.json の CreateIndexes(既定 true)
バックグラウンド 生成時刻が未設定・不一致のものを拾う BackgroundTask.json の CreateSearchIndexLot(既定 100)
手動再構築 サイトの設定の「検索インデックスの再構築」 なし

実際に Items.FullText を書き込んでいるのは、以下のメソッドです。

Implem.Pleasanter/Libraries/Search/Indexes.cs
private static void CreateFullText(Context context, long id, string fullText)
{
    if (fullText != null)
    {
        Repository.ExecuteNonQuery(
            context: context,
            statements: Rds.UpdateItems(
                where: Rds.ItemsWhere().ReferenceId(id),
                param: Rds.ItemsParam()
                    .FullText(fullText)
                    .SearchIndexCreatedTime(DateTime.Now),
                addUpdatorParam: false,
                addUpdatedTimeParam: false));
    }
}

addUpdatorParam: false と addUpdatedTimeParam: false が付いているところがポイントです。
インデックスを作り直しても更新者と更新日時は変わらないようになっています。

CreateIndexes を false にすると何が起きるか

Search.json の CreateIndexes を false にすると、同期生成が止まります。
各モデルの FullText() メソッドの先頭に、こういう判定が入っています。

Implem.Pleasanter/Models/Issues/IssueModel.cs
if (!Parameters.Search.CreateIndexes && !backgroundTask) return null;

null が返ると、先ほどの CreateFullText() は何も書き込みません。
ただし backgroundTask: true で呼ばれた場合は素通りするので、
バックグラウンド生成と手動再構築は CreateIndexes の設定に関係なく動きます。

大量データを一気に投入するときだけ CreateIndexes を false にして、
あとからまとめて再構築するという運用が可能なのは、この作りのおかげです。

Search.json のパラメータ一覧

検索の挙動は App_Data/Parameters/Search.json でまとめて制御されています。
既定値は以下のとおりです。

App_Data/Parameters/Search.json
{
    "SearchDocuments": false,
    "CreateIndexes": true,
    "PageSize": 20,
    "DisableCrossSearch": false,
    "DisableCrossSearchSites": false,
    "FullTextIncludeBreadcrumb": false,
    "FullTextIncludeSiteId": false,
    "FullTextIncludeSiteTitle": true,
    "FullTextNumberOfMails": 10,
    "FullTextMaxNumberOfMails": 100
}

それぞれの意味と、この連載でどこで詳しく扱うかを整理しました。

パラメータ 既定 内容 詳しく扱う回
SearchDocuments false 添付ファイルの中身も検索対象にするか 第6回
CreateIndexes true 作成・更新時にインデックスを生成するか 本記事
PageSize 20 横断検索の1ページあたり件数 第5回
DisableCrossSearch false 横断検索そのものを無効化するか 第5回
DisableCrossSearchSites false テーブル定義自体を結果から外すか 第5回
FullTextIncludeBreadcrumb false パンくずをインデックスに含めるか 第2回
FullTextIncludeSiteId false サイト ID を含めるか 第2回
FullTextIncludeSiteTitle true サイトのタイトルを含めるか 第2回
FullTextNumberOfMails 10 インデックスに含める送信メール数 第2回
FullTextMaxNumberOfMails 100 送信メール数の上限 第2回

パラメータクラスの定義もシンプルで、JSON のキーと1対1で対応しています。

Implem.ParameterAccessor/Parts/Search.cs
public class Search
{
    public bool SearchDocuments;
    public bool CreateIndexes;
    public int PageSize;
    public bool DisableCrossSearch;
    public bool DisableCrossSearchSites;
    public bool FullTextIncludeBreadcrumb;
    public bool FullTextIncludeSiteId;
    public bool FullTextIncludeSiteTitle;
    public int FullTextNumberOfMails;
    public int FullTextMaxNumberOfMails;
}

ソースコードの地図

最後に、この連載で読んでいくファイルを一覧にしておきます。

ファイル 役割
Libraries/Search/Indexes.cs 検索の中核。1112 行
Libraries/Extensions/FullTextExtensions.cs 型ごとの FullText 生成
Libraries/Extensions/SearchIndexExtensions.cs 検索語の分割
Libraries/Settings/View.cs 一覧の検索・列フィルタ
Libraries/Settings/SiteSettings.cs サイト単位の検索方式
Libraries/Settings/Column.cs 項目単位の検索方式
Rds/Implem.SqlServer/SqlServerCommandText.cs SQL Server 用の条件式
Rds/Implem.PostgreSql/PostgreSqlCommandText.cs PostgreSQL 用の条件式
Rds/Implem.MySql/MySqlCommandText.cs MySQL 用の条件式

検索処理の大半は Indexes.cs に集まっています。
第2回以降はこのファイルを軸に読み進めていきます。

診断してみましょう

初回の締めに、自環境の状態を確認する SQL を紹介します。
インデックスが古いままのレコードが何件あるかを数えるものです。

select count(*) from "Items"
where "SearchIndexCreatedTime" is null
   or "SearchIndexCreatedTime" <> "UpdatedTime";

この件数がゼロでないなら、バックグラウンド生成が追いついていないか、
CreateIndexes が false になっているか、どちらかです。
CreateSearchIndexLot の既定は 100 件なので、
大量に投入した直後はしばらくゼロにならないのが正常な動きです。

まとめ

初回の要点を振り返ります。

  • プリザンターの検索は入口が4系統あり、見ている列も条件式も異なります
  • 横断検索と一覧の検索は、どちらも Items.FullText という非正規化された1本の文字列を見ています
  • Items.FullText は自動で最新になるわけではなく、同期生成・バックグラウンド・手動再構築の3経路で作られます
  • SearchIndexCreatedTime と UpdatedTime の比較が、インデックスの鮮度判定に使われています
  • 検索の挙動は Search.json の10個のパラメータでまとめて制御されています

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

0
1
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
0
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?