DEV Community

Cover image for Full-Text Search: Why Tools Like OpenSearch, Elasticsearch, and Meilisearch Matter
Bolaji Ajani
Bolaji Ajani

Posted on

Full-Text Search: Why Tools Like OpenSearch, Elasticsearch, and Meilisearch Matter

Search is everywhere, whether you’re looking for a product on Amazon, a tweet on X, or a log entry in your system. But not all searches are created equal. A simple SQL LIKE '%term%' query just doesn’t cut it when your users expect lightning-fast, typo-tolerant, and contextually smart results.

That's where full-text search engines like OpenSearch, Elasticsearch and Meilisearch come in.

In this article, we’ll explore why full-text search matters, how it works behind the scenes, and walk through a hands-on Meilisearch example you can run locally using Docker, Postman, or any other tool capable of making HTTP requests such as cURL.

What is Full-Text Search?

Full-text search allows users to find relevant results based on textual content and not exact matches.

Instead of scanning through every record in a database, search engines analyze and index text data to make it searchable in milliseconds. They understand context to an extent, handle typos, and rank results by relevance, giving users what they mean, not just what they type.

How Full-Text Search Works

Here’s a high-level view of what happens under the hood.

  1. Documents are ingested into the search engine.
  2. They’re tokenized,that is split into searchable terms.
  3. The engine builds an inverted index, mapping words to documents that contain them.
  4. When a user searches, their query is analyzed, scored, and ranked by relevance.


Process Overview

Why Full-Text Search Engines Matter

Search engines exist because users expect relevance and speed. The following are some of the reasons why they are essential:

  • Speed at Scale - Uses inverted indexes to handle millions of document quickly.
  • Relevance Scoring - Ranks results by context, frequency, and meaning.
  • Fuzzy Matching - Finds “interstllar” when you meant “Interstellar.”
  • Filtering & Sorting - Combine search with advanced filters (price, tags, etc).
  • Scalability - From single-node lightweight setups to multi-cluster systems.

Choosing the Right Search Engine

There is no one-size-fits all in full-text search, different engines shine in different contexts. Here are some of the most popular ones and why teams choose them:

Engine Best For Why It’s a Good Choice
Elasticsearch Enterprise-scale search & analytics Proven, battle-tested, and highly extensible with a rich query DSL. Great for logs, metrics, and large datasets.
OpenSearch Self-hosted analytics and observability AWS-backed open-source fork of Elasticsearch. Ideal for dashboards, metrics, and log management with built-in visualization tools.
Meilisearch Developer-friendly instant search Lightweight, fast, and typo-tolerant by default. Excellent for product catalogs, SaaS apps, and small-to-medium projects.
Typesense Product and document search Easy to deploy with automatic typo tolerance and fine-tuned relevance. Perfect for modern web apps and e-commerce sites.
Apache Solr Legacy or enterprise systems Java-based, Lucene-powered, and integrates well with Hadoop and big data pipelines. Solid choice for on-premise deployments.
Algolia Managed search-as-a-service Fully hosted, fast, and feature-rich with analytics and instant results. Ideal for teams that prefer not to manage infrastructure.
MongoDB Atlas Search Search within NoSQL documents Built on Lucene, enables search directly inside MongoDB Atlas. Great for apps already using MongoDB.
PostgreSQL Full-Text Search Embedded search in relational apps Built-in text search (to_tsvector, to_tsquery) with stemming and ranking. Suitable for smaller-scale or internal systems.

When Databases Can Still Handle Search

Sometimes you don’t need a dedicated search engine, your database might already include built-in full-text search capabilities.

Here’s how popular databases stack up for smaller-scale or embedded search needs:

Database Search Feature Description
PostgreSQL to_tsvector, to_tsquery Native full-text search with stemming, ranking, and weighting. Great for moderate datasets or apps needing SQL + search in one place.
MySQL / MariaDB MATCH() AGAINST() Simple full-text search in MyISAM and InnoDB tables. Works for blogs, products, or smaller data sets but lacks fuzzy matching and relevance tuning.
SQLite FTS5 Extension Lightweight full-text indexing for mobile or desktop apps. Perfect for embedded search without a separate service.
MongoDB Atlas Search Lucene-powered search layer Provides fuzzy matching, scoring, and highlighting directly within MongoDB Atlas. Useful for document-based applications.
Oracle Database Oracle Text Enterprise-grade search supporting structured and unstructured data, but complex to configure and manage.
Microsoft SQL Server Full-Text Search Service Offers keyword and phrase search within text columns, suitable for enterprise systems already using SQL Server.

My advice is use your database's built-in search when your dataset is small, self-contained, or doesn't need typo tolerance. Use a dedicated search engine once you need fast relevance scoring, fuzzy matching, and scaling beyond a few hundred thousand records.

Example: Building Search with Meilisearch

Let's set up Meilisearch locally using Docker Compose. We should also be able to index some data, and explore search queries. I've made the assumption that you have docker locally and understand how to use docker.

Step 1: Create docker-compose.yml

version: '3.8'

services:
  meilisearch:
    image: getmeili/meilisearch:v1.24
    container_name: meilisearch
    environment:
      - MEILI_MASTER_KEY=a4qaSFPIfDbrwviM5XsogCK77Oa-YCtpKP4i6cAxr7w
    ports:
      - "7700:7700"
    volumes:
      - ./data.ms:/meili_data
Enter fullscreen mode Exit fullscreen mode

Run it:

docker-compose up -d
Enter fullscreen mode Exit fullscreen mode

You may skip step 2, and just go to this section where I've included a postman collection file that you can download and import as a collection to run the sample tests included in this article.

Step 2: Add Sample Data

Create a movies.json file

[
  { "id": 1, "title": "Inception", "genre": "Sci-Fi", "year": 2010 },
  { "id": 2, "title": "Interstellar", "genre": "Sci-Fi", "year": 2014 },
  { "id": 3, "title": "The Dark Knight", "genre": "Action", "year": 2008 },
  { "id": 4, "title": "Tenet", "genre": "Thriller", "year": 2020 }
]
Enter fullscreen mode Exit fullscreen mode

Make the following curl request to index the data:

curl -X POST 'http://localhost:7700/indexes/movies/documents' \
-H 'Authorization: Bearer a4qaSFPIfDbrwviM5XsogCK77Oa-YCtpKP4i6cAxr7w' \
-H 'Content-Type: application/json' \
--data-binary @movies.json
Enter fullscreen mode Exit fullscreen mode

Response:

{"taskUid":0,"indexUid":"movies","status":"enqueued","type":"documentAdditionOrUpdate","enqueuedAt":"2025-10-31T08:09:40.644831635Z"}
Enter fullscreen mode Exit fullscreen mode

In order for us to be able to filter results in Meilisearch, we also have to update the index settings with the attribute we need to filter by.

curl -X PUT 'http://localhost:7700/indexes/movies/settings/filterable-attributes' \
-H 'Authorization: Bearer a4qaSFPIfDbrwviM5XsogCK77Oa-YCtpKP4i6cAxr7w' \
-H 'Content-Type: application/json' \
--data-binary '["genre", "year"]'
Enter fullscreen mode Exit fullscreen mode

Response:

{"taskUid":1,"indexUid":"movies","status":"enqueued","type":"settingsUpdate","enqueuedAt":"2025-10-31T09:11:12.86560951Z"}
Enter fullscreen mode Exit fullscreen mode

We also need to configure attributes that are sortable;

curl -X PUT 'http://localhost:7700/indexes/movies/settings/sortable-attributes' \
-H 'Authorization: Bearer a4qaSFPIfDbrwviM5XsogCK77Oa-YCtpKP4i6cAxr7w' \
-H 'Content-Type: application/json' \
--data-binary '["year"]'
Enter fullscreen mode Exit fullscreen mode

Response:

{"taskUid":2,"indexUid":"movies","status":"enqueued","type":"settingsUpdate","enqueuedAt":"2025-10-31T09:19:18.009525346Z"}
Enter fullscreen mode Exit fullscreen mode

You may verify all configured attributes by checking the index settings:

curl -X GET 'http://localhost:7700/indexes/movies/settings' \
-H 'Authorization: Bearer a4qaSFPIfDbrwviM5XsogCK77Oa-YCtpKP4i6cAxr7w'
Enter fullscreen mode Exit fullscreen mode

Response:

{"displayedAttributes":["*"],"searchableAttributes":["*"],"filterableAttributes":["genre","year"],"sortableAttributes":["year"],"rankingRules":["words","typo","proximity","attribute","sort","exactness"],"stopWords":[],"nonSeparatorTokens":[],"separatorTokens":[],"dictionary":[],"synonyms":{},"distinctAttribute":null,"proximityPrecision":"byWord","typoTolerance":{"enabled":true,"minWordSizeForTypos":{"oneTypo":5,"twoTypos":9},"disableOnWords":[],"disableOnAttributes":[],"disableOnNumbers":false},"faceting":{"maxValuesPerFacet":100,"sortFacetValuesBy":{"*":"alpha"}},"pagination":{"maxTotalHits":1000},"embedders":{},"searchCutoffMs":null,"localizedAttributes":null,"facetSearch":true,"prefixSearch":"indexingTime"}
Enter fullscreen mode Exit fullscreen mode

Step 3: Try Different Searches

Simple Search

curl -X POST 'http://localhost:7700/indexes/movies/search' \
-H 'Authorization: Bearer a4qaSFPIfDbrwviM5XsogCK77Oa-YCtpKP4i6cAxr7w' \
-H 'Content-Type: application/json' \
--data '{ "q": "inception" }'
Enter fullscreen mode Exit fullscreen mode

Response:

{"hits":[{"id":1,"title":"Inception","genre":"Sci-Fi","year":2010}],"query":"inception","processingTimeMs":11,"limit":20,"offset":0,"estimatedTotalHits":1,"requestUid":"019a3994-1965-7833-94a8-03fd4553bfeb"}
Enter fullscreen mode Exit fullscreen mode

Fuzzy Search

curl -X POST 'http://localhost:7700/indexes/movies/search' \
-H 'Authorization: Bearer a4qaSFPIfDbrwviM5XsogCK77Oa-YCtpKP4i6cAxr7w' \
-H 'Content-Type: application/json' \
--data '{ "q": "interstllar" }'
Enter fullscreen mode Exit fullscreen mode

Response:

{"hits":[{"id":2,"title":"Interstellar","genre":"Sci-Fi","year":2014}],"query":"interstllar","processingTimeMs":5,"limit":20,"offset":0,"estimatedTotalHits":1,"requestUid":"019a3994-a647-7993-b8b2-d8bd4882d031"}
Enter fullscreen mode Exit fullscreen mode

Filter by year

curl -X POST 'http://localhost:7700/indexes/movies/search' \
-H 'Authorization: Bearer a4qaSFPIfDbrwviM5XsogCK77Oa-YCtpKP4i6cAxr7w' \
-H 'Content-Type: application/json' \
--data '{ "q": "Sci-Fi", "filter": "year > 2010" }'
Enter fullscreen mode Exit fullscreen mode

Response:

{"hits":[{"id":2,"title":"Interstellar","genre":"Sci-Fi","year":2014}],"query":"Sci-Fi","processingTimeMs":12,"limit":20,"offset":0,"estimatedTotalHits":1,"requestUid":"019a398c-7ad4-7522-be38-db9cb7152704"}
Enter fullscreen mode Exit fullscreen mode

Sort by Year (Descending)

curl -X POST 'http://localhost:7700/indexes/movies/search' \
-H 'Authorization: Bearer a4qaSFPIfDbrwviM5XsogCK77Oa-YCtpKP4i6cAxr7w' \
-H 'Content-Type: application/json' \
--data '{ "q": "sci-fi", "sort": ["year:desc"] }'
Enter fullscreen mode Exit fullscreen mode

Response:

{"hits":[{"id":2,"title":"Interstellar","genre":"Sci-Fi","year":2014},{"id":1,"title":"Inception","genre":"Sci-Fi","year":2010}],"query":"sci-fi","processingTimeMs":25,"limit":20,"offset":0,"estimatedTotalHits":2,"requestUid":"019a3996-f4a3-7930-8109-041465231893"}
Enter fullscreen mode Exit fullscreen mode

Download Postman collection

Access Repository on Github

You can find the complete Meilisearch demo, including the Docker setup, sample data and Postman collection, on GitHub.

Final Thoughts

Search quietly shapes how people experience your product. When it works well, it feels natural, fast, and almost invisible. Full-text search makes your app return results that are relevant to what users mean, not just what they type. Whether you use Meilisearch, OpenSearch, or PostgreSQL’s text search, choosing the right tool can turn a simple lookup into an experience that feels truly effortless.

Cover Image: by Teslariu Mihai

Top comments (0)