## 1) 問題提出:"為什麼會出現這麼多無關結果?" {#sec-b345eb02ca5e} 最近部落格的文章越來越多,開始需要更有系統的管理。我的部落格支援多語系,同一個 slug 會被不同語言的實例使用,文章數量增加後,要一次精準找出想要的貼文變得有點困難。這雖然是部落格日益豐富的好訊號,但也帶來了搜尋的挑戰。 我認為這個問題的根源在於 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` 可以像 shell 一樣尊重引號,正確切割成 token。 舉例說明: * `title:"system design"` → `["title:system design"]` * `name:diff python` → `["name:diff", "python"]` 也就是說,**欄位指定搜尋**與**一般關鍵字**可以混合使用,且仍能被妥善解析。 --- ## 3) 程式碼說明 {#sec-8edf0b65ccd2} 完整程式碼已上傳至以下倉庫,直接 copy‑paste 即可使用。 [GitHub 倉庫連結](https://github.com/mikihands/djangomixins/blob/main/admin_fieldscopedsearch.py) 以下僅挑出重點說明。 ### `_build_lookup_for`:保留 `search_fields` 前綴規則 {#sec-25f0fe79cd1c} `search_fields` 支援前綴: * `=field` → 完全相等(不分大小寫) * `^field` → 前綴匹配 * 預設為 `icontains` 我希望即使引入新語法,也能保持這些規則。因此寫了一個小 helper 依前綴返回對應的 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` 同樣的 lookup 規則也會套用在 `field:value` 的搜尋上。 --- ### `get_search_results`:欄位指定使用 AND,普通關鍵字仍走原本 OR/AND 邏輯 {#sec-2432c8a5949d} 核心邏輯在 `get_search_results()` 中: 1. 用 `shlex` 把輸入切割成 token 2. 把 token 分成兩類 * 符合 `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) 普通關鍵字保持 (field 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 搜尋的使用感,同時讓欄位指定的搜尋更精準。 --- ## 4) 使用方式:只要在 Admin 加上 Mixin {#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,效果會更好,但需要額外的設定與權重管理,超出本篇範圍。 * 以 `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/)