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 即可使用。
以下僅挑出重點說明。
_build_lookup_for:保留 search_fields 前綴規則
search_fields 支援前綴:
=field→ 完全相等(不分大小寫)^field→ 前綴匹配- 預設為
icontains
我希望即使引入新語法,也能保持這些規則。因此寫了一個小 helper 依前綴返回對應的 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且欄位在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 搜尋的使用感,同時讓欄位指定的搜尋更精準。
4) 使用方式:只要在 Admin 加上 Mixin
@admin.register(MyModel)
class MyAdmin(FieldScopedSearchMixin, admin.ModelAdmin):
search_fields = ['=name', 'title', 'description']
現在 Admin 搜尋框可以接受以下寫法:
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的簡易比較運算子
目前的實作已經足夠「小而確實」地提升工作效率。每當我在運營中因搜尋卡住時,這個工具就會派上用場,感受到一點點程式碼改動帶來的長期效益,真的很令人欣慰。
相關文章