Who This Guide Is For
This guide is for developers adding search to a web application. You have content — products, articles, records, users, documents — and your users need to find specific items quickly. You are deciding between a simple database query, your database’s built-in full-text search, or a dedicated search engine, and you want to understand the trade-offs before committing to an approach. You are comfortable writing queries and working with your framework’s ORM, and you want to know when each option is appropriate and how to implement it correctly.
Before You Start
You need a clear picture of what your users are searching and how they expect results to behave. Search that works well for an e-commerce product catalogue is architecturally different from search that works well for a knowledge base or a CRM contact list. Before writing any code, answer these questions:
What data is being searched? A single table or multiple tables? Structured fields (names, dates, statuses) or unstructured text (descriptions, articles, comments)?
How large is the dataset? Hundreds of records, thousands, or millions? The right approach at one thousand records is wrong at one million.
What do users expect from the results? Exact matches only, or fuzzy matches that handle typos? Simple keyword matching, or relevance-ranked results where the best match appears first? Do users need filtering and faceting alongside free-text search?
How frequently does the underlying data change? A product catalogue that updates hourly has different indexing requirements from a chat application where messages arrive every second.
The answers to these questions determine which tier of search technology you need. Using a dedicated search engine for a two-hundred-row lookup table is overengineering. Using LIKE queries on a million-row content table with user-facing search is underengineering.
Step 1: Start With Database Queries (and Know Their Limits)
The simplest implementation is a SQL query with a WHERE clause. For basic search requirements, this is the correct starting point.
LIKE queries match a string pattern against a column value. WHERE name LIKE '%widget%' finds any record where the name column contains “widget” anywhere in the string. The % wildcard matches any number of characters. This works for small datasets and simple use cases — an admin panel search on a table of a few hundred records, a filter on a dropdown list, an internal tool where performance is less critical than simplicity.
The limitations of LIKE are fundamental, not incidental. A leading wildcard (%widget) prevents the database from using an index, so the query performs a full table scan. On a table with a million rows, this is slow. On a table with ten million rows, it is unacceptable. LIKE is also literal — it matches character patterns, not meaning. A search for “widgets” will not match “widget” unless you add additional logic. It has no concept of relevance. Results are returned in whatever order the database produces them, not ranked by how well they match the query.
Use LIKE queries when the dataset is small (under ten thousand rows), the search is for internal or admin use, exact substring matching is acceptable, and you do not need relevance ranking. Replace them when any of these conditions change.
Step 2: Use Your Database’s Full-Text Search
Both MySQL and PostgreSQL offer built-in full-text search capabilities that solve many of LIKE’s limitations without introducing an external dependency.
MySQL full-text search requires a FULLTEXT index on the columns you want to search. Once the index exists, you query with MATCH(column) AGAINST('search term'). MySQL parses the search term into words, matches them against the indexed content, and returns results ranked by relevance. It handles word stemming to a limited degree, ignores common stop words (the, and, is), and supports boolean mode for operators like required words (+) and excluded words (-).
PostgreSQL full-text search is more powerful. It uses the tsvector and tsquery types to represent documents and queries as structured search objects. You create a tsvector column (or a generated column) that contains the parsed, stemmed, and weighted representation of your searchable content, then query it with @@ operator. PostgreSQL supports multiple language dictionaries for stemming, assigns weights to different fields (title matches can rank higher than body matches), and supports phrase search.
The practical difference is significant. PostgreSQL full-text search produces better relevance ranking out of the box and offers more control over how results are scored. MySQL full-text search is simpler to set up but less configurable.
Indexing considerations are critical for both. Full-text indexes must be created explicitly and maintained by the database. On large tables, creating the initial index takes time. Subsequent inserts and updates maintain the index automatically, but this adds overhead to write operations. If your table receives thousands of writes per minute, the indexing overhead is measurable.
Combine full-text search with structured filters. Real search queries are rarely pure text. A user searching for products wants to type “blue widget” but also filter by category, price range, and availability. Structure your query to apply full-text search on the text columns and standard WHERE clauses on the structured columns. The database can use both the full-text index and the standard indexes together.
Use database full-text search when the dataset is moderate (up to a few hundred thousand rows), you need relevance ranking, the search is across one database table or a manageable join, and you want to avoid introducing an external service. It is the right middle ground for many applications.
Step 3: Introduce a Dedicated Search Engine
When database search is no longer sufficient — the dataset is large, the relevance needs are complex, users expect typo tolerance and instant results, or you need faceted search across multiple entity types — a dedicated search engine is the right tool.
Meilisearch is an open-source search engine designed for the use case most web applications need: fast, typo-tolerant, relevance-ranked search with filtering and faceting. It runs as a separate service, accepts JSON documents, and returns search results in single-digit milliseconds. It handles typo tolerance automatically (searching “widgt” still finds “widget”), supports filtering and faceting without additional configuration, and provides a simple REST API.
Algolia is a hosted search service that provides similar capabilities without running your own infrastructure. It excels at speed (results in under 50 milliseconds globally), offers sophisticated relevance tuning through a dashboard, and includes frontend libraries for building search UIs. The trade-off is cost — Algolia charges per search operation, and at scale the bill can be substantial.
Elasticsearch is the most powerful and most complex option. It handles massive datasets, complex queries, aggregations, and analytics workloads. It is also the hardest to operate, consuming significant memory and requiring careful cluster management. For a search box on a web application, Elasticsearch is usually overkill. For a log analysis platform or a product catalogue with millions of items and complex relevance requirements, it is the right choice.
Choose based on your actual requirements, not on what sounds most impressive. Meilisearch covers the needs of most web applications with the least operational burden. Algolia covers the same needs with zero operational burden at a financial cost. Elasticsearch is for genuinely large-scale or analytically complex search workloads.
Step 4: Design Your Search Index
Regardless of which search engine you choose, you need to design how your application data maps to search documents. This is the indexing step, and getting it right determines whether your search results are useful.
A search document is not a database row. It is a denormalised representation of a searchable entity, optimised for the search engine rather than for relational integrity. A product search document might combine data from the products table, the categories table, the brands table, and the reviews table into a single flat document. The search engine does not join tables — it searches documents.
Decide which fields are searchable and which are filterable. Searchable fields are the ones the search engine matches free-text queries against (product name, description, tags). Filterable fields are the ones users apply discrete filters to (category, price range, colour, availability). Most search engines distinguish between these because they optimise for each differently.
Assign relevance weights. A match in the product title should rank higher than a match in the description. A match in a tag should rank higher than a match in a review. Configure your search engine’s ranking rules to reflect these priorities. Without explicit weighting, the engine applies default rules that may not match your users’ expectations.
Include only what the results page needs. If your search results page shows the title, a thumbnail URL, a price, and a category label, those are the fields to store in the document. Do not index the full product description, the internal SKU, and thirty other fields that the results page never displays. Smaller documents mean faster indexing and faster retrieval.
Step 5: Keep the Index in Sync
The search index is a copy of your application data, and copies go stale. The synchronisation strategy determines how quickly new or updated data appears in search results.
Event-driven indexing is the most responsive approach. When a record is created, updated, or deleted in your database, your application dispatches an event or a queue job that updates the corresponding search document. The latency between the database write and the search index update is typically seconds. Most frameworks have packages that handle this automatically — Laravel Scout, for example, hooks into model events and pushes changes to the configured search driver.
Scheduled batch indexing reindexes documents on a schedule — every five minutes, every hour, or nightly. This is simpler to implement (a cron job that exports data and sends it to the search engine) but introduces latency. A product added to the database does not appear in search until the next batch runs. This is acceptable for some use cases (a content library that updates infrequently) but not for others (an e-commerce site where sellers expect immediate visibility).
Full reindex capability is essential regardless of your primary sync strategy. You need the ability to rebuild the entire index from scratch. This covers scenarios like: the index was corrupted, you changed the document schema, you added a new searchable field, or you migrated to a different search engine. Design your indexing code so a full reindex can run alongside the live index without downtime — most search engines support index aliasing or zero-downtime reindexing for this purpose.
Handle deletions explicitly. When a record is soft-deleted or archived in your database, the corresponding document must be removed from the search index. Stale documents in the index that link to deleted records produce a frustrating user experience — the user clicks a search result and gets a 404.
Step 6: Build the Search Interface
The frontend search experience matters as much as the backend implementation. A technically excellent search engine behind a poor interface will feel broken to users.
Search-as-you-type provides results as the user types, without requiring them to press Enter. This gives instant feedback and helps users refine their query. Implement this with a debounced input that sends a request after the user pauses typing (200 to 300 milliseconds is a good debounce interval). Do not send a request on every keystroke — that floods the search engine with partial queries.
Display results meaningfully. Show the matched text with the query terms highlighted so users can see why each result was returned. Include enough context (title, a snippet, key metadata) for users to decide which result to click without having to open each one. If the search spans multiple entity types (products and articles and FAQs), group the results by type or use clear visual differentiation.
Filters and facets let users narrow results without refining their text query. A product search with filters for category, price range, and brand lets users start broad (“running shoes”) and narrow down without typing a more specific query. Faceted counts (showing how many results match each filter value) help users navigate efficiently.
Empty states and zero results deserve careful attention. When a search returns nothing, do not show a blank page. Suggest alternative queries, show popular items, or explain what happened (“No results for ‘widgtt’. Did you mean ‘widget’?”). Zero-result pages are where users decide whether to try again or leave.
Common Mistakes
- Building search before understanding the query patterns. Instrument your existing search (even if it is basic) to understand what users actually search for. The data will surprise you and should influence every architectural decision.
- Over-indexing data. Indexing every field from every table creates a bloated, slow index with poor relevance. Index what users search for, not everything you have.
- Ignoring relevance tuning after launch. The default ranking rules are a starting point. Monitor what users search for versus what they click, and adjust ranking weights based on real behaviour.
- Treating the search index as the source of truth. The database is the source of truth. The search index is a projection optimised for search. If they disagree, the database wins. Build the ability to reindex from scratch at any time.
- Not testing search with real data. Search behaves differently with ten test records versus ten thousand production records. Test with a representative dataset that includes the variety, volume, and messiness of real content.
What Good Looks Like
Effective search returns relevant results in under 200 milliseconds for the user. It handles typos gracefully, ranks the best match first, and lets users filter and refine without starting over. The index stays in sync with the database within seconds of a change. The implementation matches the complexity of the problem — a simple LIKE query for a small dataset, database full-text search for a moderate one, and a dedicated engine for large or complex search requirements. The team can reindex from scratch without downtime and can tune relevance without code changes.
Next Steps
For optimising the database queries that power your search (whether using LIKE or full-text search), see How to Optimise Database Queries. For the API design that exposes search results to your frontend, How to Structure a REST API covers endpoint design and response formatting. If your search integrates with an external service, How to Set Up Webhook Integrations covers the patterns for keeping data synchronised.