SereneDB

Docs MCP server: search and cited answers from your own docs

Serene Docs Search is a documentation search engine on SereneDB: it turns your docs into one index that a search box, Ask AI and a docs MCP server for agents all read. It pulls Markdown, HTML or PDF from a Git repo, a folder, a website or an S3-compatible bucket. Full-text BM25 needs no model; hybrid search, cited answers and the MCP server are switches you turn on.

npm install @serenedb/docs-search-react@latest
⌘k · search as you typematchedcited
An animation of the search box on serenedb.com/docs. Typing 'create ind' treats the last word as a prefix and returns CREATE INDEX, CREATE INDEX and DROP INDEX, and the blog post CREATE INDEX stopped blocking your writes, docs first. Typing 'serch synonims' finds no section as typed, so a typo pass corrects it to 'search synonyms' and returns expand_solr_synonyms, Synonyms and the tokenizer functions. Ask AI, asked why newly inserted rows do not show up yet, answers that inverted indexes are eventually consistent and that VACUUM (REFRESH_TABLE) publishes pending writes, citing two docs sections.
Search modes

Start with BM25 and no model. Turn on hybrid, Ask AI or MCP when you need them

Serene Docs Search ranks heading sections with BM25 while the reader types, with stemming, stopwords, synonyms, a typo pass and highlighted snippets, and no model at all. Hybrid adds an embedding per section and fuses both rankings with RRF. Ask AI and the MCP server read the same index. The results below come from serenedb.com's own docs index.

per query: 0 model calls
Query: create ind
prefix · the last word as ind*stemming · create → creat
Full-text results
  1. 1Sql › Statements › CREATE INDEXCREATE INDEXCREATE INDEX The CREATE INDEX statement constructs an index on the specified column(s) of the specified table.bm25
  2. 2Sql › IndexesCREATE INDEXCREATE INDEXbm25
  3. 3Sql › Statements › CREATE INDEXCREATE INDEXbm25
  4. 4The State of Serene, July 2026CREATE INDEX stopped blocking your writesCREATE INDEX ... USING inverted(...) no longer blocks writes. Inserts, updates and deletes run against the table for the whole build and the index…bm25
  5. 5Sql › IndexesCREATE INDEX and DROP INDEXCREATE INDEX DROP INDEX Indexes are created with CREATE INDEX and removed with DROP INDEX. The index type is selected with the USING clause (USING…bm25
Hybrid results
  1. 1Sql › Statements › CREATE INDEXCREATE INDEXCREATE INDEX The CREATE INDEX statement constructs an index on the specified column(s) of the specified table.bm25 · rrf
  2. 2Sql › IndexesCREATE INDEXCREATE INDEXbm25 · rrf
  3. 3Sql › Statements › CREATE INDEXCREATE INDEXbm25 · rrf
  4. 4The State of Serene, July 2026CREATE INDEX stopped blocking your writesCREATE INDEX ... USING inverted(...) no longer blocks writes. Inserts, updates and deletes run against the table for the whole build and the index…bm25 · rrf
  5. 5Sql › IndexesCREATE INDEX and DROP INDEXCREATE INDEX DROP INDEX Indexes are created with CREATE INDEX and removed with DROP INDEX. The index type is selected with the USING clause (USING…bm25 · rrf
Query: serch synonims
typo pass · 1–2 edits per worddid you mean · search synonyms
Full-text results
  1. 1Sql › Functions › Search › Tokenizers › expand_solr_synonymsexpand_solr_synonymsThe expand_solr_synonyms template expands tokens through a synonyms map written in Apache Solr synonyms-file format, supplied inline via the required…bm25
  2. 2Cookbook › Search › SynonymsSynonyms…laptop under notebook. CREATE TEXT SEARCH DICTIONARY syn AS split_text(case := 'lower') | expand_solr_synonyms('tv, television, telly laptop =>…bm25
  3. 3Sql › Functions › Search › Tokenizer FunctionsFunctionsEach signature lists the value first and then the template's options with their defaults; a required option has none. Option semantics, constraints…bm25
  4. 4Sql › Functions › Search › Tokenizers › generate_shinglesPhrase searchts_phrase, phraseto_tsquery, the quoted phrases of to_tsquery and ## work on a shingle column, and a phrase the column indexes as one shingle is a…bm25
  5. 5Sql › Functions › Search › Full-Text Search Functionsts_phrase(text[, gap, text, ...][, slop := N])ts_phrase(text[, gap, text, ...][, slop := N]) Match a run of tokens in their indexed order, optionally separated by token gaps. Parameter · Type ·…bm25
Hybrid results
  1. 1Sql › Functions › Search › Tokenizers › expand_solr_synonymsexpand_solr_synonymsThe expand_solr_synonyms template expands tokens through a synonyms map written in Apache Solr synonyms-file format, supplied inline via the required…bm25 · rrf
  2. 2Cookbook › Search › SynonymsSynonyms…laptop under notebook. CREATE TEXT SEARCH DICTIONARY syn AS split_text(case := 'lower') | expand_solr_synonyms('tv, television, telly laptop =>…bm25 · rrf
  3. 3Sql › Functions › Search › Tokenizer FunctionsFunctionsEach signature lists the value first and then the template's options with their defaults; a required option has none. Option semantics, constraints…bm25 · rrf
  4. 4Sql › Functions › Search › Tokenizers › generate_shinglesPhrase searchts_phrase, phraseto_tsquery, the quoted phrases of to_tsquery and ## work on a shingle column, and a phrase the column indexes as one shingle is a…bm25 · rrf
  5. 5Sql › Functions › Search › Full-Text Search Functionsts_phrase(text[, gap, text, ...][, slop := N])ts_phrase(text[, gap, text, ...][, slop := N]) Match a run of tokens in their indexed order, optionally separated by token gaps. Parameter · Type ·…bm25 · rrf
Query: make new rows searchable sooner
no section holds every word · partial matches
Full-text results
  1. 1Cookbook › Search › Search over Iceberg: insert → searchableSearch over Iceberg: insert → searchable…is the source of truth; SereneDB keeps a derived search index over it. Any engine — Spark, BigQuery, Flink, SereneDB itself — writes rows to the…bm25
  2. 2Cookbook › Search › One Search BoxUpdates stay searchableBecause the catch-all is a stored generated column, an UPDATE to any source column recomputes it and the index follows — no reindex step, no drift.…bm25
  3. 3Sql › Functions › Bitstring Functionsset_bit(bitstring, index, new_value)set_bit(bitstring, index, new_value) Sets the nth bit in bitstring to newvalue; the first (leftmost) bit is indexed 0. Returns a new bitstring.…bm25
  4. 4Sql › SQL IntroductionCreating a New TableYou can create a new table by specifying the table name, along with all column names and their types: CREATE TABLE weather ( city VARCHAR, temp_lo…bm25
  5. 5The State of Serene, September 2026More SQL and a new parser…biggest update yet, about 1,200 changes in one go. New SQL you can use today: MATCH_RECOGNIZE, row pattern matching from SQL:2016. Sessions, funnels…bm25
Hybrid results
  1. 1The State of Serene, September 2026More SQL and a new parser…biggest update yet, about 1,200 changes in one go. New SQL you can use today: MATCH_RECOGNIZE, row pattern matching from SQL:2016. Sessions, funnels…bm25 · rrf
  2. 2Clients › Langchain serenedb › FAQ and TroubleshootingWhy don't rows I just added show up in search results?SereneDB's inverted index is eventually consistent. A row written to the base table is invisible to full-text queries, and to vector queries routed…AI suggested
  3. 3Sql › Statements › VACUUMRefreshing — REFRESH_*REFRESH_* Inverted indexes are eventually consistent: rows you INSERT, UPDATE or DELETE may not be visible to queries until the index is refreshed.…AI suggested
  4. 4Cookbook › Search › Search over Iceberg: insert → searchableSearch over Iceberg: insert → searchable…is the source of truth; SereneDB keeps a derived search index over it. Any engine — Spark, BigQuery, Flink, SereneDB itself — writes rows to the…bm25 · rrf
  5. 5Cookbook › Search › One Search BoxUpdates stay searchableBecause the catch-all is a stored generated column, an UPDATE to any source column recomputes it and the index follows — no reindex step, no drift.…bm25 · rrf
Results from serenedb.com/docs, captured 2026-11-03
ask ai · cited answer · “create ind”

In SereneDB, the CREATE INDEX statement constructs an index on specified column(s) or expressions of a table [1].

… For full-text, vector, or geospatial search, you can specify USING inverted [1, 5].

  1. [1] CREATE INDEX
  2. [5] CREATE INDEX and DROP INDEX
seeded search · 5 sections → read [1] → read [5]
ask ai · cited answer · “serch synonims”

In SereneDB, text search synonyms are configured using text search dictionaries that combine a tokenizer with a synonym expansion filter in a pipeline [2].

… expand_solr_synonyms: Expands surface words based on inline rules in Apache Solr format [1, 2].

  1. [1] expand_solr_synonyms
  2. [2] Synonyms
seeded search · 5 sections → read [2] → read [1]
ask ai · cited answer · “make new rows searchable sooner”

In SereneDB, inverted indexes are eventually consistent [2, 3].

… Use VACUUM (REFRESH_TABLE) or VACUUM (REFRESH_INDEX) to publish pending writes to readers [3, 6].

  1. [2] Why don't rows I just added show up in search results?
  2. [3] Refreshing — REFRESH_*
  3. [6] Background intervals
seeded search · 5 sections → read [3], [4] → search “reindex_interval” → read [6], [8]
Ask AI is off. Turn it on to stream an answer with citations over the same index.
mcp · the same index for agents
claude mcp add --transport http serenedb-docs https://api.serenedb.com/mcp
BM25 over the heading, the heading trail and the section body; a heading word counts about twice a body word. No model, no API key.

Architecture

From a Git repo to one SereneDB index, three ways to read it

Serene Docs Search runs as two containers: SereneDB and the search backend. The backend pulls your source, splits pages into heading sections, writes them into one inverted index and serves /v1/search and /v1/ask. The widget, Ask AI and the optional MCP service (port 7710) all go through it. Hover a stage, or a column inside the index, to find its line.

01 · source
Git repository
branch · subdir · commit
01 · source
Website
crawl · sitemap · depth
01 · source
Folder · S3
R2 · MinIO endpoint
02 · backend
parse · index · sync
split by heading url mapping snapshots
03 · serenedb
one inverted index
title · trail · bodybm25
stopwords · synonyms · stemanalyzer
embedding ivf cosinehybrid
one row per heading section
04 · client
⌘K widget
react · script tag
04 · client
Ask AI
seeded search up to 4 tool rounds · cited
04 · client
MCP server
port 7710 search_docs · read_section
> source · git

Markdown in a repository

The backend watches the branch with a cheap ls-remote and syncs on a new commit. Markdown splits at headings h1–h4 by default, so a hit links to its section.

config
"source": {
"type": "git",
"url": "https://github.com/acme/docs",
"branch": "main",
"subdir": ["docs"]
}
> source · website

A live site

The crawler starts at a URL and follows the sitemap and a link depth. HTML is scoped with CSS selectors such as article or main, and URL mapping points results at the published pages.

config
"source": {
"type": "site",
"url": "https://docs.acme.dev",
"depth": 2,
"sitemap": true
}
> source · folder or bucket

A folder or an S3-compatible bucket

Formats: Markdown and MDX by default, plus HTML, reStructuredText, plain text, Jupyter notebooks and PDF when you list them. A custom endpoint reaches R2 or MinIO, and bucket credentials stay in the backend's environment.

config
"source": {
"type": "bucket",
"uri": "s3://acme-docs/guides",
"endpoint": "http://minio:9000"
}
> stage 02 · backend

Two containers

The configurator generates serene-search.config.json and a docker-compose.yml with a random SERENE_SEARCH_TOKEN for the admin routes. Search, health and Ask AI stay public, so the website bundle carries no secret.

shell
docker compose up -d
curl http://localhost:7700/v1/health
> stage 03 · serenedb

One inverted index per corpus

BM25 (k1 1.2, b 0.4) over the heading, the heading trail and the section body, with an analyzer that lower-cases, drops stopwords, splits code identifiers, applies Solr-format synonyms when the config lists them, and stems. Hybrid mode adds an ivf column with cosine distance.

sql · hover a line
CREATE INDEX serene_docs_sections_idx ON serene_docs_sections
USING inverted (id,
title serene_docs_sections_dict, -- bm25
trail serene_docs_sections_dict, -- bm25
body serene_docs_sections_dict, -- bm25
embedding ivf (metric = 'cosine')) -- hybrid
INCLUDE (url)
WITH (optimize_top_k = 'bm25(1.2, 0.4)');
> client · widget

A React component or one script tag

Search as you type: complete words must match and the last one counts as a prefix. Enter jumps to the section anchor. Below, the statement the backend runs for “create ind” and its three rows.

sql
SELECT title, url,
round(BM25(serene_docs_sections_idx.tableoid, 1.2, 0.4), 2) AS score
FROM serene_docs_sections_idx
WHERE (title @@ (plainto_tsquery('create') ^ 2)
OR trail @@ plainto_tsquery('create')
OR body @@ plainto_tsquery('create'))
AND (title @@ (ts_starts_with('ind') ^ 2)
OR trail @@ ts_starts_with('ind')
OR body @@ ts_starts_with('ind'))
ORDER BY score DESC, id;
-- CREATE INDEX 5.99
-- CREATE INDEX … USING inverted 5.85
-- CREATE INDEX stopped blocking your writes 4.21
> client · ask ai

Answers with citations

The model gets a seeded search, then calls search_docs and read_section itself for up to four rounds and writes a final answer with [n] citations. Answer and embedding providers are set independently: any OpenAI-compatible API or Ollama.

sql
-- one stored section; read_section returns the page around it
SELECT title, url, body FROM serene_docs_sections WHERE id = 's5';
> client · mcp

The same index for agents

An optional container serves MCP over Streamable HTTP on port 7710. It calls the backend's public API, so agents see what readers see.

shell
claude mcp add --transport http product-docs https://mcp.example.com/mcp
codex mcp add product-docs --url https://mcp.example.com/mcp
A diagram in four columns. A Git repository, a website or a folder or S3 bucket feeds the search backend, which parses pages, splits them into heading sections and syncs them. SereneDB holds one inverted index over the section title, heading trail and body with a stemming and synonym analyzer, plus an optional IVF vector column for hybrid search. The widget, Ask AI and the MCP server all read the index through the backend.

Four switches, one index

run one, two, three or all four
full-text
BM25, no model

Prefix on the last word, stemming, synonyms, a typo pass with did you mean, snippets.

hybrid
BM25 + vectors

One embedding per section, fused with BM25 by weighted reciprocal rank fusion.

ask ai
Cited answers

A seeded search, up to four rounds of tool calls, [n] citations, multi-turn.

mcp
Agents read it too

search_docs, read_section and docs_health over Streamable HTTP.


Build it

From a Git repo to ⌘K in five steps

Configure, start two containers, let the first sync build the index, then add the widget. Steps 3 and 4 are the SQL the backend runs inside SereneDB, run on SereneDB 26.11.1 on 2026-11-03.

step 01 / 05 · config
{
  "version": 1,
  "source": { "type": "git", "url": "https://github.com/acme/docs",
              "branch": "main", "subdir": ["docs"] },
  "content": {
    "extensions": [".md", ".mdx"],
    "markdown": { "mode": "split" }
  },
  "search": { "type": "fulltext" },
  "sync": { "mode": "commits", "interval": "1h" }
}
serene-search.config.json, with the keys of the guide's config reference. Full-text mode needs no model and no API key.
step 02 / 05 · shell
docker compose up -d
curl http://localhost:7700/v1/health
The health answer reports the index: {"ok":true, … "index":{"ready":true,"sections":…}}
step 03 / 05 · sql
CREATE TEXT SEARCH DICTIONARY serene_docs_sections_dict AS
    split_text(case := 'lower')
    | remove_stopwords(['the', 'a', 'an', 'this', 'these', 'those'])
    | split_text_csv('_')
    | expand_solr_synonyms('db, database
k8s => kubernetes')
    | stem_words('en')
    WITH (frequency, position, norm);

SELECT ts_lexize('serene_docs_sections_dict', 'Creating DB indexes on k8s');
-- {creat,databas,db,index,on,kubernet}

CREATE TABLE serene_docs_sections (
    id VARCHAR PRIMARY KEY, url VARCHAR, title VARCHAR,
    trail VARCHAR, body VARCHAR, embedding FLOAT[4]);

CREATE INDEX serene_docs_sections_idx ON serene_docs_sections
USING inverted (id, title serene_docs_sections_dict,
                trail serene_docs_sections_dict,
                body serene_docs_sections_dict,
                embedding ivf (metric = 'cosine'))
INCLUDE (url)
WITH (optimize_top_k = 'bm25(1.2, 0.4)');
The analyzer turns 'Creating DB indexes on k8s' into {creat, databas, db, index, on, kubernet}: the synonym adds database, and k8s becomes kubernetes. The backend adds the synonym stage only when the config lists synonyms, keeps exact-form copies and a trigram field for code in the same index, and sizes the vector column to its embedding model; the sample uses 4 dimensions.
step 04 / 05 · sql
SELECT title, url,
       round(BM25(serene_docs_sections_idx.tableoid, 1.2, 0.4), 2) AS score
FROM serene_docs_sections_idx
WHERE (title @@ (plainto_tsquery('create') ^ 2)
       OR trail @@ plainto_tsquery('create')
       OR body @@ plainto_tsquery('create'))
  AND (title @@ (ts_starts_with('ind') ^ 2)
       OR trail @@ ts_starts_with('ind')
       OR body @@ ts_starts_with('ind'))
ORDER BY score DESC, id;
3 rows: CREATE INDEX 5.99, CREATE INDEX … USING inverted 5.85, CREATE INDEX stopped blocking your writes 4.21.
step 05 / 05 · shell · tsx
npm install @serenedb/docs-search-react@latest

import { SereneDocsSearch } from "@serenedb/docs-search-react";
import "@serenedb/docs-search-react/styles.css";

<SereneDocsSearch backendUrl="https://search.example.com" />
Without React, the script tag from @serenedb/docs-search-embed renders the same ⌘K trigger and modal.

Use cases

What teams build on it

Open-source project docs

Markdown in a Git repository, split at headings, synced on each commit. Full-text BM25 needs no model and no API key, and the widget gives readers ⌘K with section links.

setup
git.md .mdxfull-text
Full-text search →
example · search as you type
create ind
SELECT title, url,
       round(BM25(serene_docs_sections_idx.tableoid, 1.2, 0.4), 2) AS score
FROM serene_docs_sections_idx
WHERE (title @@ (plainto_tsquery('create') ^ 2)
       OR trail @@ plainto_tsquery('create')
       OR body @@ plainto_tsquery('create'))
  AND (title @@ (ts_starts_with('ind') ^ 2)
       OR trail @@ ts_starts_with('ind')
       OR body @@ ts_starts_with('ind'))
ORDER BY score DESC, id;
result · 8 sample sections
titleurlscore
CREATE INDEX/docs/sql/statements/create_index#create-index5.99
CREATE INDEX … USING inverted/docs/sql/statements/create_index/inverted5.85
CREATE INDEX stopped blocking your writes/blog/state-of-serene-2026-07#create-index-stopped-blocking-your-writes4.21
The backend runs a statement like this on each keystroke.

Internal knowledge base

Exported pages in a folder or an S3-compatible bucket: Markdown, HTML, PDF. Hybrid search catches the question asked in other words, and Ask AI answers from the indexed sections with citations.

setup
s3folderhybridask ai
RAG database →
example · hybrid · rrf
new rows missing from search
WITH fused AS (
  SELECT id, RANK() OVER (ORDER BY s DESC) AS rank, 1.0 AS w, 1 AS is_lex
  FROM (SELECT id, BM25(serene_docs_sections_idx.tableoid, 1.2, 0.4) AS s
        FROM serene_docs_sections_idx
        WHERE title @@ plainto_tsquery('new rows missing search')
           OR body @@ (plainto_tsquery('new') || plainto_tsquery('rows')
                       || plainto_tsquery('missing'))
        ORDER BY s DESC LIMIT 50) lex
  UNION ALL
  SELECT id, RANK() OVER (ORDER BY dist) AS rank, 0.7 AS w, 0 AS is_lex
  FROM (SELECT id, cosine_distance(embedding,
                 [0.25, 0.92, 0.1, 0.05]::FLOAT[4]) AS dist
        FROM serene_docs_sections_idx
        WHERE embedding IS NOT NULL
        ORDER BY dist LIMIT 50) vec
), ranked AS (
  SELECT id, SUM(w / (60 + rank)) AS rrf, MAX(is_lex) AS lex_hit
  FROM fused GROUP BY id ORDER BY rrf DESC, id LIMIT 4
)
SELECT t.title, round(r.rrf, 4) AS rrf,
       CASE WHEN r.lex_hit = 0 THEN 'AI suggested'
            ELSE 'bm25 + vector' END AS badge
FROM ranked r JOIN serene_docs_sections t ON t.id = r.id
ORDER BY r.rrf DESC, t.id;
result · 8 sample sections, 4-d vectors
titlerrfbadge
Why don't rows I just added show up in search results?0.0277bm25 + vector
Refreshing — REFRESH_*0.0276bm25 + vector
CREATE INDEX stopped blocking your writes0.0111AI suggested
CREATE INDEX … USING inverted0.0109AI suggested
Rows only the vector branch found carry the widget's “AI suggested” badge.

Docs for AI agents over MCP

The optional MCP service gives Claude Code, Codex, Cursor or your own agent three tools over the same index. Try it on SereneDB's own docs first.

setup
MCPClaude CodeCodex
Database for AI agents →
example · serenedb's docs, today
claude mcp add --transport http serenedb-docs https://api.serenedb.com/mcp
tools
toolargumentsreturns
search_docsquery, limitnumbered sections: title, url, snippet
read_sectionurlthe docs page that holds the section
docs_health–connectivity, index size, search type

FAQ

Frequently asked questions

Go to docs →

A docs MCP server lets an AI agent search and read your documentation as tools. Serene Docs Search's optional MCP service exposes search_docs, read_section and docs_health over the same index the search box uses.

A search engine for a product's or project's documentation: it indexes the docs and returns the sections that answer a query, ranked, while the reader types. Serene Docs Search splits Markdown, HTML or PDF pages into heading sections, ranks them by BM25 in one SereneDB index and returns each match with a snippet and a link to its heading. Hybrid search, Ask AI with cited answers and the docs MCP server read the same index.

Run the MCP service, then claude mcp add --transport http <name> https://<host>/mcp or codex mcp add <name> --url https://<host>/mcp. To try it on SereneDB's own docs: claude mcp add --transport http serenedb-docs https://api.serenedb.com/mcp.

Yes. A Git source can be limited to subdirectories, content.exclude drops globs, and HTML can be scoped with CSS selectors. The MCP service reads only what the backend indexed.

Serene Docs Search is Apache-2.0 and self-hosted: the widget, the backend, the index and any model keys stay on your infrastructure, and your own backend indexes your repo, folder, site or bucket. The source is on GitHub.

serenedb.com/docs, a Docusaurus site, runs it: its search box is this widget over the site's own index. The widget is a React component or a single script tag that renders into any HTML page, and the backend indexes either the Markdown sources or the built HTML, so no generator-specific plugin is involved.

No. Full-text BM25 runs with no model. Hybrid search needs an embeddings provider (OpenAI-compatible or Ollama), and Ask AI needs a chat model; each is a separate switch.

Git, a mounted folder, a website crawl with sitemap, or an S3-compatible bucket (R2 and MinIO through a custom endpoint). Formats: Markdown and MDX by default, plus HTML, reStructuredText, plain text, Jupyter notebooks and PDF when you list them.

Git sources check the branch for a new commit, any source can be polled on an interval, and CI can call POST /v1/reindex. Snapshots hash every section, so unchanged sections are skipped and deleted ones pruned. Inside SereneDB, the backend publishes each sync with VACUUM (REFRESH_TABLE).

From sections of your index: the model searches and reads them through tools and cites them as [n]. The default instruction tells it to answer from the indexed docs only and to say when it is unsure. See RAG database for retrieval for LLM answers in general.

> get started

Get started with SereneDB

Open the configurator, start two containers and add the widget. Full-text search needs no model and no API key.