The AI Search Had to Learn Its Own Site
Typing hai used to kill the AI search with no_results. The culprit was MariaDB's fulltext word-length floor, not a broken prompt.
Typing hai into the blog's AI search box and getting a real answer instead of an error felt like a small fix with a bigger lesson behind it. Real info about the site, not a dead-end error page. I'd been testing it with proper article titles and technical keywords all week, and those were fine. But the moment someone types casual small talk, the whole thing fell apart.
The search endpoint was hitting MariaDB's FULLTEXT MATCH...AGAINST in natural language mode, configured and indexed exactly how the docs say to. So when I saw empty results for "hai", my first instinct was that something was misconfigured in the CREATE FULLTEXT INDEX statement. Maybe the parser was wrong, maybe the minimum word length setting needed a tweak.
Neither guess was the real problem.
Two different bugs wearing the same hat
Turns out there were two separate issues stacked on top of each other, and they conspired to make short queries silently return nothing.
MariaDB silently skips short words. The InnoDB fulltext parser ignores any word under 3 characters [1]. So "hai" at exactly 3 characters is right on the boundary depending on storage engine and locale settings. The word "hi" at 2 characters? Guaranteed to be invisible. But even if the query word survives the index, there's a second wall.
Natural language mode is not a chatbot. When you use MATCH(col) AGAINST('query' IN NATURAL LANGUAGE MODE), MariaDB computes a relevance score based on term frequency in the document set. It's designed for "find me articles about deployment pipelines", not "what's this website about." A two-character small-talk word against article content produces zero relevance. The query doesn't fail. It just... finds nothing. Silently.
So the bug wasn't a misconfiguration. It was a design mismatch. I was asking a text retrieval engine to do Q&A over site-level information it was never supposed to have.
A JSON file beats a vector database here
The fix lives in commit 86ba567 [3]. I added a site_knowledge.json with four curated entries: Home, About, Activity, and Portfolio. Each entry has a slug, a description in both locales, and an array of keywords for each language. The source data comes straight from the frontend's own data files, so it stays in sync without manual maintenance.
Here's what one entry looks like:
{
"url_path": "/about",
"title_id": "Tentang",
"title_en": "About",
"keywords_id": ["tentang", "jasa", "keahlian", "pengalaman"],
"keywords_en": ["about", "services", "skills", "experience"],
"summary_id": "Layanan web development, DevOps, dan integrasi AI.",
"summary_en": "Web development, DevOps, and AI integration services."
}
The query logic runs a keyword match against both locales before the fulltext search even kicks in. If site_knowledge entries match, they go to the top of the results list with a null relevance score, and the response is capped at two pages max. Here's the simplified match function:
def _match_site_knowledge(query: str) -> list[dict]:
q = query.lower()
matches = []
for entry in SITE_KNOWLEDGE:
kws = entry["keywords_id"] + entry["keywords_en"]
if any(kw in q for kw in kws):
matches.append(entry)
if len(matches) == 2:
break
return matches
Keywords are short, the list is tiny, and substring matching is plenty fast for four entries. No embeddings, no cosine similarity, no latency.
The broader lesson here is that people reach for vector databases and embedding pipelines the moment they hear "RAG," even when the corpus is fifty pages. For a small personal site, a hand-curated JSON file with good keywords does the job better: zero maintenance, instant updates, no infrastructure. Fulltext search still handles the long-tail article queries where it actually shines. The JSON layer covers the gaps: short queries, small talk, site-level questions that no article will ever answer.
I'm not adding pgvector for a 50-page portfolio site. A kilobyte of JSON and a 15-line function got me from "no results" to working answers for every query I tested, including "hai", "what is this", and "your projects". When the site grows to thousands of articles with complex retrieval needs, I'll revisit. Right now, boring works.
That said, the /articles/ask endpoint still leans on one API key check [2]. Fine for a personal blog with no user accounts, but worth noting before anyone copies this pattern into something with an actual threat model.
If you're curious about the full pipeline including how the LLM generates answers from these sources, the article on streaming AI answers with plain fetch covers the transport side.
---