Skip to content

ish — Interactive Semantic Hunt

Semantic search for code, inspired by fzf. Point ish at a directory. It parses each source file into named chunks, embeds them with a local model, and ranks them against your query. Everything runs on your machine.

Languages: Python, C and C++, Markdown, AsciiDoc, YAML, and JSON. Documentation and configuration are indexed beside the code they describe, so one query searches all of it.

Install

pip install interactive-semantic-hunt

Nothing in that install compiles: the default backend reaches Ollama over HTTP with the standard library. A backend that runs the model in this process is an extra — [llama] for llama.cpp, [st] for sentence-transformers.

The default embedding backend is Ollama, which keeps the model resident so no run pays a model load. Start it once and pull the embedding model:

ollama serve
ollama pull nomic-embed-text

Set OLLAMA_HOST to reach a daemon elsewhere.

Two other backends need no daemon:

  • --embedder llama.cpp downloads a GGUF model and loads it per run. Slower per query, faster for a first index of a large tree. Needs the [llama] extra.
  • --embedder st uses sentence-transformers. Needs the [st] extra.

Use

List every chunk under a path:

ish src/
src/ish/domain/chunk.py:7-28  class  Chunk

Search for a query:

ish "parse a python file" src/
[0.71] src/ish/adapters/parser/python.py:14-31  method  PythonParser.parse

Run the interactive picker and open the selection in your editor:

nvim $(ish -i src/)

Type to search, up/down to move, enter to choose, escape to quit.

The first search of a tree builds its index. Later searches parse only the files that changed and embed only text they have not seen.

Write a filter into the query. It works the same in every interface:

lang:cpp state machine transitions     only C and C++
under:/system/ state                only paths that match
type:doc how do I configure this       only prose
type:test,doc retry backoff            the tests and what they document

type: takes code, doc, test, and config.

Press Tab to finish a filter word: ty becomes type:, lang:cp becomes lang:cpp.

Use it from your editor or your code

from how read
Neovim contrib/nvim/ish.lua, an fzf-lua picker clients
VS Code contrib/vscode/, a QuickPick on Ctrl+Alt+I clients
Python from ish.interfaces.python.api import Ish clients
an agent ish-mcp, a Model Context Protocol server clients

To keep the index of a large tree current in the background, run ish-serve /path/to/tree. Every client then reads the index and refreshes nothing itself. See indexing.

Configure

Every command-line option is also a key in .ish/config.toml at the root of your repository:

limit = 10
ignore = [".git", ".venv", "build", "node_modules"]

# Regular expressions, searched against the path.
exclude = ["/vendor/", "/generated/", "_pb2\\.py$"]

Keep generated code out of the index. It is large, nobody searches it by meaning, and embedding costs about one chunk per second.

Documentation

  • Use: every filter, every option, and the picker themes.
  • Clients: Neovim, VS Code, Python, and MCP.
  • Configure: config files and their order, keeping generated code out, the model's context window.
  • Index: where the index lives, federation, worktrees, and ish-serve.
  • Extend: add a language or an embedding backend.

Develop

uv sync
uv run poe check   # lint, typecheck, test
uv run --group docs mkdocs serve   # preview the documentation site

The architecture is ports and adapters. .claude/CLAUDE.md describes the layers, the composition root, and the decisions behind them.