ポータルサイト モデル検索TOP画面

モデルの量がかなり多い、該当するモデルをどう探すかが重要になってくる

検索&コマンドパレットは “モデルのみ” 対象に絞ります。
100–200件規模で“最短でモデルに飛べる”設計を、実装しやすい形でまとめます。

コマンドパレット(モデル専用)

入力 → 候補

  • 対象: モデル技術名account.move)、表示名Journal Entry)、アプリ名/モジュール名(補助)
  • プレフィクスや疑似クエリは最小限:
    • app:account, module:sale, model:stock(任意)
  • 結果アイテム(1行):
    • 左: モデル技術名(太字)
    • 下: 表示名(薄字)
    • 右: バッジ(MENU / FORM / M2O / O2M / M2M)※一覧と同じ判定でOK

操作

  • Enter → モデル詳細ページへ遷移(既存の詳細画面)
  • Alt+Enter → 新規タブで詳細を開く
  • ↑/↓ 選択、Esc 閉じる
  • 履歴: 直近アクセス上位5件を先頭にピン留め(学習なしでも体感が速い)

検索スコア(軽量・実用)

優先度の高い順に加点し、上位から表示:

  1. 前方一致account. で始まる等)
  2. 完全一致(技術名 or 表示名)
  3. 部分一致(n-gram/fuzzyは任意)
  4. タイブレーク用ブースト(同点時)
    • MENUあり +1
    • FORMあり +1
    • refs_in 高いほど微加点(親になりやすい)
    • 最近開いた/最近更新 +微加点

200件なら Postgres の ILIKE + 簡易前方一致で十分高速です。後から trigram に差し替え可能。


一覧との役割分担(モデルのみ)

  • 一覧(中央テーブル): 絞り込み・俯瞰
  • コマンドパレット: 直行・ジャンプ

必要列(一覧と共通化し、同じメタでパレットも描画)

  • model(技術名)
  • display_name(表示名)
  • app / module
  • バッジ: has_menu, has_form, has_m2o, has_o2m, has_m2m
  • refs_in / refs_out
  • updated_at

最小API設計(モデル専用)

検索API(パレット/一覧共通)

GET /search/models?q=move&app=account&module=account,account_payment&limit=20

レスポンス(例)

[
  {
    "model": "account.move",
    "display_name": "Journal Entry",
    "app": "Accounting",
    "module": "account",
    "badges": ["MENU","FORM","M2O","O2M"],
    "refs": {"in": 12, "out": 7},
    "updated_at": "2025-08-20T08:30:00Z",
    "score": 8.7
  }
]

一覧API(ページ初期表示)

GET /models?app=account&has_menu=true&sort=menu,refs_in,updated_at

取得ロジック(簡潔SQLの芯)

モデル索引(マテビュー推奨)





CREATE MATERIALIZED VIEW IF NOT EXISTS portal_model_index AS
WITH owners AS (
  SELECT im.model, im.name AS display_name, imd.module AS module
  FROM ir_model im
  JOIN ir_model_data imd ON imd.model='ir.model' AND imd.res_id=im.id
),
apps AS (
  SELECT name AS module, shortdesc AS app
  FROM ir_module_module
  WHERE state='installed'
),
views AS (
  SELECT iv.model, 
         COUNT(*) FILTER (WHERE iv.type='form') AS form_views
  FROM ir_ui_view iv
  GROUP BY iv.model
),
menus AS (
  SELECT aw.res_model AS model, COUNT(*) AS menu_cnt
  FROM ir_ui_menu m
  JOIN ir_act_window aw ON aw.id = split_part(m.action, ',', 2)::int
  WHERE m.action LIKE 'ir.actions.act_window,%'
  GROUP BY aw.res_model
),
rels_out AS (
  SELECT m.model, COUNT(*) AS refs_out
  FROM ir_model m JOIN ir_model_fields f ON f.model_id=m.id
  WHERE f.relation IS NOT NULL GROUP BY m.model
),
rels_in AS (
  SELECT f.relation AS model, COUNT(*) AS refs_in
  FROM ir_model_fields f
  WHERE f.relation IS NOT NULL GROUP BY f.relation
)
SELECT
  o.model,
  o.display_name,
  COALESCE(a.app, o.module) AS app,
  o.module,
  (COALESCE(m.menu_cnt,0) > 0)    AS has_menu,
  (COALESCE(v.form_views,0) > 0)  AS has_form,
  EXISTS(SELECT 1 FROM ir_model_fields f JOIN ir_model m2 ON m2.id=f.model_id 
         WHERE m2.model=o.model AND f.ttype='many2one')  AS has_m2o,
  EXISTS(SELECT 1 FROM ir_model_fields f JOIN ir_model m2 ON m2.id=f.model_id 
         WHERE m2.model=o.model AND f.ttype='one2many')  AS has_o2m,
  EXISTS(SELECT 1 FROM ir_model_fields f JOIN ir_model m2 ON m2.id=f.model_id 
         WHERE m2.model=o.model AND f.ttype='many2many') AS has_m2m,
  COALESCE(ri.refs_in,0)  AS refs_in,
  COALESCE(ro.refs_out,0) AS refs_out,
  NOW()::timestamp         AS updated_at  -- 必要なら write_date 集計に差し替え
FROM owners o
LEFT JOIN apps a   ON a.module = o.module
LEFT JOIN menus m  ON m.model  = o.model
LEFT JOIN views v  ON v.model  = o.model
LEFT JOIN rels_in ri  ON ri.model  = o.model
LEFT JOIN rels_out ro ON ro.model  = o.model;
SELECT *, 
  (CASE WHEN model ILIKE $q||'%' THEN 3
        WHEN display_name ILIKE $q||'%' THEN 2
        WHEN model ILIKE '%'||$q||'%' THEN 1
        WHEN display_name ILIKE '%'||$q||'%' THEN 1
        ELSE 0 END)
  + (CASE WHEN has_menu THEN 0.5 ELSE 0 END)
  + (CASE WHEN has_form THEN 0.5 ELSE 0 END)
  + LEAST(refs_in/10.0, 1.0)*0.5 AS score
FROM portal_model_index
WHERE (model ILIKE '%'||$q||'%' OR display_name ILIKE '%'||$q||'%')
  AND ($app IS NULL OR app = $app)
  AND ($modules IS NULL OR module = ANY($modules))
ORDER BY score DESC
LIMIT 20;

小さなUXルール

  • 候補の最大表示は7件(速く決めやすい)
  • 最近アクセス3件は常に上に(薄い区切り線で)
  • 候補の右端に 「↗」 アイコン(新タブで開くヒント)
  • ヒット件数が0なら即「フィルタ解除」ショートカットを提案

まとめ

  • 検索&コマンドパレットはモデルのみを対象に最小設計
  • 前方一致と小さなブーストで“体感最速”を実現
  • 一覧と同じ軽量メタを使いまわし、実装と運用を簡素化

「タイブレーク用ブースト」というのは、検索結果でスコアが同点になったときに、より“重要そうなモデル”を上に出すための追加評価のことです。
もう少し具体的に、なぜ必要でどう使うかを解説します。


1. 何を解決するためのブーストか

  • モデルは 100~200件 程度 → 検索結果の重複は多い
  • 例えば move で検索した場合:
    • account.move
    • stock.move
    • hr.expense.move
  • 単純な部分一致だとどれを上に出すべきか判断できない
    → そこで、重要度に応じたスコア加点を入れて、モデルの並び順を最適化します。

2. ブースト項目の意味

① MENUあり +1

  • 意味:Odoo画面のメニューから直接アクセスできるモデルは“ユーザー操作で重要”。
  • 理由
    • ir_ui_menuir_act_window.res_model に存在するモデル
    • 例:
      • account.move(仕訳入力画面がある)→ +1点
      • account.partial.reconcile(内部処理用モデル)→ +0点
  • 効果
    • “画面で使うモデル”を上に出すので、開発者も探しやすい

② FORMあり +1

  • 意味:formビューがあるモデルは“UIの中心”。
  • 理由
    • ir_ui_viewtype='form' があるモデルは、編集・表示画面がある
    • 例:
      • sale.order → フォームビューがある → +1点
      • sale.order.line → 明細モデルだがフォームビューなし → +0点
  • 効果
    • “操作対象”のモデルを優先的に出せる

③ refs_in 高いほど微加点(親になりやすい)

  • 意味:他のモデルから参照されている数(Many2oneの“受け側”)が多いモデルは“親モデル”。
  • 理由
    • ir_model_fields.relation何回参照されているかをカウント
    • 例:
      • res.partner → 20以上のモデルから参照 → +0.5点
      • res.country.state → 3件だけ参照 → +0.1点
  • 効果
    • “基幹モデル”を優先して表示できる
    • 例:res.partneraccount.moveproduct.product など

④ 最近開いた / 最近更新 +微加点

  • 意味:ユーザーがよく使っている or 最近変わったモデルを上に出す。
  • 2種類ある
    1. 最近開いた
      • ポータルで開いたモデルをローカルにキャッシュ
      • ユーザーごとによく使うモデルを上位表示
    2. 最近更新
      • ir_model.write_dateir_model_fields.write_dateのMAXを利用
      • 最近ビューやフィールドが更新されたモデルを上に出す
  • 効果
    • “今まさに触っているモデル”がすぐ出てくる

3. 実際のスコア例

例:ユーザーが move で検索した場合

モデル部分一致MENUFORMrefs_in最近開いた合計スコア
account.move3+1+1+0.4+0.35.7
stock.move3+1+1+0.2+0.05.2
hr.expense.move300+0.1+0.03.1

→ 検索キーワードでは3モデルとも同点だけど、実務上もっとも重要な account.move が1位に来ます。

SELECT *,
    -- 検索マッチ度(3点満点)
    (CASE WHEN model ILIKE $q||'%' THEN 3
          WHEN display_name ILIKE $q||'%' THEN 2
          WHEN model ILIKE '%'||$q||'%' THEN 1
          WHEN display_name ILIKE '%'||$q||'%' THEN 1
          ELSE 0 END)
    -- MENU + FORMブースト
    + (CASE WHEN has_menu THEN 1 ELSE 0 END)
    + (CASE WHEN has_form THEN 1 ELSE 0 END)
    -- refs_in比率(最大0.5点)
    + LEAST(refs_in / 20.0, 0.5)
    -- 最近開いたモデル(ユーザー単位のキャッシュ)
    + COALESCE(recent_score, 0.3)
AS score
FROM portal_model_index
WHERE model ILIKE '%'||$q||'%'
   OR display_name ILIKE '%'||$q||'%'
ORDER BY score DESC
LIMIT 20;

5. まとめ

  • ブースト=同点時に“より重要そう”なモデルを上げる仕組み
  • 優先度の判断基準:
    1. MENUあり → UIで使うモデルを優先
    2. FORMあり → 操作対象モデルを優先
    3. refs_in多い → 基幹モデルを優先
    4. 最近開いた/更新 → “今触る可能性が高い”モデルを優先
  • 100~200件規模なら、このブーストを足すと**「欲しいモデルが上に来る」**体験になる

Comments

コメントを残す

メールアドレスが公開されることはありません。 が付いている欄は必須項目です