In a previous post, I detailed how I eliminated the “Amnesia Problem” for Google Antigravity (agy) by creating a two-way synchronization bridge between local SQLite conversation databases and Obsidian. That system gave my CLI assistant persistent working state (Meta/states/antigravity.md), fleet operational learnings (Meta/antigravity-memory.md), and an automated audit history of over 90 sessions.
That solved curated memory and episodic tracking. But as my homelab and personal knowledge base grew, another architectural bottleneck emerged:
What happens when the agent needs to recall a specific detail buried deep inside 3,000+ notes across years of projects, research, and server configs?
A curated memory file cannot hold an entire personal vault without exploding the model’s context window. Manually grepping 20 GB of vault files from bash is slow, noisy, and unranked. And setting up heavy external RAG frameworks (vector databases, embedding daemons, Python virtual environment dependencies) introduces bloat, CPU overhead, and operational fragility.
To solve this, I designed and deployed a Tier-3 Knowledge Retrieval Engine for Antigravity: a zero-dependency, dual-engine search system that queries Obsidian’s native Omnisearch HTTP API when Obsidian is running, and falls back to an optimized sub-second local Python scanner when running headlessly in the terminal.
The result is instant, ranked semantic retrieval across 2,970+ Markdown files in under 250 milliseconds.
1. The Dilemma: Why Heavy RAG Stacks Fail for Local Agents
When developers want to give an LLM access to their personal notes, the default impulse is to deploy a vector database:
- Spin up Chroma, Qdrant, or pgvector.
- Chunk every note into 500-token blocks.
- Compute dense vector embeddings using an OpenAI or local HuggingFace model.
- Run cosine similarity search whenever the agent asks a question.
While vector RAG sounds modern on paper, in a privacy-first, local-first homelab workflow it introduces massive drawbacks:
- Embedding Lag & Sync Overhead: Whenever notes change, a background process must detect updates and re-embed chunks. If an agent writes a note and queries it seconds later, stale embeddings produce missed results.
- Resource Footprint: Running a dedicated vector database and embedding model consumes precious RAM and GPU cycles that are better spent on agent reasoning and core services.
- Loss of Keyword Precision: Exact identifiers—such as server MAC addresses, specific script filenames (
backup_to_tape.sh), model numbers (HP Ultrium 4-SCSI), or API error codes—often get lost in high-dimensional vector space. - Daemon Dependency: If the vector DB daemon crashes or reboots, the agent’s retrieval mechanism completely breaks.
Instead of adding another moving part, I looked at what was already installed and working flawlessly on my workstation: Obsidian and its Omnisearch plugin.
2. Unlocking Omnisearch: The Hidden Local HTTP API
If you use Obsidian, you are likely already familiar with Omnisearch. It is hands-down the fastest, most capable search plugin in the Obsidian ecosystem. Built on MiniSearch, Omnisearch indexes Markdown notes, PDFs, images, and frontmatter, providing:
- BM25-based relevance scoring
- Typo tolerance and fuzzy matching
- Title, header, and path weighting
- Contextual excerpts and match highlights
What many Obsidian users don’t realize is that Omnisearch has a built-in HTTP server.
When enabled, Omnisearch exposes a lightweight REST endpoint right on localhost (default port 51361).
Activating the Server
Inside the vault’s plugin configuration (.obsidian/plugins/omnisearch/data.json), two simple keys control the API:
{
"httpApiEnabled": true,
"httpApiPort": "51361",
"httpApiNotice": false
}
Once active, any local process can query the entire vault with a simple HTTP GET request:
curl -s "http://127.0.0.1:51361/search?q=tape+backup+nas2" | jq .
The response is a clean, ranked JSON payload containing scores, file paths, and excerpt snippets:
[
{
"score": 42.15,
"path": "01-Projects/Blog Post - Remote LTO4 Tape Disaster Recovery and Homelab Archiving/Drafts/Cold Storage for the AI Age.md",
"basename": "Cold Storage for the AI Age",
"excerpt": "...air-gapped LTO-4 tape disaster recovery pipeline for homelab infrastructure...",
"matches": ["tape", "backup", "nas2"]
}
]
This gives the agent instant access to Obsidian’s live, indexed cache with zero cold-start penalty.
3. The Headless Problem: Engineering the Fallback Scanner
There was one critical edge case: Antigravity often operates when Obsidian is closed.
Whether running an automated maintenance script via cron, executing off-grid tasks over a LoRa Meshtastic serial gateway, or diagnosing a remote server over SSH from my phone, the agent cannot assume that the Obsidian desktop Electron app is open.
If the HTTP connection to port 51361 refused or timed out, the agent would be blind.
High-Speed Standard Library Scanning
To guarantee 100% availability, I built a deterministic local scanner in pure Python (search_vault.py).
A common misconception is that searching thousands of files without an index is slow. In my vault:
- Total vault size: 20 GB (dominated by audio, PDFs, and media archives).
- Total Markdown notes: 2,978 files.
- Total Markdown text payload: 3.1 Megabytes.
Reading 3.1 MB of text across 2,900 files from a fast NVMe drive takes under 200 milliseconds in Python using standard os.walk and buffered string reads.
Designing the Scoring Function
To ensure the local scanner produces ranking quality competitive with Omnisearch, I implemented a multi-stage scoring heuristic:
- Term Coverage (The AND/OR Balance): Every query term is tokenized. Files containing all search terms receive a massive
+50bonus, while files containing a subset are scored proportionally (len(matched_terms) * 20). - Exact Phrase Matching: Exact full-string matches in the note body receive
+30, while exact matches in the filename receive+80. - Heading Weighting: Markdown headers (
#,##,###) containing the query terms receive+8per occurrence, ensuring concept notes surface above passing mentions. - Term Frequency: Body occurrences add
+1per match, capped at25to avoid long-note bias. - Contextual Windowing: The scanner dynamically extracts a 200-character excerpt around the highest-priority match, wrapping it in ellipses for immediate agent consumption.
Here is the benchmark in practice:
$ python3 ~/scripts/search_vault.py "tape backup nas2" -n 2
=== Vault Search: 'tape backup nas2' [local - 0.22s] ===
1. [344] 01-Projects/Blog Post - Remote LTO4 Tape Disaster Recovery and Homelab Archiving/Drafts/Cold Storage for the AI Age.md
Excerpt: ...seo-title: "Cold Storage for the AI Age: Remote LTO-4 Tape Archival for Homelabs" seo-description: "Build an air-gapped LTO-4 tape disaster recovery pipeline...
2. [174] Meta/tape-backups.md
Excerpt: --- type: archive-ledger updated: "2026-10-11" tape-id: "LTO4-HOMELAB-NAS2" drive-model: "HP StorageWorks Ultrium 4-SCSI"...
0.22 seconds without a running database, without index files, and without external Python packages.
4. The Complete Architecture: Dual-Engine Orchestration
The complete script, search_vault.py, seamlessly fuses both engines behind a unified CLI and Python API:
+-----------------------------------+
| Antigravity Agent (agy) |
+-----------------+-----------------+
|
python3 search_vault.py <query>
|
v
+-----------------------------+
| Check HTTP Port 51361? |
+--------------+--------------+
|
+----------------+----------------+
[Yes, Running] [No / Closed / Timeout]
| |
v v
+-----------------------+ +-----------------------+
| Omnisearch HTTP API | | Fast Local Scanner |
| - MiniSearch BM25 | | - 2,900+ .md files |
| - Typo tolerance | | - Multi-term scoring |
| - Sub-10ms response | | - NVMe 200ms scan |
+-----------+-----------+ +-----------+-----------+
| |
+----------------+---------------+
|
v
+-------------------------------+
| Ranked Results + Excerpts |
| (Terminal Text or JSON) |
+-------------------------------+
Clean Implementation Walkthrough
Here is the core logic of search_vault.py:
#!/usr/bin/env python3
"""
search_vault.py - Fast semantic & full-text search across the Obsidian Vault.
Uses Omnisearch HTTP API when available, falling back seamlessly to an optimized local scanner.
"""
import argparse
import json
import os
import re
import time
import urllib.parse
import urllib.request
from pathlib import Path
VAULT_DIR = Path(os.environ.get("OBSIDIAN_VAULT_PATH", Path.home() / "Vault"))
DEFAULT_OMNI_PORT = 51361
def query_omnisearch(query: str, limit: int = 10, port: int = DEFAULT_OMNI_PORT):
"""Query the Omnisearch HTTP API if Obsidian is running with the plugin enabled."""
url = f"http://127.0.0.1:{port}/search?q={urllib.parse.quote(query)}"
req = urllib.request.Request(url, headers={"User-Agent": "Antigravity-Search/1.0"})
try:
with urllib.request.urlopen(req, timeout=0.8) as resp:
if resp.status == 200:
data = json.loads(resp.read().decode("utf-8"))
results = []
for item in data[:limit]:
path = item.get("path", "")
results.append({
"path": path,
"basename": item.get("basename", Path(path).stem),
"score": round(float(item.get("score", 0)), 2),
"excerpt": item.get("excerpt", "").replace("\n", " ").strip(),
"engine": "omnisearch",
})
return results
except Exception:
return None
def query_local(query: str, limit: int = 10, folder: str = None):
"""Deterministic, high-speed local scanner across Markdown files in the vault."""
target_dir = VAULT_DIR / folder if folder else VAULT_DIR
if not target_dir.exists():
return []
terms = [re.escape(t.lower()) for t in query.split() if t.strip()]
if not terms:
return []
regexes = [re.compile(t, re.IGNORECASE) for t in terms]
exact_re = re.compile(re.escape(query), re.IGNORECASE)
results = []
for root, dirs, files in os.walk(target_dir):
dirs[:] = [d for d in dirs if not d.startswith(".")]
for f in files:
if not f.endswith(".md"):
continue
fpath = Path(root) / f
try:
content = fpath.read_text(encoding="utf-8", errors="replace")
except Exception:
continue
content_lower = content.lower()
fname_lower = f.lower()
matched_terms = [r for r in regexes if (r.search(content_lower) or r.search(fname_lower))]
if not matched_terms:
continue
score = len(matched_terms) * 20
if len(matched_terms) == len(regexes):
score += 50
if exact_re.search(fname_lower):
score += 80
elif exact_re.search(content_lower):
score += 30
for t in terms:
if t in fname_lower:
score += 15
headings = [line for line in content.splitlines() if line.startswith("#")]
for h in headings:
h_lower = h.lower()
for r in matched_terms:
if r.search(h_lower):
score += 8
term_matches = sum(len(r.findall(content_lower)) for r in matched_terms)
score += min(term_matches, 25)
first_pos = -1
for r in matched_terms:
m = r.search(content)
if m and (first_pos == -1 or m.start() < first_pos):
first_pos = m.start()
excerpt = ""
if first_pos != -1:
start = max(0, first_pos - 70)
end = min(len(content), first_pos + 130)
raw_snippet = content[start:end].replace("\n", " ").strip()
excerpt = f"...{raw_snippet}..." if start > 0 else f"{raw_snippet}..."
try:
rel_path = str(fpath.relative_to(VAULT_DIR))
except ValueError:
rel_path = str(fpath)
results.append({
"path": rel_path,
"basename": f[:-3],
"score": score,
"excerpt": excerpt,
"engine": "local",
})
results.sort(key=lambda x: x["score"], reverse=True)
return results[:limit]
def search(query: str, limit: int = 10, folder: str = None, source: str = "auto", port: int = DEFAULT_OMNI_PORT):
if source in ("auto", "omnisearch") and not folder:
omni_res = query_omnisearch(query, limit=limit, port=port)
if omni_res is not None:
return omni_res, "omnisearch"
if source == "omnisearch":
return [], "omnisearch (offline)"
local_res = query_local(query, limit=limit, folder=folder)
return local_res, "local"
5. Integrating with Agent Directives and Memory Tooling
To make this capability native to Antigravity, I hooked it directly into two operational interfaces:
1. Unified Memory CLI (obsidian_memory.py)
I extended the existing memory management script with a search subcommand:
# Query via the central memory tool
python3 ~/scripts/obsidian_memory.py search "Withings weight goal" -n 3
2. Standalone Agent Invocation with --json
When the agent invokes the tool programmatically, it can request machine-readable JSON:
python3 ~/scripts/search_vault.py "E.ON utility integration" --json
{
"query": "E.ON utility integration",
"engine": "local",
"elapsed_seconds": 0.2312,
"total_results": 2,
"results": [
{
"path": "Meta/antigravity-memory.md",
"basename": "antigravity-memory",
"score": 105,
"excerpt": "...Netatmo, Somfy, Liebherr, Withings, Daikin, Brother Printer, E.ON, and Hidroelectrica via Home Assistant CLI scripts...",
"engine": "local"
}
]
}
The agent consumes the exact paths and excerpts in a single turn, allowing it to immediately read the target note using its native file-reading tools.
3. Standing Directives in AGENTS.md
To ensure the agent autonomously leverages search when encountering unfamiliar topics, I documented the script directly in ~/.agents/AGENTS.md:
### 9. Obsidian Cross-Session Memory & Long-Term Context
* **CLI Memory Management & Background Auto-Sync**:
* **Status & Overview**: `python3 ~/scripts/obsidian_memory.py status`
* **Read Memory Component**: `python3 ~/scripts/obsidian_memory.py read <memory|state|log|profile>`
* **Append Log Entry**: `python3 ~/scripts/obsidian_memory.py log "<message>"`
* **Record Persistent Learning**: `python3 ~/scripts/obsidian_memory.py add-note "<note>"`
* **Search Vault (Omnisearch / Local)**: `python3 ~/scripts/search_vault.py "<query>" [--limit N] [--folder FOLDER] [--json]` or `python3 ~/scripts/obsidian_memory.py search "<query>"`
6. Key Takeaways & Architectural Lessons
Building this tier reinforced three core systems engineering principles:
- Subtract Before You Add: Before pulling down another vector database container or wiring complex LangChain pipelines, look at the filesystem. 3,000 Markdown files constitute barely a few megabytes of text. An optimized standard-library script can scan that in a quarter of a second with zero operational maintenance.
- Embrace Graceful Degradation: Always design agents to survive severed GUI or server links. By wrapping Omnisearch’s HTTP API with a fast local scanner, the agent gets the best of both worlds: advanced fuzzy indexed search when desktop Obsidian is open, and guaranteed 100% reliability when operating headless.
- Keep Knowledge Plaintext: Because Obsidian stores notes as transparent Markdown on the local filesystem, search tools require no proprietary drivers, database exports, or migration scripts. Plaintext remains the most durable, AI-friendly format ever created.
With Tier 1 (Curated Memory), Tier 2 (Episodic Session Sync), and Tier 3 (Sub-Second Active Retrieval) now running in tandem, Antigravity has evolved from an amnesic terminal runner into a truly contextual pair programmer that has the entirety of my homelab and personal knowledge base at its fingertips.