Frappe Search System
Four Search Subsystems
| Subsystem |
Module |
Purpose |
Real-time? |
| Link Field Search |
frappe.desk.search |
Autocomplete in link fields |
Yes |
| Global Search |
frappe.utils.global_search |
Cross-doctype search (desk + web) |
No (15min sync) |
| FullTextSearch |
frappe.search.full_text_search |
Whoosh-based index (website) |
On rebuild |
| SQLiteSearch [v15+] |
frappe.search.sqlite_search |
FTS5 with scoring + spelling |
Yes (5min queue) |
Decision Tree
What search do you need?
│
├─ Link field autocomplete (user types in a Link field)?
│ ├─ Default behavior sufficient → Configure search_fields on DocType
│ └─ Custom logic needed → standard_queries hook or query parameter
│
├─ Cross-doctype search (user searches for anything)?
│ ├─ Desk users → Global Search (auto-enabled)
│ │ └─ Set in_global_search=1 on important fields
│ └─ Website visitors → web_search() or WebsiteSearch (Whoosh)
│
├─ Custom full-text search for your app [v15+]?
│ └─ SQLiteSearch subclass + sqlite_search hook
│ → Spelling correction, recency boost, custom scoring
│
└─ Awesomebar customization?
└─ Client-side: override build_options or use search dialog
Link Field Search
Configuring search_fields (Most Common Need)
# In DocType JSON or via customize form
{
"search_fields": "customer_name, customer_group",
"title_field": "customer_name",
"show_title_field_in_link": 1
}
ALWAYS set search_fields — Without it, users can only search by name (often a code like CUST-001).
How Link Search Works
- User types in link field → calls
search_link(doctype, txt)
- Searches across:
name + title_field + search_fields
- Allowed field types: Data, Text, Small Text, Long Text, Link, Select, Autocomplete, Read Only, Text Editor
- Prefix matches rank higher than substring matches
- Respects
enabled/disabled fields automatically
Custom Link Query
# hooks.py — override search for a specific DocType
standard_queries = {
"Customer": "my_app.queries.customer_query"
}
# my_app/queries.py — MUST be @frappe.whitelist()
@frappe.whitelist()
def customer_query(doctype, txt, searchfield, start, page_length, filters,
as_dict=False, reference_doctype=None,
ignore_user_permissions=False):
# Return list of dicts: [{"value": name, "description": label}, ...]
return frappe.db.sql("""
SELECT name, customer_name as description
FROM `tabCustomer`
WHERE (name LIKE %(txt)s OR customer_name LIKE %(txt)s)
AND status = 'Active'
ORDER BY customer_name
LIMIT %(start)s, %(page_length)s
""", {"txt": f"%{txt}%", "start": start, "page_length": page_length},
as_dict=True)
Per-Field Query Override
// In Client Script or Form JS
frappe.ui.form.on("Sales Order", {
setup(frm) {
frm.set_query("customer", () => ({
filters: { status: "Active", territory: frm.doc.territory }
}));
}
});
Global Search
Enabling
Set in_global_search = 1 on DocType fields that should be searchable.
How It Works
- Indexed fields stored in
__global_search table
- Synced via Redis queue every 15 minutes
- Uses DB-native fulltext: MariaDB
MATCH...AGAINST, PostgreSQL TSVECTOR
- Permission-filtered results
Rebuilding Index
# Rebuild for specific DocType
from frappe.utils.global_search import rebuild_for_doctype
rebuild_for_doctype("Sales Order")
# Rebuild everything
from frappe.utils.global_search import rebuild
rebuild()
hooks.py Configuration
# Default doctypes for global search
global_search_doctypes = {
"Default": [
{"doctype": "Contact"},
{"doctype": "Customer"},
{"doctype": "Sales Order"},
]
}
SQLiteSearch [v15+]
Creating Custom Search
# my_app/search.py
from frappe.search.sqlite_search import SQLiteSearch
class ProjectSearch(SQLiteSearch):
INDEX_SCHEMA = {
"metadata_fields": ["project", "owner", "status"],
"tokenizer": "unicode61 remove_diacritics 2 tokenchars '-_'",
}
INDEXABLE_DOCTYPES = {
"Task": {
"fields": ["name", {"title": "subject"}, {"content": "description"},
"modified", "project"],
"filters": {"status": ("!=", "Cancelled")}
},
"Project": {
"fields": ["name", {"title": "project_name"}, {"content": "notes"},
"modified", "status"],
}
}
def get_search_filters(self, query, scope=None):
"""Permission filtering — return additional WHERE conditions"""
return {}
Register in hooks.py
sqlite_search = ['my_app.search.ProjectSearch']
Features (automatic)
- Spelling correction: Trigram-based fuzzy matching
- Recency boosting: 1.8x (24h) → 1.5x (7d) → 1.2x (30d) → 1.1x (90d)
- Resumable indexing: Progress tracked, atomic replacement
- Auto-scheduling: Build every 3h, queue every 5min, doc events trigger updates
Anti-Patterns
| NEVER |
ALWAYS |
Why |
Omit search_fields on DocType |
Set search_fields for user-friendly names |
Users can't find records by name codes |
Custom query without @frappe.whitelist() |
Decorate with @frappe.whitelist() |
Silently fails — rejected by security check |
| Raw SQL without params in search |
Use parameterized queries (%(txt)s) |
SQL injection risk |
| Index all fields in global search |
Only in_global_search=1 on key fields |
Bloats table, slows 15-min sync |
| Use global search for real-time |
Use link field search for real-time |
Global search has 15-min sync delay |
Skip get_search_filters() in SQLiteSearch |
Implement permission filtering |
Returns all results regardless of access |
| Index cancelled/deleted docs |
Set filters in INDEXABLE_DOCTYPES |
Stale results confuse users |
Version Differences
| Feature |
v14 |
v15+ |
| Link search caching |
-- |
@http_cache(max_age=60) |
link_fieldname param |
-- |
Added |
page_length default |
20 |
10 |
| SQLiteSearch (FTS5) |
-- |
Full implementation |
| Spelling correction |
-- |
Trigram-based |
| Recency boosting |
-- |
Time-based multipliers |
sqlite_search hook |
-- |
Available |
| Global search |
Yes |
Yes |
| Whoosh FullTextSearch |
Yes |
Yes (legacy) |
Reference Files
- Link Search API — search_link, search_widget, custom queries
- Global & Website Search — Global search, WebsiteSearch, SQLiteSearch
1---2name: frappe-core-search3description: Use when implementing search functionality in Frappe v14-v16. Covers link field search (search_link), global search, FullTextSearch (Whoosh), SQLiteSearch FTS5 [v15+], Awesomebar customization, search_fields configuration, custom search queries, and website search. Prevents common mistakes with missing search_fields and permission filtering. Keywords: search, search_link, global_search, FullTextSearch, Awesomebar,, search not finding, link field empty, autocomplete not working, global search missing results. search_fields, standard_queries, SQLiteSearch, FTS5, Whoosh.4license: MIT5---67# Frappe Search System89## Four Search Subsystems1011| Subsystem | Module | Purpose | Real-time? |12|-----------|--------|---------|:----------:|13| **Link Field Search** | `frappe.desk.search` | Autocomplete in link fields | Yes |14| **Global Search** | `frappe.utils.global_search` | Cross-doctype search (desk + web) | No (15min sync) |15| **FullTextSearch** | `frappe.search.full_text_search` | Whoosh-based index (website) | On rebuild |16| **SQLiteSearch** [v15+] | `frappe.search.sqlite_search` | FTS5 with scoring + spelling | Yes (5min queue) |1718---1920## Decision Tree2122```23What search do you need?24│25├─ Link field autocomplete (user types in a Link field)?26│ ├─ Default behavior sufficient → Configure search_fields on DocType27│ └─ Custom logic needed → standard_queries hook or query parameter28│29├─ Cross-doctype search (user searches for anything)?30│ ├─ Desk users → Global Search (auto-enabled)31│ │ └─ Set in_global_search=1 on important fields32│ └─ Website visitors → web_search() or WebsiteSearch (Whoosh)33│34├─ Custom full-text search for your app [v15+]?35│ └─ SQLiteSearch subclass + sqlite_search hook36│ → Spelling correction, recency boost, custom scoring37│38└─ Awesomebar customization?39 └─ Client-side: override build_options or use search dialog40```4142---4344## Link Field Search4546### Configuring search_fields (Most Common Need)4748```python49# In DocType JSON or via customize form50{51 "search_fields": "customer_name, customer_group",52 "title_field": "customer_name",53 "show_title_field_in_link": 154}55```5657**ALWAYS set `search_fields`** — Without it, users can only search by `name` (often a code like `CUST-001`).5859### How Link Search Works60611. User types in link field → calls `search_link(doctype, txt)`622. Searches across: `name` + `title_field` + `search_fields`633. Allowed field types: Data, Text, Small Text, Long Text, Link, Select, Autocomplete, Read Only, Text Editor644. Prefix matches rank higher than substring matches655. Respects `enabled`/`disabled` fields automatically6667### Custom Link Query6869```python70# hooks.py — override search for a specific DocType71standard_queries = {72 "Customer": "my_app.queries.customer_query"73}74```7576```python77# my_app/queries.py — MUST be @frappe.whitelist()78@frappe.whitelist()79def customer_query(doctype, txt, searchfield, start, page_length, filters,80 as_dict=False, reference_doctype=None,81 ignore_user_permissions=False):82 # Return list of dicts: [{"value": name, "description": label}, ...]83 return frappe.db.sql("""84 SELECT name, customer_name as description85 FROM `tabCustomer`86 WHERE (name LIKE %(txt)s OR customer_name LIKE %(txt)s)87 AND status = 'Active'88 ORDER BY customer_name89 LIMIT %(start)s, %(page_length)s90 """, {"txt": f"%{txt}%", "start": start, "page_length": page_length},91 as_dict=True)92```9394### Per-Field Query Override9596```javascript97// In Client Script or Form JS98frappe.ui.form.on("Sales Order", {99 setup(frm) {100 frm.set_query("customer", () => ({101 filters: { status: "Active", territory: frm.doc.territory }102 }));103 }104});105```106107---108109## Global Search110111### Enabling112113Set `in_global_search = 1` on DocType fields that should be searchable.114115### How It Works116117- Indexed fields stored in `__global_search` table118- Synced via Redis queue every 15 minutes119- Uses DB-native fulltext: MariaDB `MATCH...AGAINST`, PostgreSQL `TSVECTOR`120- Permission-filtered results121122### Rebuilding Index123124```python125# Rebuild for specific DocType126from frappe.utils.global_search import rebuild_for_doctype127rebuild_for_doctype("Sales Order")128129# Rebuild everything130from frappe.utils.global_search import rebuild131rebuild()132```133134### hooks.py Configuration135136```python137# Default doctypes for global search138global_search_doctypes = {139 "Default": [140 {"doctype": "Contact"},141 {"doctype": "Customer"},142 {"doctype": "Sales Order"},143 ]144}145```146147---148149## SQLiteSearch [v15+]150151### Creating Custom Search152153```python154# my_app/search.py155from frappe.search.sqlite_search import SQLiteSearch156157class ProjectSearch(SQLiteSearch):158 INDEX_SCHEMA = {159 "metadata_fields": ["project", "owner", "status"],160 "tokenizer": "unicode61 remove_diacritics 2 tokenchars '-_'",161 }162163 INDEXABLE_DOCTYPES = {164 "Task": {165 "fields": ["name", {"title": "subject"}, {"content": "description"},166 "modified", "project"],167 "filters": {"status": ("!=", "Cancelled")}168 },169 "Project": {170 "fields": ["name", {"title": "project_name"}, {"content": "notes"},171 "modified", "status"],172 }173 }174175 def get_search_filters(self, query, scope=None):176 """Permission filtering — return additional WHERE conditions"""177 return {}178```179180### Register in hooks.py181182```python183sqlite_search = ['my_app.search.ProjectSearch']184```185186### Features (automatic)187188- **Spelling correction**: Trigram-based fuzzy matching189- **Recency boosting**: 1.8x (24h) → 1.5x (7d) → 1.2x (30d) → 1.1x (90d)190- **Resumable indexing**: Progress tracked, atomic replacement191- **Auto-scheduling**: Build every 3h, queue every 5min, doc events trigger updates192193---194195## Anti-Patterns196197| NEVER | ALWAYS | Why |198|-------|--------|-----|199| Omit `search_fields` on DocType | Set `search_fields` for user-friendly names | Users can't find records by name codes |200| Custom query without `@frappe.whitelist()` | Decorate with `@frappe.whitelist()` | Silently fails — rejected by security check |201| Raw SQL without params in search | Use parameterized queries (`%(txt)s`) | SQL injection risk |202| Index all fields in global search | Only `in_global_search=1` on key fields | Bloats table, slows 15-min sync |203| Use global search for real-time | Use link field search for real-time | Global search has 15-min sync delay |204| Skip `get_search_filters()` in SQLiteSearch | Implement permission filtering | Returns all results regardless of access |205| Index cancelled/deleted docs | Set `filters` in `INDEXABLE_DOCTYPES` | Stale results confuse users |206207---208209## Version Differences210211| Feature | v14 | v15+ |212|---------|:---:|:----:|213| Link search caching | -- | `@http_cache(max_age=60)` |214| `link_fieldname` param | -- | Added |215| `page_length` default | 20 | 10 |216| SQLiteSearch (FTS5) | -- | Full implementation |217| Spelling correction | -- | Trigram-based |218| Recency boosting | -- | Time-based multipliers |219| `sqlite_search` hook | -- | Available |220| Global search | Yes | Yes |221| Whoosh FullTextSearch | Yes | Yes (legacy) |222223---224225## Reference Files226227- [Link Search API](references/link-search-api.md) — search_link, search_widget, custom queries228- [Global & Website Search](references/global-website-search.md) — Global search, WebsiteSearch, SQLiteSearch