knowledge-index

knowledge-index

A private, self-hosted RAG service over MCP that enables document ingestion, hybrid retrieval (BM25 + dense vectors fused with RRF), and notebook management through 14 tools, keeping documents on your own hardware.

Category
Visit Server

README

knowledge-index (ki)

A private, self-hosted RAG service exposed over MCP (Model Context Protocol), built as a replacement for NotebookLM in a personal AI-tooling ecosystem: documents stay on hardware I control, retrieval is hybrid (BM25 + dense vectors fused with RRF), and every capability is reachable as a tool from a Claude Code session instead of a web UI.

Project status — read this first. The ingestion, chunking, retrieval and embedding layers are implemented and covered by 130 passing unit tests under ruff + mypy --strict. The storage layer (PostgreSQL schema/migrations and the OpenSearch index + RRF pipeline) is written and its integration tests exist, but its live acceptance criteria are still unverified: the host that runs OpenSearch and Postgres has been offline, so the end-to-end pipeline has not been exercised against real services. This is not a deployed product — it is a working codebase with a deliberate architecture, and I would rather say so than imply otherwise.


Why it exists

Hosted notebook tools are convenient but impose three costs I did not want to pay: documents leave my machine, the tool cannot be driven programmatically, and scraping around those limits violates the terms of service. ki trades that convenience for control — the corpus lives on my own host, and the interface is MCP, so an agent can ingest, query and manage notebooks as ordinary tool calls.

What it does

  • Ingests documents (PDF, Markdown, TXT today; URL, YouTube, Google Docs, DOCX planned) into isolated notebooks.
  • Retrieves with a hybrid strategy: BM25 lexical search and dense k-NN vector search, fused by Reciprocal Rank Fusion, then optionally re-ranked with a cross-encoder.
  • Exposes 14 MCP tools over SSE, consumed directly from agent sessions.
  • Abstracts the embedding provider behind one interface — Cohere (default), Voyage, OpenAI, Gemini — so the model is a configuration decision, not an architectural one.

Architecture

SOURCE            PDF · Markdown · TXT
   |
   v
ADAPTERS          pypdf primary, pdfplumber fallback when extraction yields <100 chars/page
                  de-hyphenation · whitespace normalization · encoding cascade (utf-8-sig → cp1252 → latin-1)
                  page_map preserved (page, char_start, char_end) for citations
   |
   v
CHUNKING          structure-aware splitting · content_hash for deduplication
   |
   v
EMBEDDINGS        provider registry behind one ABC · 1024-dim vectors
                  notebook freezes its provider+model at creation time
   |
   v
STORAGE           PostgreSQL  → notebooks, sources, chunks, jobs, costs (5 tables, 8 indices,
                                 NOTIFY + updated_at triggers, Alembic migrations)
                  OpenSearch  → ki_chunks index, HNSW (lucene, cosine, 1024) + BM25
   |
   v
RETRIEVAL         hybrid query → RRF fusion → optional cross-encoder rerank
   |
   v
INTERFACE         14 MCP tools over SSE

Stack: Python 3.11 · Pydantic · asyncio · OpenSearch · PostgreSQL + Alembic · Cohere · pypdf/pdfplumber · pytest · ruff · mypy (strict).


Design decisions and trade-offs

The interesting part of this project is not the code, it is what got ruled out and why.

Vector store: reuse OpenSearch instead of adding Qdrant or pgvector

Chosen: the OpenSearch cluster already running on my host, with a dedicated index.

  • Qdrant has better ergonomics and native payload filtering, but it means standing up another service at roughly 500 MB of RAM on hardware that is already tight.
  • pgvector adds zero infrastructure, but HNSW performance degrades past ~100k chunks, lexical search via pg_trgm is materially weaker than BM25, and there is no native RRF.

OpenSearch gives BM25 and k-NN in one engine with RRF available in the search pipeline, at no additional memory cost. The price paid: the index mapping fixes vector dimensionality at 1024, which constrains which embedding models are usable, and the cluster is shared — so isolation is by index name with number_of_shards=1.

Embeddings: multi-provider abstraction, Cohere as default

Self-hosting bge-m3 was the theoretically better answer (no runtime cost, nothing leaves the host) and was measured and rejected: the available hardware has 8 GB of RAM and no usable GPU, and the model's working memory would starve the OpenSearch instance sharing the box. Gemini's embeddings are 768-dimensional and would not fit the index without a full rebuild.

So the provider sits behind an ABC with a registry, and each notebook freezes its provider and model at creation time — mixing embedding spaces inside one index silently destroys retrieval quality, and freezing makes that failure impossible rather than merely discouraged. Migration is handled explicitly by a re-index operation.

Query returns chunks, not a synthesized answer

ki_query returns ranked chunks; synthesis is opt-in through a separate tool. The consumer is already an LLM session, so synthesizing server-side would mean paying for a second model call to produce something the caller can do for free — and it would force notebooks marked sensitive through a cloud provider they are specifically configured to avoid.

Privacy as an enforced constraint, not a convention

Notebooks can be flagged sensitive. The provider layer rejects any provider whose is_cloud flag is true for those notebooks — enforced in the abstraction, with parametric tests covering it, rather than left to the caller to remember.


Measured numbers

Metric Value
Unit tests 130, passing
Type checking mypy --strict, clean
Lint ruff, clean
Embedding latency (Cohere embed-multilingual-v3.0) ~1150 ms p95 (target was <2 s)
Embedding dimensionality 1024, confirmed against the live API
Embedding providers implemented 4 (1 fully live, 3 behind the same interface)
PostgreSQL schema 5 tables · 8 indices · 2 triggers

The rerank path is deliberately disabled by default in development: the Cohere trial allows 10 rerank calls per month against 1000 embedding calls, which is trivially exhausted during active retrieval work. Finding that in the response headers before it caused a mid-development outage is exactly the kind of cost detail that separates a demo from something operable.


Running it

pip install -e ".[dev]"
cp .env.example .env.local        # fill in COHERE_API_KEY and KI_PG_DSN

# Unit tests — no network, no external services required
pytest tests/unit -v

# Live integration tests — require reachable OpenSearch + PostgreSQL and real credentials
KI_TEST_LIVE=1 pytest tests/integration -v

Bootstrapping the storage layer (both idempotent, safe to re-run):

alembic upgrade head              # PostgreSQL: schema, tables, indices, triggers
python scripts/init_opensearch.py # OpenSearch: ki_chunks index + RRF search pipeline

Configuration

Variable Purpose
COHERE_API_KEY Embedding provider credential
KI_PG_DSN libpq DSN; rewritten internally to +asyncpg / +psycopg
KI_PG_SCHEMA Defaults to ki
KI_OPENSEARCH_URL e.g. http://localhost:9200
KI_EMBEDDING_PROVIDER cohere (default), voyage, openai, gemini
KI_TEST_LIVE Set to 1 to run integration tests against real services

Secrets are typed as Pydantic SecretStr so they are never emitted through logs or reprs.


What I would do next

  • Verify the live acceptance criteria for the storage layer once the host is back, and run the end-to-end smoke over a real corpus.
  • Add retrieval quality evaluation — a ground-truth query set with recall@k, so changes to chunking or fusion can be judged by measurement instead of impression.
  • Add tracing over the ingestion and query paths; per-notebook cost accounting is already modelled in the schema but not yet surfaced.

License

Not currently licensed for reuse. Published as a portfolio artifact.

Recommended Servers

playwright-mcp

playwright-mcp

A Model Context Protocol server that enables LLMs to interact with web pages through structured accessibility snapshots without requiring vision models or screenshots.

Official
Featured
TypeScript
Magic Component Platform (MCP)

Magic Component Platform (MCP)

An AI-powered tool that generates modern UI components from natural language descriptions, integrating with popular IDEs to streamline UI development workflow.

Official
Featured
Local
TypeScript
Audiense Insights MCP Server

Audiense Insights MCP Server

Enables interaction with Audiense Insights accounts via the Model Context Protocol, facilitating the extraction and analysis of marketing insights and audience data including demographics, behavior, and influencer engagement.

Official
Featured
Local
TypeScript
VeyraX MCP

VeyraX MCP

Single MCP tool to connect all your favorite tools: Gmail, Calendar and 40 more.

Official
Featured
Local
Kagi MCP Server

Kagi MCP Server

An MCP server that integrates Kagi search capabilities with Claude AI, enabling Claude to perform real-time web searches when answering questions that require up-to-date information.

Official
Featured
Python
graphlit-mcp-server

graphlit-mcp-server

The Model Context Protocol (MCP) Server enables integration between MCP clients and the Graphlit service. Ingest anything from Slack to Gmail to podcast feeds, in addition to web crawling, into a Graphlit project - and then retrieve relevant contents from the MCP client.

Official
Featured
TypeScript
Neon Database

Neon Database

MCP server for interacting with Neon Management API and databases

Official
Featured
Exa Search

Exa Search

A Model Context Protocol (MCP) server lets AI assistants like Claude use the Exa AI Search API for web searches. This setup allows AI models to get real-time web information in a safe and controlled way.

Official
Featured
Qdrant Server

Qdrant Server

This repository is an example of how to create a MCP server for Qdrant, a vector search engine.

Official
Featured
E2B

E2B

Using MCP to run code via e2b.

Official
Featured