## 1) 問題提起: “え、なんでこんなに無関係な結果が混ざってくるの?” {#sec-b345eb02ca5e} 最近、運営しているブログの投稿数が増えてきて、体系的な管理が必要になってきました。 私のブログは多言語で公開しているため、同じスラッグでも別インスタンスが使われており、記事が増えるほど目的のポストを一発で見つけ出すのが少し大変になってきました。コンテンツが充実している証拠でもありますが。 このように目的のインスタンスを一括で探しにくい原因は、Django Admin の標準検索(`search_fields`)のロジックが**曖昧**だからだと考えました。 ![マジックルーペを持って本を探す図書館の司書](/media/editor_temp/6/12d4b1c2-0844-485d-b9ef-0e1f766b18ca.png) `search_fields` を多く設定すると(機能が増えるほど普通に増えていきます)、検索ボックスに単語一つ入れた瞬間に: * タイトルにヒットし * 説明にヒットし * ユーザー名にヒットし * 余計なメモフィールドにまでヒットし 結果が“たくさん出る”レベルを超えて、“欲しいものが埋もれてしまう”状態になります。運用中に急に特定のドメインだけを探したいとき、結果が数十件に広がると… 正直イライラします。 そこで「**特定のフィールドだけをピンポイントで検索できたら**」という発想が始まりました。 --- ## 2) 解決アイデア: `field:value` 文法 + `shlex` でクオート処理 {#sec-5381e00c4696} アイデアはシンプルです。 * `name:diff` のように**フィールドを指定して**検索する * `title:"system design"` のように**空白を含む検索語**もサポートする ここで `shlex.split()` がとても役立ちます。Admin の検索ボックスは普通「空白で分割」してしまうため、`"system design"` のような入力が崩れてしまいます。`shlex` を使えばシェルと同様にクオートを尊重してトークン化できます。 例: * `title:"system design"` → `["title:system design"]` のようにきれいに処理 * `name:diff python` → `["name:diff", "python"]` つまり、**フィールド指定検索**と**通常検索語**を混在させても綺麗にパースできるわけです。 --- ## 3) コード説明 {#sec-8edf0b65ccd2} 全コードは以下のリポジトリに置いてあります。コピーしてそのまま使えるようにしています。 [GitHub リポジトリへ](https://github.com/mikihands/djangomixins/blob/main/admin_fieldscopedsearch.py) ブログでは要点だけ抜粋して説明します。 ### `_build_lookup_for`: `search_fields` のプレフィックスをそのまま尊重 {#sec-25f0fe79cd1c} Django Admin の `search_fields` にはプレフィックス規則があります。 * `=field` → 完全一致(大文字小文字無視) * `^field` → プレフィックス一致 * デフォルトは `icontains` この規則を**新しい文法を導入しても**そのまま維持したいので、プレフィックスを見て lookup を決める小さなヘルパーを用意しました。 ```python def _build_lookup_for(self, raw_field): if raw_field.startswith('='): return raw_field.lstrip('=^@'), '__iexact' if raw_field.startswith('^'): return raw_field.lstrip('=^@'), '__istartswith' return raw_field.lstrip('=^@'), '__icontains' ``` 例えば `admin.py` で `search_fields = ['=name', 'title']` と書くと * `name` は `iexact` * `title` は `icontains` そして `field:value` でも**同じ lookup 方針**が適用されます。 --- ### `get_search_results`: フィールド指定は AND、通常検索語は既存通り {#sec-2432c8a5949d} 核心ロジックは `get_search_results()` で行います。 1. 入力を `shlex` でトークン化 2. トークンを二つに分類 * `field:value` 形式でかつ `search_fields` に含まれるフィールド → **field_specific_terms** * それ以外 → **normal_terms** 3. フィールド指定検索は **AND** で累積 4. 通常検索語は Django Admin の既存ルールに従い(概念的に) * 各 term は複数フィールドに対して OR * term 同士は AND 重要な部分だけ抜き出すと次のようになります。 ```python terms = shlex.split(search_term or "") field_specs = [self._build_lookup_for(f) for f in getattr(self, 'search_fields', [])] field_lookup_map = {} for name, lookup in field_specs: field_lookup_map.setdefault(name, lookup) field_specific_terms = [] normal_terms = [] for t in terms: if ':' in t: field, value = t.split(':', 1) if field in field_lookup_map and value != '': field_specific_terms.append((field, value)) else: normal_terms.append(t) else: normal_terms.append(t) ``` ここで「存在しないフィールド名」や「空の値」は単なる通常検索語として扱われます。 #### (1) `field:value` は AND で累積 ```python for field, value in field_specific_terms: lookup = field_lookup_map[field] qs = qs.filter(Q(**{f"{field}{lookup}": value})) ``` `name:diff status:active` のような入力は自然に `name == diff AND status == active` と積み重なります。 #### (2) 通常検索語は term ごとに (フィールド OR) → term 同士は AND ```python if normal_terms and field_lookup_map: for term in normal_terms: term_q = Q() for field, lookup in field_lookup_map.items(): term_q |= Q(**{f"{field}{lookup}": term}) qs = qs.filter(term_q) ``` したがって以下の入力が可能です: * `name:diff python` * `name=diff` AND * `(name OR title OR description のいずれかに python が含まれる)` このやり方が良いのは、**既存の Admin 検索感覚を大きく崩さない**点です。`field:value` で意図を明確に絞り込み、残りは従来通り広く検索させることができます。 --- ## 4) 使用方法: Mixin を Admin に組み込むだけで完了 {#sec-0eb3a92e04b6} 使い方はシンプルです。 ```python @admin.register(MyModel) class MyAdmin(FieldScopedSearchMixin, admin.ModelAdmin): search_fields = ['=name', 'title', 'description'] ``` これで Admin の検索ボックスに次のように入力できます。 * `name:diff` * `title:"system design"` * `name:diff python` ヘルプメッセージをカスタマイズしたい場合は `get_changelist_instance()` をオーバーライドすれば自由に変更できます。 --- ## 5) 振り返りと限界: “便利だけど、まだ欲がある” {#sec-4fc456da292d} この Mixin を作って一番嬉しかったのは、運用中に**自分が欲しい検索範囲を自在に調整できるようになった**ことです。実感としての快適さは思った以上に大きく、満足感が得られました。 しかし限界もあります。 * **Full‑text search(@ プレフィックス)** は未対応です。PostgreSQL の full‑text を組み込めばもっと賢くなりますが、設定や重み付け、tsvector の管理が別問題になるので、今回は「運用ストレスの軽減」に注力しました。 * `icontains` が中心なのでデータが大きくなると**パフォーマンス問題**が出やすいです。特に複数フィールドに OR を掛け、複数 term を使うとクエリが重くなります。(実運用では検索語は短くなることが多いですが、最悪ケースは常に存在します。) * 今後挑戦したいことは次の通りです。 * `created_at:2026-01-01..2026-01-31` のような**日付範囲検索** * `status:active,-status:deleted` のような**否定条件** * `field>10` のような**簡易比較演算子** 現状の形は「小さく、確実に使いやすい」地点にピッタリで、非常に気に入っています。運用中に自分がイライラしたポイントを自作ツールで解決できるので、手が頻繁に伸びます。 **少しの工夫とアイデアで業務効率が永続的に向上するのは本当に嬉しいことです。** これこそがコーディングの魅力なのかもしれません。 --- **関連記事** - [admin を今すぐ隠すべき理由](/ko/whitedec/2025/11/10/admin-now-need-to-hide/)