ai-search

approvedunreviewed

by QuincyLeo

Use an AI decision model for fast fuzzy search, without exact keyword matching or a complex search engine.

Updated todayMIT

obsidian-ai-search

English | 简体中文

Fuzzy search a note or a whole vault with an AI decision model — no exact word matching, no search engine to set up.

The default preset targets Jev (TypeSafe's decision model), whose systemone endpoint returns a probability instead of text. Any endpoint speaking the same protocol works through the Custom preset.

This plugin was written entirely by Claude Opus 5.

  • Minimum Obsidian version: 1.4.0
  • Platforms: Desktop only
  • UI languages: Simplified Chinese / English
  • License: MIT

✨ Features

  • 🔎 Two scopes: search the current note, or the whole vault filtered by folder and extension.
  • 📊 Ranked by relevance: every hit carries the model's probability, shown as a percentage and used as the sort key.
  • 🚦 Budgeted calls: configurable concurrency, tokens per request, tokens per question, requests/second and tokens/second.
  • ⏹️ Interruptible: a live progress bar with a Stop button; stopping keeps and ranks whatever came back.
  • 🌏 Bilingual UI: every label, notice and error message exists in Chinese and English.
  • 🙈 No traces: no index, no search history, nothing written to disk except the settings file.

🚀 Installation

git clone <this repo>
cd obsidian-ai-search
npm install
# Point TARGET_DIR at <your vault>/.obsidian/plugins/ai-search, then:
./build.sh

build.sh runs npm run build (esbuild bundles src/*.js into main.js) and copies main.js + manifest.json into the target folder. Enable ai-search in Settings → Community plugins, then fill in the API key.

🖱️ Triggering

Entry pointHow
Command paletteai-search → "AI fuzzy search"
Ribbon iconthe text-search icon in the left sidebar

Both open the same search popup:

  • The text field is on the left; This note and Whole vault sit on the right. In the field, Enter runs This note and Shift+Enter runs Whole vault; Enter with any other modifier does nothing. Hovering either button shows its shortcut.
  • The popup has no close button — click anywhere outside it to dismiss. Re-opening restores exactly what was there, and a search that was running keeps running while the popup is closed.
  • Editing the query (adding or deleting) drops the previous results and stops a running search, because those results answer the old question.
  • While searching, a progress bar with a Stop button replaces the result list. Stopping ranks and shows what has arrived so far.
  • Each result shows the file name in bold small type with the relevance percentage, then up to two lines of the matching passage. Clicking one opens the file at that line and dismisses the popup.

⚙️ Settings

Language

Interface language, Chinese by default. Applies to the command name, the ribbon tooltip and every message.

AI

SettingJev preset defaultCustom preset default
PresetJev—
Base URLhttps://api.typesafe.aiblank
Model namejev-latestblank
API keyblankblank
Max concurrent requests8blank → serial
Max tokens per request64000blank → bounded only by the per-second share
Max tokens per question32000blank → never split
Max requests per second80blank → unlimited
Max tokens per second100000blank → unlimited
Relevance threshold0.50.5

Each preset is stored, updated and read as its own object under presets in data.json, so switching presets never overwrites the other one's values. The Jev defaults are hard-coded in src/settings.js from the published limits — 80 requests/s, 100K tokens/s, 64k tokens per request, and 32k tokens for state plus the longest single question; the plugin never reads them from a document at runtime. Reset AI settings restores the selected preset only.

The request path /v1/systemone is appended to the base URL.

Folders

SettingDefaultNotes
Folders to searchblank → whole vaultone vault-relative path per line, recursive
Folders to excludeblank → nothing excludedone path per line, recursive; wins over the include list
File extensionsmd,txtcomma-separated, no dots

Plain-text notes only. Binary files are ignored — by extension, and as a second line of defence any file whose content contains a NUL byte is skipped. Reset folder settings restores all three; Reset all settings also restores the language and both presets.

🏗️ How a search runs

  1. Split each file into judgement units: one per line, but a line shorter than 10 characters is merged into the next one. Code blocks and tables are the exception — each is joined into a single unit rather than split per line (one table row or one line of code is meaningless on its own, and joining turns a table's N questions into 1). Blank lines, ATX headings, both code-fence markers, table delimiter rows and leading YAML frontmatter are skipped; code inside a fence is still searched. A unit made only of layout punctuation (---, | --- |) is dropped. Each unit remembers its first line number (the opening fence, for a code block), which is where clicking the result jumps to.
  2. Batch units into requests. The search phrase and the judging rule go into state; each unit becomes its own noul question carrying the passage in instructions. One request therefore judges many passages, the passages cannot interfere with each other, and the rule shared by the whole batch is paid for once in state rather than once per passage. A unit whose search phrase + passage exceeds max tokens per question is split into several questions for the request (cutting at a newline or space where possible), so its tail is judged rather than dropped; the unit still yields a single result scored by its best-matching piece, and the full text is what you see.
  3. Schedule the batches across max concurrent requests workers, behind a sliding-window limiter enforcing both rate caps. Token cost is estimated from the request body before sending (deliberately an over-estimate). One request is sized at the smaller of max tokens per request and max tokens per second ÷ concurrency: packing a request to the API ceiling spends tokens most efficiently and time worst — Jev answers 60 questions about as fast as 1, so a fatter request takes the same latency while claiming proportionally more of the per-second budget, leaving the limiter to stall every other worker and making the configured concurrency moot. Capping each request at its fair share lets all the workers hold a grant at once.
  4. Retry on HTTP 429 / 529 / 5xx and on network errors — 3 attempts with 500 ms exponential backoff. HTTP 400 / 401 / 403 / 422 abort the whole search instead, since they cannot be fixed by retrying. A batch that exhausts its retries is counted as failed and reported at the end; the other batches still finish.
  5. Rank the units whose probability reached the threshold, highest first, and render them.

🧱 Code layout

FileResponsibility
src/main.jsplugin registration: command, ribbon icon, settings tab, lifecycle
src/settings.jslocalized strings, schema versioning, presets, settings page
src/search.jstext splitting, file filtering, scheduling, rate limiting, state
src/window.jsthe search popup (presentation only)
src/ai.jsrequest building, HTTP, error classification, retry

Search state lives on SearchController in search.js, not on the modal — that is what makes "close and re-open unchanged" work, and what keeps a search alive while the popup is closed.

🔐 Settings file

data.json carries a version field, currently 1.

  • On read: a missing, non-integer or lower version is reported as an invalid field; a higher version is reported as "newer than the plugin". In both cases the plugin refuses to load — no command, no ribbon icon, no settings tab — rather than risk overwriting a file it cannot read.
  • On write: if the file on disk turns out to be older, it is copied to data.json.bak before being replaced; if it is newer, the write is refused.

The API key is stored in plain text in data.json, like every other Obsidian plugin credential. It is sent only to the base URL you configured, as an Authorization: Bearer header.

⚠️ Known limitations

  • Text search only. For simplicity, only text is searchable for now. If you need to search images or other non-plain-text documents, issues and PRs are welcome.
  • Cost and latency scale linearly with vault size. Every line is judged by a model call. With the default limits a request holds a few hundred average-length lines, so a vault with 100k lines means a few hundred requests. Start with a folder filter.
  • No cache. Repeating a search repeats every call, by design.
  • Single-line context. A line is judged on its own (after short-line merging), so a pronoun-heavy sentence may score lower than its paragraph would.
  • Custom endpoints must match the systemone shape: {state, questions} in, {answers: {id: {noul}}} out.

🛠️ Development notes

npm test          # node --test ut/*.test.js
npm run check     # bundle, then syntax-check the bundle

ut/helpers/bootstrap.js stubs require("obsidian") and enough DOM to render and click the popup in plain Node; ut/helpers/fakePlugin.js provides an in-memory vault and settings store.

📄 License

MIT — see LICENSE.

For plugin developers

Search results and similarity scores are powered by semantic analysis of your plugin's README. If your plugin isn't appearing for searches you'd expect, try updating your README to clearly describe your plugin's purpose, features, and use cases.