Profile    Mohammed Shiroz Status   Loading  
Logo
Share This
Back to blog
Filter by:
Tags
//Article title

Seven Mistakes When Building Search Features (and What to Do Instead)

About Post

Search looks like the easiest feature on the backlog. One input box, one query, done by lunch:

Product::where('name', 'like', "%{$term}%")->get();

And it works. With 200 rows in your local database, it's instant and finds everything. Then real data arrives, real users type real things, and search quietly becomes the feature people complain about most. It's slow, it finds the wrong things first, and it can't find "colour" when the product says "color".

Here are the mistakes I see most often when building search, why each one hurts, and what to do instead.

Mistake 1: LIKE '%term%' on a big table

Why it hurts: a normal B-tree index is sorted from the first character, like a phone book sorted by surname. You can quickly find everyone whose name starts with "Ali". But "contains ali" means reading every entry. A leading wildcard forces MySQL to scan the whole table, every search, for every user.

The fix: it depends on what users search for.

  • Codes, references and emails usually make sense as a prefix search: LIKE 'INV-2026%' can use an index.
  • Words in names, titles and descriptions need a full-text index (mistake 3).
  • On a genuinely small table, %term% is fine. Don't over-engineer a search across 300 rows.

Mistake 2: forgetting that % and _ are wildcards

Why it hurts: in a LIKE pattern, % means "anything" and _ means "any single character". A user searching for "50%" or "first_name" gets surprising results, and someone typing a lone % matches every row. Query bindings protect you from SQL injection, but they don't escape LIKE wildcards.

The fix: escape them before building the pattern.

$escaped = str_replace(['\\', '%', '_'], ['\\\\', '\%', '\_'], $term);

Product::where('sku', 'like', $escaped . '%')->get();

MySQL uses backslash as the default LIKE escape character, so this works there; other databases may need an explicit ESCAPE clause.

Mistake 3: never trying FULLTEXT

Why it hurts: people jump from LIKE straight to "we need Elasticsearch", skipping the tool already built into MySQL and PostgreSQL.

The fix: a full-text index splits text into words and indexes them, so word searches stay fast on large tables. Laravel supports it directly:

// Migration
$table->fullText(['name', 'description']);

// Query
Product::whereFullText(['name', 'description'], $term)->paginate(20);

A few MySQL gotchas worth knowing: very short words are ignored by default (InnoDB's minimum token size is 3 characters), common "stopwords" are skipped, and the columns in your query must match the columns of a full-text index. If a search for "IT" returns nothing, now you know why.

Mistake 4: ignoring relevance

Why it hurts: a search for "laptop bag" that shows twenty laptops before the one bag isn't wrong, it's useless. Sorting results by newest or by ID treats a product with the term in its title the same as one that mentions it once in paragraph six.

The fix: order by how well each result matches. In MySQL's natural language mode, MATCH ... AGAINST returns a relevance score you can sort by:

Product::query()
    ->select('*')
    ->selectRaw('MATCH(name, description) AGAINST (?) AS score', [$term])
    ->whereFullText(['name', 'description'], $term)
    ->orderByDesc('score')
    ->paginate(20);

Even without full-text, simple rules help a lot: exact matches first, then title matches, then everything else.

Mistake 5: searching on every keystroke, and showing stale results

Why it hurts: typing "invoice" fires seven requests. That's load on your server for nothing, and it causes a sneaky bug: responses can arrive out of order. If the request for "inv" is slow and finishes after the one for "invoice", the user sees results for the wrong word.

The fix: debounce the input and cancel requests that are no longer needed.

let timer, controller;

input.addEventListener('input', () => {
  clearTimeout(timer);
  timer = setTimeout(async () => {
    const q = input.value.trim();
    if (q.length < 2) return;

    controller?.abort();                 // cancel the previous search
    controller = new AbortController();
    try {
      const res = await fetch(`/api/search?q=${encodeURIComponent(q)}`, { signal: controller.signal });
      renderResults(await res.json());
    } catch (e) {
      if (e.name !== 'AbortError') throw e;
    }
  }, 300);
});

Now the request only fires once the user pauses, only for useful queries, and an old response can never overwrite a newer one.

Mistake 6: expecting users to type perfectly

Why it hurts: people make typos, use plurals, mix spellings, and search in their own words. Database search mostly does exact word matching, so "recieve" finds nothing.

The fix: when search is central to your product, use a dedicated search engine. Laravel Scout gives you one API over several engines: Meilisearch and Typesense (both self-hostable) and Algolia (hosted) offer typo tolerance, relevance tuning and filters out of the box.

class Product extends Model
{
    use Searchable;

    public function toSearchableArray(): array
    {
        return ['id' => $this->id, 'name' => $this->name, 'description' => $this->description];
    }
}

Product::search($term)->paginate(20);

Scout also has a database engine that uses your existing database, with attributes to choose full-text or prefix matching per column. It's a nice starting point: you get the Scout API now and can switch to a real engine later without rewriting your controllers. The trade-off with external engines is another service to run and keep in sync, which Scout handles through model events and queues.

A simple ladder: prefix LIKE for codes and IDs, full-text for words in a few columns, a search engine through Scout when typos, ranking and filters really matter. Climb only as high as your users need.

Mistake 7: not looking at what people search for

Why it hurts: you're tuning search based on your guesses, not on real behaviour.

The fix: log search terms and result counts (without personal data), and look at the searches that return zero results. They tell you exactly which synonyms, spellings and missing content to deal with. It's the cheapest search improvement there is.

The checklist

  • No leading-wildcard LIKE on big tables.
  • Escape % and _ in user input.
  • Try full-text indexes before reaching for new infrastructure.
  • Sort by relevance, not by date.
  • Debounce, set a minimum length, cancel stale requests.
  • Use Scout with a real engine when typo tolerance matters.
  • Read your zero-result searches.

What's the strangest thing you've seen users type into a search box? Every product has a few that make you rethink everything.

Comments (0)
Leave your review

Thanks for your valuable comments. Your comments has been updated and appreciate your getting in touch...

01. About Shiroz

Mohammed Shiroz

Hi, I'm Mohammed Shiroz, a software engineer and AI enthusiast from Sri Lanka who turns ideas into intelligent, real-world solutions. With over 9 years of hands-on experience, I currently lead real estate ERP development at Kate Group, a...

03.My Projects

04. Categories

Ready To order Your Project ?

Get in Touch
Close