Skip to main content
Technology & EngineeringSearch Services247 lines

Manticore Search

"Manticore Search: open-source full-text search, SQL-based queries, real-time indexes, columnar storage, Elasticsearch-compatible API"

Quick Summary17 lines
You are an expert in integrating Manticore Search for full-text search functionality.

## Key Points

- **Use real-time tables for mutable data** — real-time tables support INSERT, UPDATE, and DELETE operations. Plain tables are read-only and require re-indexing to update.
- **Enable columnar storage for large analytical datasets** — add `engine='columnar'` to column definitions that are primarily used for filtering and aggregation rather than full-text search.

## Quick Example

```typescript
// Suggest corrections for misspelled words
const suggestions = await utilsApi.sql({
  body: `CALL SUGGEST('wireles', 'products')`,
});
```
skilldb get search-services-skills/Manticore SearchFull skill: 247 lines
Paste into your CLAUDE.md or agent config

Manticore Search — Search Integration

You are an expert in integrating Manticore Search for full-text search functionality.

Core Philosophy

Overview

Manticore Search is an open-source search engine forked from Sphinx Search. It provides full-text search with SQL syntax support, real-time index updates, and an Elasticsearch-compatible HTTP JSON API. Manticore excels at searching structured and semi-structured data with complex filtering, supports columnar storage for analytical queries, and handles billions of documents on modest hardware. It is a strong choice when you need SQL-familiar search syntax, real-time indexing, and low operational complexity.

Setup & Configuration

Running with Docker

docker run -d \
  --name manticore \
  -p 9306:9306 \
  -p 9308:9308 \
  -v manticore-data:/var/lib/manticore \
  manticoresearch/manticore

Port 9306 is the MySQL-compatible protocol. Port 9308 is the HTTP JSON API.

Configuration File

Manticore's config file lives at /etc/manticoresearch/manticore.conf:

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

Client Setup in Node.js

// npm install manticoresearch
import * as Manticoresearch from "manticoresearch";

const config = new Manticoresearch.Configuration();
config.basePath = "http://localhost:9308";

const indexApi = new Manticoresearch.IndexApi(config);
const searchApi = new Manticoresearch.SearchApi(config);
const utilsApi = new Manticoresearch.UtilsApi(config);

Client Setup in Python

# pip install manticoresearch
import manticoresearch
from manticoresearch.api import index_api, search_api, utils_api

config = manticoresearch.Configuration(host="http://localhost:9308")
client = manticoresearch.ApiClient(config)

index_client = index_api.IndexApi(client)
search_client = search_api.SearchApi(client)
utils_client = utils_api.UtilsApi(client)

Core Patterns

Creating a Real-Time Table

// Using the SQL endpoint via HTTP
await utilsApi.sql({
  body: `CREATE TABLE products (
    title text,
    description text,
    price float,
    category string,
    created_at timestamp
  ) morphology='stem_en' min_word_len='2' html_strip='1'`,
});

Indexing Documents

// Insert a single document
await indexApi.insert({
  index: "products",
  id: 1,
  doc: {
    title: "Wireless Mouse",
    description: "Ergonomic wireless mouse with USB-C receiver",
    price: 29.99,
    category: "electronics",
    created_at: Math.floor(Date.now() / 1000),
  },
});

// Bulk insert
await indexApi.bulk(
  [
    { insert: { index: "products", id: 2, doc: { title: "Keyboard", price: 79.99, category: "electronics" } } },
    { insert: { index: "products", id: 3, doc: { title: "Monitor", price: 349.99, category: "electronics" } } },
  ]
    .map((line) => JSON.stringify(line))
    .join("\n")
);

Full-Text Search via JSON API

const searchResult = await searchApi.search({
  index: "products",
  query: {
    bool: {
      must: [
        { match: { title: "wireless" } },
      ],
      filter: {
        range: { price: { gte: 10, lte: 100 } },
      },
    },
  },
  highlight: {
    fields: { title: {}, description: {} },
    pre_tags: "<b>",
    post_tags: "</b>",
  },
  sort: [{ price: "asc" }],
  limit: 20,
  offset: 0,
});

for (const hit of searchResult.hits.hits) {
  console.log(hit._id, hit._source.title, hit._score);
}

Full-Text Search via SQL

Manticore supports querying via MySQL protocol or the /sql HTTP endpoint:

const result = await utilsApi.sql({
  body: `SELECT id, title, price, HIGHLIGHT() AS h
         FROM products
         WHERE MATCH('wireless mouse')
         AND price BETWEEN 10 AND 100
         ORDER BY price ASC
         LIMIT 20
         OPTION ranker=bm25`,
});
# Using the MySQL-compatible protocol directly
import mysql.connector

conn = mysql.connector.connect(host="127.0.0.1", port=9306)
cursor = conn.cursor(dictionary=True)

cursor.execute("""
    SELECT id, title, price, HIGHLIGHT({}, 'title') AS title_hl
    FROM products
    WHERE MATCH('wireless mouse')
    ORDER BY WEIGHT() DESC
    LIMIT 20
""")
rows = cursor.fetchall()

Faceted Search

const result = await utilsApi.sql({
  body: `SELECT category, COUNT(*) AS cnt
         FROM products
         WHERE MATCH('mouse')
         GROUP BY category
         ORDER BY cnt DESC`,
});

Autocomplete with CALL SUGGEST

// Suggest corrections for misspelled words
const suggestions = await utilsApi.sql({
  body: `CALL SUGGEST('wireles', 'products')`,
});

Updating and Deleting

// Partial update
await indexApi.update({
  index: "products",
  id: 1,
  doc: { price: 24.99 },
});

// Delete by ID
await indexApi.callDelete({
  index: "products",
  id: 1,
});

// Delete by query
await indexApi.callDelete({
  index: "products",
  query: { equals: { category: "discontinued" } },
});

Best Practices

  • Use real-time tables for mutable data — real-time tables support INSERT, UPDATE, and DELETE operations. Plain tables are read-only and require re-indexing to update.
  • Leverage SQL syntax for complex queries — Manticore's SQL support is extensive and often more readable than JSON queries for multi-condition filters, GROUP BY, and JOIN operations across tables.
  • Enable columnar storage for large analytical datasets — add engine='columnar' to column definitions that are primarily used for filtering and aggregation rather than full-text search.

Common Pitfalls

  • Confusing string and text column typestext columns are full-text indexed and searchable with MATCH(). string columns are stored as-is and can only be filtered with exact equality. Use text for searchable content and string for category-like attributes.
  • Not tuning morphology settings — by default, no stemming or lemmatization is applied. Enable morphology='stem_en' (or the relevant language) on the table to ensure search matches word variants like "running" and "run".

Anti-Patterns

Using the service without understanding its pricing model. Cloud services bill differently — per request, per GB, per seat. Deploying without modeling expected costs leads to surprise invoices.

Hardcoding configuration instead of using environment variables. API keys, endpoints, and feature flags change between environments. Hardcoded values break deployments and leak secrets.

Ignoring the service's rate limits and quotas. Every external API has throughput limits. Failing to implement backoff, queuing, or caching results in dropped requests under load.

Treating the service as always available. External services go down. Without circuit breakers, fallbacks, or graceful degradation, a third-party outage becomes your outage.

Coupling your architecture to a single provider's API. Building directly against provider-specific interfaces makes migration painful. Wrap external services in thin adapter layers.

Install this skill directly: skilldb add search-services-skills

Get CLI access →