The Substring Bug: When "gem" Matched "management"
A reader typed "gem" into ShabelleHub's search bar, expecting Gemini. What came back included tools tagged "Project Management." Nothing about that result set was visibly broken — no error, no crash, no empty state. It j…
A reader typed "gem" into ShabelleHub's search bar, expecting Gemini. What came back included tools tagged "Project Management." Nothing about that result set was visibly broken — no error, no crash, no empty state. It just quietly returned the wrong things, which is a harder bug to notice than one that fails loudly.
What the Search Was Actually Doing
The site's search matches a typed query against a tool's name, its category, and its tags. The tag-matching logic looked reasonable at a glance:
const tagMatch = tool.tags.some(tag =>
tag.toLowerCase().includes(query.toLowerCase())
);
.includes() checks whether one string appears anywhere inside another — not just at the start of a word, anywhere. Type "gem" and JavaScript will happily confirm that "gem" appears inside "management." It's not wrong about that. It's just not what a search box is supposed to mean by "gem."
The same logic affected any query that happened to be a substring of an unrelated word: short, common letter sequences are exactly the ones most likely to turn up buried inside something else entirely. A three- or four-letter query is small enough to hide inside dozens of longer words that have nothing to do with what the person was actually looking for.
Why It Took a While to Notice
The bug didn't fail every search — most queries are specific enough, or long enough, that an accidental substring match inside an unrelated word is rare. "Claude" isn't going to accidentally match anything else. It's specifically short, generic queries — "gem," "AI," "V" — that expose the problem, because short strings are statistically far more likely to appear as a fragment of some longer, unrelated word.
That's also why it's an easy category of bug to ship without noticing: it passes casual testing. Type a few obvious full tool names while checking that search works, get correct results every time, move on. The failure only shows up with the kind of short, ambiguous input a real user types without thinking twice about it.
The Fix: Match From the Start of a Word
The intent behind tag search was never "does this string appear anywhere in this tag" — it was "does this tag start with what I typed," the same way autocomplete works almost everywhere else. The fix replaces the substring check with a word-boundary check:
function wordStartMatch(text, query) {
const words = text.toLowerCase().split(/\s+/);
return words.some(word => word.startsWith(query.toLowerCase()));
}
Splitting a tag or category into individual words and checking whether any of them *starts with* the query, rather than checking whether the query appears anywhere inside the raw string, changes the matching behavior exactly where it needed to change. "gem" now matches "Gemini" and matches a tag like "Image Generation" — because "Generation" itself starts with "gen" — but does not match "management," because no word in that phrase starts with "gem."
That second part is worth sitting with: the fix isn't "make search stricter" in a way that only returns exact matches. "Image Generation" showing up for a "gem" query is completely correct — "Generation" does start with "gem." The distinction the fix draws isn't between loose and strict matching, it's between matching at a word boundary versus matching anywhere in a string, which is a more specific and more correct rule than either extreme.
Verifying the Fix Didn't Overcorrect
The natural risk with any search-matching change is fixing one class of false positive while accidentally introducing false negatives elsewhere — making search too narrow instead of too loose. After the patch shipped, single-letter queries were tested deliberately, precisely because short queries are where this bug lived: searching "V" correctly returned tools tagged with words like "Voice," "Video," and "Vector," each confirmed against the actual tag data in Supabase rather than assumed from the UI alone. Searching "M" continued to correctly surface "Management"-tagged tools when someone actually typed something matching that word — the fix didn't remove that behavior, it just stopped an unrelated three-letter query from stumbling into it by accident.
The General Shape of This Bug
.includes() and .startsWith() look interchangeable in a quick read of the code — both are built-in string methods, both take a substring argument, both return a boolean. They express fundamentally different rules. .includes() answers "does this appear anywhere," which is the right tool for something like a full-text content search where a match in the middle of a sentence is meaningful. .startsWith() — or a word-boundary variant of it — answers "does this begin with," which is the right tool for anything meant to behave like autocomplete or tag matching, where a person expects to be typing the beginning of the word they mean.
Neither is universally correct. The bug here wasn't using .includes() — it was using .includes() in a context that actually needed word-boundary matching, and the mismatch was invisible until a specific, short, common query exposed it.
Takeaway: when a search feature returns technically-correct-but-obviously-wrong results, check whether the match is happening at a word boundary or anywhere in the string. Short queries are where that distinction gets tested first — because they're the ones most likely to hide inside something unrelated.
📬 Get the latest AI tool reviews
Expert picks and comparisons, weekly. No spam.