Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ _test-wheel-contents:
cd $(GENERATED_PROJECT) && rm -rf dist && uv build --wheel > /dev/null
cd $(GENERATED_PROJECT) && unzip -l dist/*.whl \
| grep -q "entrypoints/ingest.py" \
&& unzip -l dist/*.whl | grep -q "entrypoints/mcp_server.py" \
&& unzip -l dist/*.whl | grep -q "vespa_app/__init__.py" \
&& echo "OK: wheel contains entrypoints and vespa_app" \
|| (echo "FAIL: wheel is missing required packages" && exit 1)
17 changes: 15 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -64,19 +64,32 @@ template/
└── migrations/ # mistral-vespa migrate (hybrid query profile)
```

Port selection is intentionally not part of the initial Copier questions. Generated projects default to `18080` / `19072`; if needed later, users can edit `.env` (`VESPA_QUERY_PORT`, `VESPA_CONFIG_PORT`) without re-generating the project.

## Quick start (after `copier copy`)

![Demo](demo.gif)

These commands run in the **generated project**. The repo root `Makefile` is only for template CI.

```bash
cd my-search-project
make setup-vespa
vibe --trust # Or your agent of choice
```

Alternatively, run the ingest and search entrypoints directly:

```bash
make ingest path=sample_data/hello.txt
make search query="hello world"
make bruno # optional: API files under vespa/bruno/vespa/
```

Port selection is intentionally not part of the initial Copier questions. Generated projects default to `18080` / `19072`; if needed later, users can edit `.env` (`VESPA_QUERY_PORT`, `VESPA_CONFIG_PORT`) without re-generating the project.
Generate [Bruno](https://www.usebruno.com/) API collection files to explore the Vespa query and document APIs interactively:

```bash
make bruno
```

## Variables

Expand Down
Binary file added demo.gif
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
117 changes: 117 additions & 0 deletions demo.tape
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
# VHS documentation
#
# Output:
# Output <path>.gif Create a GIF output at the given <path>
# Output <path>.mp4 Create an MP4 output at the given <path>
# Output <path>.webm Create a WebM output at the given <path>
#
# Require:
# Require <string> Ensure a program is on the $PATH to proceed
#
# Settings:
# Set FontSize <number> Set the font size of the terminal
# Set FontFamily <string> Set the font family of the terminal
# Set Height <number> Set the height of the terminal
# Set Width <number> Set the width of the terminal
# Set LetterSpacing <float> Set the font letter spacing (tracking)
# Set LineHeight <float> Set the font line height
# Set LoopOffset <float>% Set the starting frame offset for the GIF loop
# Set Theme <json|string> Set the theme of the terminal
# Set Padding <number> Set the padding of the terminal
# Set Framerate <number> Set the framerate of the recording
# Set PlaybackSpeed <float> Set the playback speed of the recording
# Set MarginFill <file|#000000> Set the file or color the margin will be filled with.
# Set Margin <number> Set the size of the margin. Has no effect if MarginFill isn't set.
# Set BorderRadius <number> Set terminal border radius, in pixels.
# Set WindowBar <string> Set window bar type. (one of: Rings, RingsRight, Colorful, ColorfulRight)
# Set WindowBarSize <number> Set window bar size, in pixels. Default is 40.
# Set TypingSpeed <time> Set the typing speed of the terminal. Default is 50ms.
#
# Sleep:
# Sleep <time> Sleep for a set amount of <time> in seconds
#
# Type:
# Type[@<time>] "<characters>" Type <characters> into the terminal with a
# <time> delay between each character
#
# Keys:
# Escape[@<time>] [number] Press the Escape key
# Backspace[@<time>] [number] Press the Backspace key
# Delete[@<time>] [number] Press the Delete key
# Insert[@<time>] [number] Press the Insert key
# Down[@<time>] [number] Press the Down key
# Enter[@<time>] [number] Press the Enter key
# Space[@<time>] [number] Press the Space key
# Tab[@<time>] [number] Press the Tab key
# Left[@<time>] [number] Press the Left Arrow key
# Right[@<time>] [number] Press the Right Arrow key
# Up[@<time>] [number] Press the Up Arrow key
# Down[@<time>] [number] Press the Down Arrow key
# PageUp[@<time>] [number] Press the Page Up key
# PageDown[@<time>] [number] Press the Page Down key
# Ctrl+<key> Press the Control key + <key> (e.g. Ctrl+C)
#
# Display:
# Hide Hide the subsequent commands from the output
# Show Show the subsequent commands in the output
# ScrollUp[@<time>] [number] Scroll terminal viewport up by rows
# ScrollDown[@<time>] [number] Scroll terminal viewport down by rows

Output demo.gif

Require echo

Set Shell "bash"
Set FontSize 24
Set Width 1200
Set Height 600

# Env VIBE_HOME "..."

# Copy project
Type "copier copy gh:mistralai/search-starter-app my-search-project"
Enter
Sleep 2s
Hide
Type '...' # API Key
Enter
Show
Sleep 1s
Enter
Sleep 1s
Type "cd my-search-project"
Enter

# Setup Vespa
Type "make setup-vespa"
Enter
Sleep 15s

# Start Vibe
Type "vibe --trust"
Sleep 1s
Enter
Sleep 5s

# Show MCP
Type "/mcp"
Enter
Sleep 2s
Escape
Sleep 1s

# Ingest
Type "Ingest https://arxiv.org/pdf/1706.03762.pdf"
Enter
Sleep 15s
Enter # Approve ingest tool usage
Sleep 30s

# Search
Type "How does the attention mechanism work?"
Enter
Sleep 15s
Enter # Approve search tool usage
Sleep 15s
Enter # Approve open tool usage
Sleep 30s
8 changes: 8 additions & 0 deletions template/.mcp.json.jinja
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
{
"mcpServers": {
"{{ _copier_conf.dst_path.name }}": {
"command": "uv",
"args": ["run", "python", "-m", "entrypoints.mcp_server"]
}
}
}
5 changes: 5 additions & 0 deletions template/.vibe/config.toml.jinja
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
[[mcp_servers]]
name = "{{ _copier_conf.dst_path.name }}"
transport = "stdio"
command = "uv"
args = ["run", "python", "-m", "entrypoints.mcp_server"]
9 changes: 8 additions & 1 deletion template/Makefile.jinja
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
.PHONY: installdeps install-workflows setup-vespa start-vespa stop-vespa reset-vespa migrate-vespa verify-vespa ingest search bruno generate-vespa-lock start-examples execute-ingestion
.PHONY: installdeps install-workflows setup-vespa start-vespa stop-vespa reset-vespa migrate-vespa verify-vespa ingest search mcp bruno generate-vespa-lock start-examples execute-ingestion

ifneq (,$(wildcard .env))
include .env
Expand All @@ -10,6 +10,8 @@ VESPA_QUERY_PORT := $(or $(VESPA_QUERY_PORT),18080)
VESPA_CONFIG_PORT := $(or $(VESPA_CONFIG_PORT),19072)
VESPA_ENDPOINT := $(or $(VESPA_ENDPOINT),http://localhost:$(VESPA_QUERY_PORT))
VESPA_CONFIG_URL := $(or $(VESPA_CONFIG_URL),http://localhost:$(VESPA_CONFIG_PORT))
MCP_HOST := $(or $(host),127.0.0.1)
MCP_PORT := $(or $(port),8000)

## Install dependencies
installdeps:
Expand Down Expand Up @@ -53,6 +55,11 @@ ingest:
search:
uv run python -m entrypoints.search "$(query)" $(if $(top_k),--top-k $(top_k),) $(if $(query_profile),--query-profile $(query_profile),)

## Start the MCP server in HTTP mode
## Usage: make mcp [host=0.0.0.0] [port=8000]
mcp:
uv run python -m entrypoints.mcp_server --http --host $(MCP_HOST) --port $(MCP_PORT)

## Generate Bruno API files under vespa/bruno/vespa/ (requires WORKSPACE_ROOT in .env)
bruno:
uv run mistral-vespa bruno \
Expand Down
43 changes: 35 additions & 8 deletions template/README.md.jinja
Original file line number Diff line number Diff line change
Expand Up @@ -48,14 +48,38 @@ Uses `QueryEngine` with `VectorRetriever` (hybrid BM25 + vector via Vespa):
make search query="hello world"
```

#### Custom query profile
### MCP server

Ranking weights live in a Vespa **query profile**, not in the request. The search defaults to the `hybrid-search` profile (tuned BM25 + vector weights, defined in `src/vespa_app/migrations/001_create_index_schema.py`).
The sample app includes an MCP server which exposes search, agentic navigation, and ingest as MCP tools so agents (Vibe, Claude Code, etc.) can query and populate the local index directly.

To use your own, register it with `add_query_profiles([...])` in a new `00X_*.py` migration, rerun `make migrate-vespa`, then select it by name with `query_profile=`:
The server fails fast at startup with a clear error if `MISTRAL_API_KEY` is missing or if the search index does not support agentic navigation. Vespa must be running before you start the server (`make start-vespa`).

**Available tools:**

| Tool | Description |
|------|-------------|
| `search(query, top_k=5)` | Hybrid BM25 + vector search; returns ranked chunks with score, content, source_id, locator, **start_offset**, **end_offset**, and metadata |
| `ingest(uri)` | Ingest a local path/directory, `file://` URI, or `http(s)://` URL; text files use plain-text extraction, everything else uses Mistral OCR |
| `open_source(source_id, start_offset, end_offset, window=2)` | Expand around a search hit — returns the anchor chunk plus `window` neighbours on each side, in reading order |
| `navigate_source(source_id, start_offset, end_offset, direction, top_k=1)` | Step through a document from a known position; `direction` is `"next"` or `"previous"` |
| `read_source(source_id, start_offset=None, end_offset=None, top_k=20)` | Fetch a known offset range directly; omit either bound to read from the start or to the end |
| `grep_source(source_id, pattern, mode="phrase", top_k=5)` | Lexical search within a single source; `mode` is `"phrase"` (ordered) or `"term"` (any order) |

### Vibe CLI

Run `vibe` from this project directory. It automatically reads `.vibe/config.toml` and connects to the server via stdio — no manual setup needed. You can immediately ask Vibe to search or ingest documents.

> On first run, Vibe will ask you to trust this directory before loading the project config. Accept the prompt, or pass `--trust` to skip it for that session.

### Claude Code

Open this project directory in Claude Code. It automatically reads `.mcp.json` and connects to the server via stdio — no manual setup needed. You can immediately ask Claude to search or ingest documents.

### MCP Inspector

```bash
make search query="hello world" query_profile=my-profile
make mcp
npx @modelcontextprotocol/inspector http://127.0.0.1:8000/mcp
```

### Bruno API files (optional)
Expand All @@ -77,11 +101,14 @@ make generate-vespa-lock
```
src/
├── entrypoints/
│ ├── ingest.py # mistralai.search.toolkit.ingestion.pipelines.Pipeline
│ └── search.py # mistralai.search.toolkit.retrieval.QueryEngine
│ ├── ingest.py # mistralai.search.toolkit.ingestion.pipelines.Pipeline
│ ├── mcp_server.py # MCP server (search + ingest tools, stdio transport)
│ └── search.py # mistralai.search.toolkit.retrieval.QueryEngine
└── vespa_app/
├── __init__.py # VespaApp — mistralai.search.toolkit.plugins.vespa
└── migrations/ # mistral-vespa migrate
├── __init__.py # VespaApp — mistralai.search.toolkit.plugins.vespa
└── migrations/ # mistral-vespa migrate
.mcp.json # MCP server config (auto-loaded by Claude Code)
.vibe/config.toml # MCP server config (auto-loaded by Vibe CLI)
sample_data/ # Sample documents
vespa/bruno/vespa/ # Generated by `make bruno` (optional)
```
Expand Down
2 changes: 2 additions & 0 deletions template/pyproject.toml.jinja
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ version = "0.1.0"
description = "A Mistral Search Toolkit project"
requires-python = ">=3.12,<3.15"
dependencies = [
"fastmcp>=3.3.1",
"httpx>=0.27",
"mistralai-search-toolkit[text-splitter-langchain,vespa]==0.0.10",
"python-dotenv>=0.9.9",
]
Expand Down
Loading
Loading