Discourse search is better than most people think, and also weirder. The obvious operators are documented in the UI and on Meta's search guide, but the real fun starts when you read the source. I went through lib/search.rb and found a dozen features that are easy to miss, mildly strange, or both.
This is not a complete reference. It is the useful stuff that surprised me.
1. #foo is not just a hashtag search
When you search for something like #bug, Discourse does not immediately treat it as plain text. It first tries to resolve it as a category slug, then as a tag, then as a tag group, and only after that falls back to searching for literal text containing that token. You can see that decision tree in the category/tag handler in search.rb.
That means #support might search a category, while #solved might search a tag, even though both look like the same kind of query. It is a compact little polymorphic operator, which is either elegant or a bit cheeky depending on your tolerance for overloaded syntax.
2. #parent:child jumps straight to a subcategory
The same #... syntax also understands parent:child category slugs. So #feature:ux can directly target a subcategory without needing the more verbose category: form. That logic is in the same slug parser.
If you already know your category slugs, this is the fastest way to narrow a search. It feels almost command-line-ish, which I approve of.
3. Prefix a category with = to stop Discourse including subcategories
This is one of the best hidden search features in the file. Category filters are inclusive by default: if you search category:support, Discourse will include posts from that category and its subcategories. But if you want the category only, use =:
category:support— support plus its subcategoriescategory:=support— support only
That exact-match behavior is implemented in the category:/categories: filter, and the same idea exists in the #slug parser.
This is the sort of thing that saves you ten minutes of confusion and then annoys you that nobody told you earlier.
4. Tag search has two modes: comma means OR, plus means AND
Discourse supports two different tag combination styles in the same operator:
tags:bug,uxmeans topics with either tagtags:bug+uxmeans topics with both tags
That split happens in search_tags. The + path builds a combined full-text query over aggregated tag names; the comma path does a simpler set inclusion check.
If you are trying to find a very specific cluster of topics, the + version is vastly better than throwing more keywords into the text query and hoping relevance ranking does something sensible.
5. Negative tag filters exist, and they are excellent
Discourse also supports excluding tags:
-tag:solved-tags:legacyperformance status:open -tags:solved
That operator is easy to miss because people assume a leading minus only works in full-text search. In Discourse it is a first-class advanced filter, implemented in the negative tag matcher.
This is one of the few operators that makes search results feel immediately more adult.
6. before: and after: accept more than dates
The obvious forms work:
before:2026-03-01after:2025-12
But the parser also accepts several less obvious forms via word_to_date:
after:30— after 30 days agobefore:yesterdayafter:mondaybefore:january
The month-name behavior is particularly odd in a good way: before:january resolves to the beginning of the most recent January, not some vague calendar concept. The filters themselves are wired up here.
7. @me works
The username shorthand is not limited to actual usernames. In the @username filter, Discourse special-cases @me and maps it to the current user when possible.
That gives you neat little queries like:
@me in:replies@me with:images@me after:30
It is one of those details that makes search feel less like SQL in costume and more like a tool designed for humans.
8. There are a lot more in: filters than most people realise
The public guides cover the common ones, but the source has a larger set. A few especially useful ones:
in:taggedin:untaggedin:wikiin:pinnedin:likesin:bookmarksin:watchingin:trackingin:postedin:createdandin:mine
Several of these are personal-state queries rather than text queries. That is why they feel powerful: you are searching your relationship to the content, not just the content itself.
9. in:seen and in:unseen search your read history
This pair is better than it sounds. Discourse joins against post_timings to distinguish posts you have viewed from posts you have not, as shown in the seen/unseen filters.
Useful examples:
postgres in:unseensecurity in:seen after:30
This turns search into a backlog triage tool. On busy forums, that is much more valuable than another syntax for finding old trivia.
10. group: and group_messages: are completely different
These two look related, but they search different things:
group:stafffinds posts written by users in that groupgroup_messages:stafffinds messages sent to that group
The distinction is visible in the source: group: filters by authorship through group_users, while group_messages: filters PM topics through topic_allowed_groups.
Same noun, different universe. Very Discourse.
11. with:images is narrower than it sounds
You might assume with:images means “posts containing any image-like thing.” It is more specific than that. The filter checks whether image_upload_id is present on the post, which you can see in the single-line matcher.
If what you really want is “posts containing attached files,” use filetypes: instead:
filetypes:pdffiletypes:png,jpgfiletypes:zip,log
That operator searches both linked file extensions and uploaded file extensions via the filetypes: filter.
12. The one-letter shortcuts are real
Discourse quietly supports a few one-letter search shorthands in process_advanced_search!:
l=order:latestr=order:readt=in:titlef=in:first
Examples:
redis tmemory leak fpostgres l
These are not earth-shattering, but they are delightfully terse. They also tell you something about the history of the feature: power-user shortcuts accumulate the way shell aliases do.
A small but nice extra: search cleans up ugly pasted text
Before search terms are processed, Discourse normalises curly quotes and apostrophes and strips zero-width characters in clean_term. Depending on site settings, it can also ignore accents using PostgreSQL's unaccent extension, wired up in unaccent handling.
That means copied smart quotes and accented text are less likely to sabotage a search than you might expect. The software quietly cleans up after you, which is one of the few forms of mercy the modern web still offers.
Two references worth keeping open
- Searching for content effectively — the best official-ish user-facing guide
- Advanced Search for Discourse — older, but still useful for the mental model
And if you want the truth rather than the brochure, read lib/search.rb. The code is where the interesting search features tend to admit what they actually are.