Manticore Search is an open-source full-text search engine that grew out of Sphinx Search. It speaks the MySQL wire protocol, so you can create tables, insert documents and run searches with plain SQL from any MySQL client or driver, and it also offers an HTTP JSON API. In this tutorial you will install Manticore Search on Ubuntu 24.04 from the official repository, create a real-time table, load documents, and use full-text operators, filters, highlighting and fuzzy matching.

Prerequisites

To follow this tutorial you need:

  • A server running Ubuntu 24.04 LTS, for example a CubePath VPS, with at least 1 GB of RAM. Search performance improves when the working set fits in memory.
  • A non-root user with sudo privileges.

Step 1 - Adding the Manticore repository

Ubuntu does not package Manticore Search, so install it from the vendor's APT repository. Manticore distributes a small package that adds the repository and its signing key (/usr/share/keyrings/manticore-archive-keyring.gpg) for you.

Install the tools the repository package needs:

sudo apt update
sudo apt install wget gnupg

Download and install the repository package:

cd /tmp
wget https://repo.manticoresearch.com/manticore-repo.noarch.deb
sudo dpkg -i manticore-repo.noarch.deb

Refresh the package index:

sudo apt update

Check that the manticore package now comes from the Manticore repository:

apt policy manticore
manticore:
  Installed: (none)
  Candidate: 29.9.0-26091108-58e88d5ab
  Version table:
     29.9.0-26091108-58e88d5ab 500
        500 https://repo.manticoresearch.com/repository/manticoresearch_jammy jammy/main amd64 Packages

Your version number will be newer or older depending on when you install.

Install the server together with manticore-extra, which adds Manticore Buddy and the columnar library. Buddy is required for several SQL features used later, such as fuzzy search. Also install the MySQL command-line client to talk to Manticore:

sudo apt install manticore manticore-extra mysql-client

Enable the service so that it starts at boot, and start it now:

sudo systemctl enable --now manticore

Check the service status:

sudo systemctl status manticore --no-pager
● manticore.service - Manticore Search Engine
     Loaded: loaded (/lib/systemd/system/manticore.service; enabled; preset: enabled)
     Active: active (running) since Thu 2026-09-24 10:20:41 UTC; 6s ago

Confirm which ports it is listening on:

sudo ss -ltnp | grep searchd
LISTEN 0  4096  127.0.0.1:9306  0.0.0.0:*  users:(("searchd",pid=2181,fd=9))
LISTEN 0  4096  127.0.0.1:9308  0.0.0.0:*  users:(("searchd",pid=2181,fd=10))
LISTEN 0  4096  127.0.0.1:9312  0.0.0.0:*  users:(("searchd",pid=2181,fd=8))

The three ports are:

PortProtocolUsed for
9306MySQLSQL clients and MySQL drivers
9308HTTPJSON API and SQL over HTTP
9312BinaryReplication and distributed tables

All of them are bound to 127.0.0.1 by default, which is the safe choice when your application runs on the same server.

Step 3 - Reviewing the configuration

The configuration lives in /etc/manticoresearch/manticore.conf. Open it:

sudo nano /etc/manticoresearch/manticore.conf

The default searchd section looks like this:

searchd {
    listen = 127.0.0.1:9312
    listen = 127.0.0.1:9306:mysql
    listen = 127.0.0.1:9308:http
    log = /var/log/manticore/searchd.log
    query_log = /var/log/manticore/query.log
    pid_file = /run/manticore/searchd.pid
    data_dir = /var/lib/manticore
}

Because data_dir is set, Manticore runs in real-time (RT) mode: tables are created and changed with SQL statements such as CREATE TABLE, and their schema is stored in the data directory, not in this file. This is the recommended mode and the one used in this tutorial. You do not need to change anything for now.

If your application runs on a different server, do not bind these ports to 0.0.0.0 on a public interface. Change 127.0.0.1 to the server's private network address instead, restart with sudo systemctl restart manticore, and allow only your application server in the firewall, for example sudo ufw allow from 10.0.0.5 to any port 9306 proto tcp. Manticore has no built-in user authentication.

Step 4 - Creating a real-time table

Connect to Manticore with the MySQL client. No user or password is needed:

mysql -h 127.0.0.1 -P 9306

The welcome banner shows Server version: 29.x.x ... followed by the Manticore components that are loaded (columnar, secondary, knn, buddy), and then the mysql> prompt.

Create a table for a small product catalog:

CREATE TABLE products (
    title       TEXT,
    description TEXT,
    category    STRING,
    brand       STRING,
    price       FLOAT,
    in_stock    BOOL,
    tags        MULTI,
    created_at  TIMESTAMP
) morphology='stem_en' min_infix_len='2';

A few things to note about this schema:

  • TEXT columns are full-text indexed and are what MATCH() searches.
  • STRING, FLOAT, BOOL, MULTI and TIMESTAMP columns are attributes, used for filtering, sorting and grouping. MULTI stores a set of integers, such as tag IDs.
  • The id column is created automatically as a 64-bit document ID.
  • morphology='stem_en' applies English stemming, so a search for "switch" also matches "switches".
  • min_infix_len='2' indexes word parts, which enables wildcard searches such as *board* and is needed for fuzzy search.

Verify the schema:

DESCRIBE products;
+-------------+-----------+----------------+
| Field       | Type      | Properties     |
+-------------+-----------+----------------+
| id          | bigint    |                |
| title       | text      | indexed stored |
| description | text      | indexed stored |
| category    | string    |                |
| brand       | string    |                |
| price       | float     |                |
| in_stock    | bool      |                |
| tags        | mva       |                |
| created_at  | timestamp |                |
+-------------+-----------+----------------+

Step 5 - Inserting and updating documents

Real-time tables accept writes immediately, with no rebuild step. Insert a few products:

INSERT INTO products (id, title, description, category, brand, price, in_stock, tags, created_at) VALUES
(1, 'Mechanical Keyboard TKL', 'Tenkeyless layout with Cherry MX Brown switches', 'keyboards', 'Keychron', 89.99, 1, (1,2,3), 1767225600),
(2, 'Ergonomic Vertical Mouse', 'Reduces wrist strain during long work sessions', 'mice', 'Logitech', 45.00, 0, (2,4), 1767312000),
(3, 'USB-C Hub 7-in-1', 'HDMI, USB-A, SD card reader and power delivery', 'accessories', 'Anker', 39.99, 1, (5,6), 1767398400),
(4, 'Low Profile Wireless Keyboard', 'Quiet scissor switches and Bluetooth multi-device pairing', 'keyboards', 'Logitech', 69.00, 1, (1,4), 1767484800);
Query OK, 4 rows affected

created_at takes a Unix timestamp. Count the documents:

SELECT COUNT(*) FROM products;
+----------+
| count(*) |
+----------+
|        4 |
+----------+

Attributes can be updated in place:

UPDATE products SET price = 79.99, in_stock = 1 WHERE id = 2;

To change a full-text field, replace the whole document with REPLACE INTO, using the same column list as the INSERT. Delete a document with DELETE FROM products WHERE id = 3;.

Step 6 - Running full-text searches

MATCH() runs a full-text query against the TEXT columns, and WEIGHT() returns the relevance score. Search for keyboards:

SELECT id, title, price FROM products WHERE MATCH('keyboard') ORDER BY WEIGHT() DESC;
+------+-------------------------------+-----------+
| id   | title                         | price     |
+------+-------------------------------+-----------+
|    1 | Mechanical Keyboard TKL       | 89.989998 |
|    4 | Low Profile Wireless Keyboard | 69.000000 |
+------+-------------------------------+-----------+

Combine a full-text query with attribute filters in the same WHERE clause:

SELECT id, title, brand, price FROM products
WHERE MATCH('keyboard | mouse') AND price < 80 AND in_stock = 1
ORDER BY price ASC;
+------+-------------------------------+----------+-----------+
| id   | title                         | brand    | price     |
+------+-------------------------------+----------+-----------+
|    4 | Low Profile Wireless Keyboard | Logitech | 69.000000 |
|    2 | Ergonomic Vertical Mouse      | Logitech | 79.989998 |
+------+-------------------------------+----------+-----------+

The most useful operators of the full-text query syntax are:

QueryMeaning
keyboard mouseBoth words (implicit AND)
keyboard | mouseEither word
keyboard -wirelessExclude a word
"mechanical keyboard"Exact phrase
"keyboard switches"~5Words within 5 positions of each other
@title keyboardSearch only the title field
*board*Infix wildcard (needs min_infix_len)

Restrict a search to a field and exclude a term:

SELECT id, title FROM products WHERE MATCH('@title keyboard -wireless');
+------+-------------------------+
| id   | title                   |
+------+-------------------------+
|    1 | Mechanical Keyboard TKL |
+------+-------------------------+

Give matches in the title more weight than matches in the description with field_weights:

SELECT id, title, WEIGHT() AS score FROM products
WHERE MATCH('switches')
ORDER BY score DESC
OPTION field_weights=(title=10, description=1);

Because of stemming, this query matches both products whose description mentions switches.

HIGHLIGHT() returns a field with the matching words wrapped in <b> tags, ready for a search results page:

SELECT id, HIGHLIGHT({}, 'description') AS excerpt FROM products WHERE MATCH('switches');
+------+------------------------------------------------------------------+
| id   | excerpt                                                          |
+------+------------------------------------------------------------------+
|    1 | Tenkeyless layout with Cherry MX Brown <b>switches</b>           |
|    4 | Quiet scissor <b>switches</b> and Bluetooth multi-device pairing |
+------+------------------------------------------------------------------+

Group results by an attribute to build facet counts, for example products per category among the matches:

SELECT category, COUNT(*) AS total, AVG(price) AS avg_price
FROM products WHERE MATCH('keyboard | mouse')
GROUP BY category ORDER BY total DESC;
+-----------+-------+-----------+
| category  | total | avg_price |
+-----------+-------+-----------+
| keyboards |     2 | 79.494999 |
| mice      |     1 | 79.989998 |
+-----------+-------+-----------+

Fuzzy search tolerates typos. It is handled by Manticore Buddy, which you installed with manticore-extra, and it needs the table to have min_infix_len set:

SELECT id, title FROM products WHERE MATCH('mechanicl keybord') OPTION fuzzy=1;
+------+-------------------------+
| id   | title                   |
+------+-------------------------+
|    1 | Mechanical Keyboard TKL |
+------+-------------------------+

Keep the query to plain words when using fuzzy=1: full-text operators other than phrase quotes are not supported in fuzzy mode.

Type exit to leave the MySQL client.

Step 8 - Using the HTTP JSON API

Applications that do not use a MySQL driver can talk to port 9308 instead. Insert a document with /insert:

curl -s -X POST http://127.0.0.1:9308/insert \
  -H 'Content-Type: application/json' \
  -d '{"table": "products", "id": 5, "doc": {"title": "Wireless Numeric Keypad", "description": "Compact keypad for laptops", "category": "keyboards", "brand": "Satechi", "price": 29.99, "in_stock": true}}'
{"table":"products","_id":5,"created":true,"result":"created","status":201}

Search with /search. The * key matches against all full-text fields:

curl -s -X POST http://127.0.0.1:9308/search \
  -H 'Content-Type: application/json' \
  -d '{"table": "products", "query": {"match": {"*": "keypad"}}, "_source": ["title", "price"]}'
{"took":0,"timed_out":false,"hits":{"total":1,"total_relation":"eq","hits":[{"_id":5,"_score":1500,"_source":{"title":"Wireless Numeric Keypad","price":29.990000}}]}}

You can also send any SQL statement over HTTP, which is handy for scripts:

curl -s -X POST http://127.0.0.1:9308/sql?mode=raw -d 'SHOW TABLES'

Troubleshooting

The service does not start. Read the service log and the Manticore log:

sudo journalctl -u manticore -n 50 --no-pager
sudo tail -n 50 /var/log/manticore/searchd.log

A common cause is a syntax error after editing manticore.conf, or another process already using port 9306, 9308 or 9312 (sudo ss -ltnp | grep -E '9306|9308|9312').

ERROR 2003 (HY000): Can't connect to MySQL server on '127.0.0.1:9306'. Manticore is not running or listens on a different address. Check the listen lines in the configuration.

fuzzy or BACKUP return an error about Buddy. Install manticore-extra and restart the service. Buddy runs as a child process of searchd, and its messages appear in searchd.log.

A search returns nothing. Check that you are searching a TEXT column (DESCRIBE products); STRING columns are not full-text indexed. Use CALL KEYWORDS('your words', 'products'); to see how Manticore tokenizes and stems the query.

Wildcard queries like key* return an error or no results. The table needs min_prefix_len or min_infix_len. These settings apply to indexing, so create the table with them (or rebuild it) before loading data.

Conclusion

Manticore Search is now running on Ubuntu 24.04 with a real-time table that you can query through SQL on port 9306 or JSON on port 9308, including filters, facets, highlighting and fuzzy matching. As next steps, connect your application with its regular MySQL driver, schedule backups with the manticore-backup tool included in the official packages, and read the Manticore documentation on replication if you need more than one node.