QuincyLeo1 downloadsUse an AI decision model for fast fuzzy search, without exact keyword matching or a complex search engine.
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.
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.
| Entry point | How |
|---|---|
| Command palette | ai-search → "AI fuzzy search" |
| Ribbon icon | the text-search icon in the left sidebar |
Both open the same search popup:
Interface language, Chinese by default. Applies to the command name, the ribbon tooltip and every message.
| Setting | Jev preset default | Custom preset default |
|---|---|---|
| Preset | Jev |
— |
| Base URL | https://api.typesafe.ai |
blank |
| Model name | jev-latest |
blank |
| API key | blank | blank |
| Max concurrent requests | 8 | blank → serial |
| Max tokens per request | 64000 | blank → bounded only by the per-second share |
| Max tokens per question | 32000 | blank → never split |
| Max requests per second | 80 | blank → unlimited |
| Max tokens per second | 100000 | blank → unlimited |
| Relevance threshold | 0.5 | 0.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.
| Setting | Default | Notes |
|---|---|---|
| Folders to search | blank → whole vault | one vault-relative path per line, recursive |
| Folders to exclude | blank → nothing excluded | one path per line, recursive; wins over the include list |
| File extensions | md,txt |
comma-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.
---, | --- |) is dropped. Each unit remembers its first line number (the opening fence, for a code block), which is where clicking the result jumps to.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.| File | Responsibility |
|---|---|
src/main.js |
plugin registration: command, ribbon icon, settings tab, lifecycle |
src/settings.js |
localized strings, schema versioning, presets, settings page |
src/search.js |
text splitting, file filtering, scheduling, rate limiting, state |
src/window.js |
the search popup (presentation only) |
src/ai.js |
request 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.
data.json carries a version field, currently 1.
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.
systemone shape: {state, questions} in, {answers: {id: {noul}}} out.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.
MIT — see LICENSE.