## 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 Repository] (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` 同樣的 lookup 也會套用在 `field:value` 的搜尋上。 --- ### `get_search_results`:欄位指定使用 AND、一般關鍵字保留原有 OR/AND 邏輯 {#sec-2432c8a5949d} 主要邏輯寫在 `get_search_results()` 中: 1. 先用 `shlex` 把輸入字串切成 token。 2. 把 token 分成兩類 * 符合 `field:value` 且 `field` 在 `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 搜尋的使用感,同時讓 `field:value` 能精準縮小範圍。 --- ## 4) 使用方式:只要把 Mixin 加到 Admin 即可 {#sec-0eb3a92e04b6} 設定非常簡單。 ```python @admin.register(MyModel) class MyAdmin(FieldScopedSearchMixin, admin.ModelAdmin): search_fields = ['=name', 'title', 'description'] ``` 現在管理介面可以接受以下輸入: * `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/)