1) 問題提出:"為什麼還會出現這些無關結果?"
最近部落格文章不斷增加,管理需求也隨之上升。因為我的部落格支援多語系,即使 slug 相同,也會有不同實例在使用,文章越多就越難一次定位到想要的貼文。這本是部落格內容豐富的好訊號,但同時也帶來搜尋的困擾。
我發現,Django Admin 的預設搜尋 (search_fields) 之所以會把太多不相關結果混進來,是因為搜尋邏輯過於寬鬆。

當 search_fields 設得很多(功能越多越常見),只要在搜尋框輸入一個字,就會同時匹配到:
- 標題
- 摘要
- 使用者名稱
- 甚至與主題無關的備註欄位
結果不再是「多」到「足夠」的程度,而是「我想找的東西被埋在一大堆噪音裡」的狀況。運營時若急需找出某個特定領域的資料,結果卻被數十筆不相關的項目淹沒,真的會讓人火大。
因此,我開始思考「如果能只針對單一欄位搜尋就好了」的需求。
2) 解決構想:field:value 語法 + shlex 處理引號
概念其實很簡單。
- 透過
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) 程式碼說明
完整程式碼已放在以下倉庫,直接 copy‑paste 即可使用。
[GitHub Repository] (https://github.com/mikihands/djangomixins/blob/main/admin_fieldscopedsearch.py)
以下僅挑出核心概念說明。
_build_lookup_for:保留 search_fields 前綴規則
Django Admin 的 search_fields 支援前綴:
=field→ 完全相等(不分大小寫)^field→ 前綴匹配- 預設則是
icontains
我希望在新語法中仍能遵守這些規則,於是寫了一個小幫手根據前綴回傳對應的 lookup。
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會使用iexacttitle會使用icontains
同樣的 lookup 也會套用在 field:value 的搜尋上。
get_search_results:欄位指定使用 AND、一般關鍵字保留原有 OR/AND 邏輯
主要邏輯寫在 get_search_results() 中:
- 先用
shlex把輸入字串切成 token。 - 把 token 分成兩類
* 符合
field:value且field在search_fields內 → field_specific_terms * 其餘 → normal_terms - 欄位指定的搜尋以 AND 累加
- 一般關鍵字則遵循 Django Admin 的傳統規則: * 每個 term 在所有欄位間採 OR * 各 term 之間採 AND
以下是關鍵程式片段:
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 方式累積
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
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 = diffAND(name OR title OR description) 包含 python
這樣的設計保留了原本 Admin 搜尋的使用感,同時讓 field:value 能精準縮小範圍。
4) 使用方式:只要把 Mixin 加到 Admin 即可
設定非常簡單。
@admin.register(MyModel)
class MyAdmin(FieldScopedSearchMixin, admin.ModelAdmin):
search_fields = ['=name', 'title', 'description']
現在管理介面可以接受以下輸入:
name:difftitle:"system design"name:diff python
若需要客製化說明文字,只要改寫 get_changelist_instance() 即可。
5) 回顧與限制:"雖然已經很好,但我還想要更多"
這個 Mixin 最大的收穫是,讓我在日常運營時能自行調整搜尋範圍,感受到即時的效率提升。
然而它仍有以下限制:
- Full‑text search(@ 前綴) 尚未支援。若結合 PostgreSQL 的 full‑text,搜尋會更聰明,只是需要額外的設定與權重管理。
- 以
icontains為主,資料量大時可能會出現效能問題。特別是多欄位 OR 再加上多個 term,查詢會變得沉重。雖然大多數營運情境下關鍵字都很短,但最壞情況仍須留意。 - 未來想嘗試的功能包括:
created_at:2026-01-01..2026-01-31的日期範圍搜尋status:active,-status:deleted的否定條件field>10的簡易比較運算子
總體而言,這個小工具已經足夠「小而確實」地提升使用體驗。每當我在運營中碰到搜尋卡關的點,它就會馬上派上用場,讓工作流程更順暢。
只要稍微投入一些思考與實作,就能讓日常工作效率永久提升,這也是程式開發的魅力所在。
相關文章
目前沒有評論。