Semantic Search Implementation
Relevant source files
The following files were used as context for generating this wiki page:
The Semantic Search Implementation provides RAG (Retrieval-Augmented Generation) capabilities over an Obsidian vault. The memory-api service parses Markdown notes, generates vector embeddings using local LLMs, and stores them in a Qdrant vector database. This system allows the Nyansa stack to retrieve relevant note segments based on natural language queries rather than simple keyword matching.
Sources: README.md:16-20, services/memory-api/README.md:1-6
Architecture Overview
The semantic search architecture relies on a TypeScript service (memory-api) that coordinates between the local filesystem, an embedding provider (Ollama), and a vector store (Qdrant). The system uses the nomic-embed-text model to transform text into high-dimensional vectors for similarity comparisons.
System Data Flow
The following diagram illustrates the flow from a user query to the retrieval of relevant note chunks.
Search API Specification
The search functionality is exposed via a POST /search endpoint. It requires a Bearer token for authentication and accepts JSON payloads to customize retrieval behavior.
Endpoint: POST /search
| Parameter | Type | Description |
|---|---|---|
query | string | The natural language search term (1-2000 chars). |
limit | number | Maximum number of results to return (1-50, default 5). |
tags | string[] | Optional list of tags to filter the results. |
minScore | number | Minimum similarity score threshold (-1 to 1). |
Sources: services/memory-api/src/app.ts:25-31, services/memory-api/README.md:41-45
Response Structure
The API returns a list of SearchHit objects containing the relevance score, the source note path, and the specific text chunk.
{
"query": "query text",
"results": [
{
"score": 0.85,
"path": "folder/note.md",
"title": "Note Title",
"section": "Heading > Subheading",
"tags": ["project", "idea"],
"chunkIndex": 0,
"text": "The content of the note chunk..."
}
]
}
Sources: services/memory-api/src/domain.ts:60-63, services/memory-api/README.md:49-60
Ingestion and Vectorization
Before notes are searchable, the memory-api must ingest and vectorize the vault content. This process ensures that text is represented in a format suitable for semantic comparison.
Parsing and Chunking Logic
Notes are processed in three distinct stages to preserve context:
- Parsing: Extracts frontmatter, titles (from YAML or H1), and tags. Fenced code blocks are identified to avoid indexing them as prose.
- Chunking: Splits the note body by ATX headings. Each chunk includes its heading path (e.g.,
Setup > Docker) to maintain hierarchical context. - Context Injection: Every chunk is prefixed with the
Note Title > Heading Pathbefore embedding to ensure the vector captures the section's location within the vault.
Vector Storage Schema
Each chunk is stored as a "point" in Qdrant with a payload containing metadata used for filtering and source attribution.
| Field | Type | Description |
|---|---|---|
path | string | Vault-relative path to the source file. |
tags | string[] | Lower-cased, deduplicated tags from frontmatter and inline #tags. |
section | string | The heading path for the specific chunk. |
note_hash | string | SHA-256 of the file content for incremental updates. |
index_version | string | Derived from model and chunking config to trigger re-indexing. |
Sources: services/memory-api/src/domain.ts:40-52, services/memory-api/src/ingest/ingest.ts:135-151
Incremental Indexing
The system implements incremental indexing to avoid redundant processing. During the incremental ingest mode, the Ingestor compares the current vault file hashes against the note_hash stored in Qdrant.
- Unchanged: If the hash and
index_versionmatch, the note is skipped. - Modified: If hashes differ, the old points for that path are overwritten or deleted, and new vectors are generated.
- Deleted: If a file no longer exists in the vault, all associated points are removed from Qdrant.
Sources: services/memory-api/src/ingest/ingest.ts:74-90, services/memory-api/src/ingest/plan.ts
Configuration
| Variable | Default Value | Description |
|---|---|---|
EMBEDDING_MODEL | nomic-embed-text | The Ollama model used for vectorization. |
CHUNK_MAX_CHARS | 1500 | Maximum character length for a single text chunk. |
QDRANT_COLLECTION | obsidian_notes | The name of the vector store collection. |
Sources: services/memory-api/README.md:73-83
Summary
The Semantic Search Implementation transforms a flat folder of Markdown files into a queryable knowledge base. By combining heading-aware chunking with vector embeddings and incremental updates, it provides a high-performance retrieval system that powers the Nyansa AI "second brain" workflows.