impertio-studio/frappe-core-search
> 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. search_fields, standard_queries, SQLiteSearch, FTS5, Whoosh.
npx skills add https://github.com/Impertio-Studio/Frappe_Claude_Skill_Package --skill frappe-core-search
| 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) |
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
# 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).
search_link(doctype, txt)name + title_field + search_fieldsenabled/disabled fields automatically# 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)
// 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 }
}));
}
});
Set in_global_search = 1 on DocType fields that should be searchable.
__global_search tableMATCH...AGAINST, PostgreSQL TSVECTOR# 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()
# Default doctypes for global search
global_search_doctypes = {
"Default": [
{"doctype": "Contact"},
{"doctype": "Customer"},
{"doctype": "Sales Order"},
]
}
# 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 {}
sqlite_search = ['my_app.search.ProjectSearch']
| 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 |
| 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) |
Take impertio-studio/frappe-core-search from the repository into ~/.claude/skills for personal
use, or into .claude/skills inside a project.
The agent identifies a skill by the name field in its header. Two skills with the
same name cannot sit side by side — one of them will be ignored.