← The Tickd Guide

Tutorials & Guides

How to build a local semantic search engine for markdown files using Ollama and ChromaDB

Stop relying on clunky keyword searches. Build a private, fully offline semantic search tool for your local markdown notes in under 50 lines of Python.

Updated 8/19/2026

Beyond Grep: Why Keyword Search Is Failing Your Notes

We have all been there. You are digging through your personal knowledge base, a massive directory of markdown files, trying to find that one specific thought you had six months ago about "database scaling solutions". You run a standard keyword search or grep command, but it returns nothing because, back then, you happened to write about "optimising backend storage clusters".

Keyword search is rigid. It requires you to remember the exact vocabulary your past self used. Semantic search, however, understands conceptual intent. It knows that "database scaling" and "backend storage cluster" are siblings in the same conceptual family tree.

Building a semantic search tool used to mean renting expensive vector databases and shipping your sensitive private notes off to third-party APIs. Not anymore. Today, you can build a lightning-fast, highly accurate, and completely offline semantic search engine on your own machine. We will use Ollama for generating vector embeddings locally, ChromaDB as our ultra-lightweight local vector database, and a few lines of clean Python to tie it all together.

The Stack under the Hood

Before we write any code, let us clarify how these pieces interact. If you want a quick primer on how vector databases actually work, check out our AI glossary.

Here is how our local pipeline works: 1. Document Loading: We read your local markdown files and split them into manageable semantic chunks. 2. Vector Embedding: We pass these chunks to Ollama using an open-weight embedding model (like nomic-embed-text) which converts text into numerical coordinates (vectors) representing their meaning. 3. Vector Database: We store these coordinates in ChromaDB, an open-source, serverless vector database that runs entirely in-memory or in a local directory. 4. Querying: When you search, we embed your search query and tell ChromaDB to find the nearest vectors mathematically.

Let us get your environment ready.

Step 1: Install Ollama and Pull the Embedding Model

First, make sure you have Ollama installed and running. Head to the official Ollama website, download the client for your operating system, and start the service.

Once Ollama is running in your background, open your terminal and run the following command to download the embeddings model:

`bash ollama pull nomic-embed-text `

We are using nomic-embed-text because it is exceptionally lightweight, fast on consumer hardware, and has an 8192-token context window, making it perfect for processing long paragraphs of text.

Step 2: Set Up Your Python Environment

Create a new directory for your project and set up a virtual environment. Install the necessary dependencies using pip:

`bash mkdir local_search && cd local_search python3 -m venv venv source venv/bin/activate pip install chromadb ollama `

(Note: If you run into issues installing ChromaDB on certain architectures, verify your Python build tools are up to date, or check [Google Gemini Support](https://googlegemini-support.com) if you decide to port this workflow to enterprise environments.)

Step 3: Write the Indexing Script

Now, we need a Python script that scans your markdown directory, reads the files, chunks them up so we do not hit token limits, embeds them via Ollama, and saves them to ChromaDB.

Create a file named index_notes.py and add the following code:

`python import os import chromadb import ollama

Configure paths NOTES_DIR = "./my_notes" # Change this to your markdown folder DB_PATH = "./chroma_db"

Initialize ChromaDB local client chroma_client = chromadb.PersistentClient(path=DB_PATH) collection = chroma_client.get_or_create_collection(name="local_notes")

def chunk_text(text, max_chars=1000): """Splits text into chunks, trying not to cut sentences in half.""" sentences = text.split('. ') chunks = [] current_chunk = "" for sentence in sentences: if len(current_chunk) + len(sentence) < max_chars: current_chunk += sentence + ". " else: chunks.append(current_chunk.strip()) current_chunk = sentence + ". " if current_chunk: chunks.append(current_chunk.strip()) return chunks

def index_markdown_files(): if not os.path.exists(NOTES_DIR): print(f"Directory '{NOTES_DIR}' not found. Please create it and add markdown files.") return

for root, _, files in os.walk(NOTES_DIR): for file in files: if file.endswith(".md"): file_path = os.path.join(root, file) with open(file_path, "r", encoding="utf-8") as f: content = f.read() chunks = chunk_text(content) for i, chunk in enumerate(chunks): # Generate embedding locally using Ollama response = ollama.embeddings( model="nomic-embed-text", prompt=chunk ) embedding = response["embedding"] # Store in ChromaDB doc_id = f"{file}_{i}" collection.upsert( ids=[doc_id], embeddings=[embedding], documents=[chunk], metadatas=[{"source": file_path, "chunk_index": i}] ) print(f"Indexed: {file} ({len(chunks)} chunks)")

if __name__ == "__main__": index_markdown_files() `

Create a dummy folder called my_notes and drop a few markdown files into it. Then run the indexing script:

`bash python index_notes.py `

Step 4: Write the Search Interface

With your files successfully indexed and converted into high-dimensional vector representations, we can now write a search script that lets you query your local collection conceptually.

Create a file named search_notes.py:

`python import chromadb import ollama import sys

DB_PATH = "./chroma_db" chroma_client = chromadb.PersistentClient(path=DB_PATH) collection = chroma_client.get_collection(name="local_notes")

def search(query_text, num_results=3): # 1. Embed the search query using the exact same model query_response = ollama.embeddings( model="nomic-embed-text", prompt=query_text ) query_embedding = query_response["embedding"] # 2. Query ChromaDB for the closest vector matches results = collection.query( query_embeddings=[query_embedding], n_results=num_results ) # 3. Print the matches nicely print(f"\nSearch Results for: '{query_text}'\n" + "="*40) for i in range(len(results["ids"][0])): doc = results["documents"][0][i] metadata = results["metadatas"][0][i] distance = results["distances"][0][i] print(f"\n[Result #{i+1}] (Match Confidence Score: {round(1 - distance, 4)})") print(f"Source File: {metadata['source']}") print(f"Snippet:\n{doc}\n") print("-"*40)

if __name__ == "__main__": if len(sys.argv) < 2: print("Usage: python search_notes.py 'your search query here'") else: query = " ".join(sys.argv[1:]) search(query) `

Now test it. Run a search query using concepts rather than exact words:

`bash python search_notes.py "improving database load speeds" `

Even if none of your documents use the precise phrase "improving database load speeds", your semantic search tool will surface the notes discussing caching, index creation, or query optimisations.

Customising Your Engine

This setup runs completely offline, ensuring your proprietary research, diary entries, or company wikis never leave your local device.

If you find your computer struggling with local model compilation or if you want to scale this beyond individual project boundaries into a collaborative environment, you might consider migrating your ingestion pipeline to commercial APIs. You can read up on how to configure cloud-based embedding alternatives in our deep-dive comparison on /platforms/openai.

With under 100 lines of code, you have built a private, lightning-fast semantic search tool. No API keys, no monthly subscriptions, and zero data leakage. Now go drop your entire markdown folder in there and see what your notes have been trying to tell you.

pythonollamachromadbsemantic-searchtutorials

Keep going

Build something with the prompt generator, decode the jargon in the glossary, or compare the tools on our platform deep-dives.