MiniSearch / docs /configuration.md
Nymbo's picture
Sync with upstream felladrin/MiniSearch (2026-10-01)
ff7a361 verified
|
Raw History Blame Contribute Delete
25.5 kB

Configuration

Environment Variables

All configuration is done via environment variables. Create a .env file in the project root.

Access Control

Variable Default Description
ACCESS_KEYS '' Comma-separated list of valid access keys (e.g., 'key1,key2,key3')
ACCESS_KEY_TIMEOUT_HOURS 24 Hours to cache validated keys in browser. Set to 0 to require validation on every request

Example:

ACCESS_KEYS="my-secret-key-1,my-secret-key-2"
ACCESS_KEY_TIMEOUT_HOURS="24"

AI Model Defaults

Configure default models for different inference types:

Variable Default Description
WLLAMA_DEFAULT_MODEL_ID littlelamb-290m Default Wllama model ID (used for both WebGPU-accelerated and CPU inference)

Server Model Request Configuration

The server-side model request defaults can be adjusted at runtime with these optional environment variables. Integer values are parsed as decimal numbers, and temperature and top-p values are parsed as floating-point numbers. An empty value uses the corresponding default.

Variable Default Description
MODEL_MAX_RETRIES 5 Maximum number of retries for a model request
MODEL_BASE_BACKOFF_MS 100 Initial retry backoff in milliseconds
MODEL_MAX_BACKOFF_MS 5000 Maximum retry backoff in milliseconds
MODEL_REQUEST_TIMEOUT_MS 30000 Model request timeout in milliseconds
MODEL_MAX_CONCURRENT_REQUESTS 10 Maximum number of simultaneous model requests
MODEL_DEFAULT_MAX_TOKENS 2048 Default maximum number of generated tokens
MODEL_TEMPERATURE 0.7 Model sampling temperature
MODEL_TOP_P 0.9 Model nucleus-sampling probability

Internal API Configuration

For self-hosted OpenAI-compatible APIs:

Variable Default Description
INTERNAL_OPENAI_COMPATIBLE_API_BASE_URL '' Base URL of your API (e.g., https://api.internal.company.com/v1)
INTERNAL_OPENAI_COMPATIBLE_API_KEY '' API key for authentication
INTERNAL_OPENAI_COMPATIBLE_API_MODEL '' Model ID to use (auto-detected if empty)
INTERNAL_OPENAI_COMPATIBLE_API_NAME Internal API Display name shown in UI

Example:

INTERNAL_OPENAI_COMPATIBLE_API_BASE_URL="https://llm.internal.company.com/v1"
INTERNAL_OPENAI_COMPATIBLE_API_KEY="sk-internal-xxx"
INTERNAL_OPENAI_COMPATIBLE_API_MODEL="llama-3.1-8b"
INTERNAL_OPENAI_COMPATIBLE_API_NAME="Company LLM"

Default Behavior

Variable Default Description
DEFAULT_INFERENCE_TYPE browser Default AI inference type (browser, openai, horde, internal)

Server Configuration

These variables control the Vite development/preview server behavior:

Variable Default Description
HOST 0.0.0.0 Host address for the Vite server to bind to
PORT 7860 Port number for the main server
HMR_PORT 7861 Port for Hot Module Replacement during development
ALLOWED_HOSTS true Comma-separated list of allowed hostnames for the preview server
BASIC_SSL false Enable basic SSL for HTTPS support during development
TRUST_PROXY false Trust X-Forwarded-For/X-Real-IP for the per-client rate-limit key. Enable only behind a reverse proxy that sets the rightmost X-Forwarded-For entry. Leave off when the instance is exposed directly - otherwise clients could forge the header to evade rate limiting.

These defaults are provided by docker-compose.yml/docker-compose.production.yml (e.g. PORT=${PORT:-7860}), not by the application itself - vite.config.ts reads these variables with no fallback, so when running directly via npm run dev/vite preview without Docker, unset variables fall through to Vite's own built-in defaults.

Dictation

Variable Default Description
DICTATION_MODELS_DIR <system temp>/minisearch-dictation-models Directory where the server caches the speech-to-text model files it serves under /dictation-models/. The files are fetched once from the pinned upstream URL on the first request, verified against a pinned SHA-256, and reused after that. They land in a subdirectory named after the model release, so a later release cannot be served from an older cache. The default lives under the system temp directory, so a container re-downloads ~51 MB after a restart unless this points at a volume

Application Settings

Settings are stored in browser localStorage and can be changed via the Settings UI.

Core Settings

Setting Type Default Description
enableAiResponse boolean false Enable AI-generated responses for searches
showEnableAiResponsePrompt boolean true Show prompt to enable AI response on first use
enableImageSearch boolean true Include image results in searches
enableTextSearch boolean true Include text results in searches
searchResultsLimit number 15 Maximum search results to fetch
systemPrompt string (template) Custom system prompt template for AI
enterToSubmit boolean true Press Enter to submit query (vs Shift+Enter for new line)
enableAiResponseScrolling boolean true Auto-scroll AI response as it generates
enableNotificationOnAiComplete boolean false Show a browser notification when AI response generation finishes
enablePageContentFetch boolean true Read the pages behind the top results and ground the answer on their text instead of on snippets alone.

Inference Settings

Setting Type Default Description
inferenceType enum 'browser' AI provider: browser, openai, horde, internal
cpuThreads number (auto) Number of CPU threads for inference (Wllama), defaults to half the logical processors (navigator.hardwareConcurrency / 2), minimum 1
allowAiModelDownload boolean false Allow automatic AI model downloads
wllamaModelId string WLLAMA_DEFAULT_MODEL_ID env var Default Wllama model ID
hordeApiKey string '0000000000' AI Horde API key (default is anonymous)
hordeModel string '' Specific AI Horde model to request
openAiApiBaseUrl string '' Base URL for the OpenAI-compatible API
openAiApiKey string '' API key for the OpenAI-compatible API
openAiApiModel string '' Model identifier for the OpenAI-compatible API
openAiContextLength number 4096 Context window size for OpenAI-compatible models, sent as max_tokens

Model Selection

Wllama Models:

  • 30+ pre-configured models
  • Range from 135M to 4B parameters
  • All quantized to Q4_K_S or UD-Q4_K_XL
  • Stored at: Felladrin/gguf-sharded-* on HuggingFace

OpenAI/Internal:

  • Any OpenAI-compatible API
  • Auto-model detection if not specified
  • Supports streaming and reasoning models

AI Horde:

  • Uses aihorde.net distributed network
  • Anonymous or authenticated access
  • Parallel generation with race conditions

History Settings

Setting Type Default Description
enableHistory boolean true Enable search history persistence
historyRetentionDays number 30 Days to keep search history
historyMaxEntries number 1000 Maximum history entries before cleanup
historyAutoCleanup boolean true Automatically clean old history entries
historyGroupByDate boolean true Group history entries by date in UI

System Prompt Configuration

The default system prompt supports template placeholders populated at runtime:

{
  systemPrompt: `Answer using the search results below as your primary source, supplemented by your own knowledge when needed. Write your response in the same language as the query.

Cite every fact taken from the search results with an inline Markdown link immediately after it. Format: [domain.com](https://full-url). Use only the top-level domain (no https://, www., or paths) as link text. Example: [youtube.com](https://www.youtube.com/watch?v=dQw4w9WgXcQ).

When the search results disagree with each other, point out the conflict. When you rely on your own knowledge because the results don't cover something, make that clear rather than presenting it as sourced.

Today's date is {{currentDate}}. Use it to resolve relative date references in both the question and the results.

You are allowed to use these Markdown elements: anchor, bold, italic, code, quote, table.

Search results:

{{searchResults}}`,
  reasoningStartMarker: '<think>',
  reasoningEndMarker: '</think>'
}

Placeholders:

  • {{currentDate}}: Current date injected at generation time
  • {{dateTime}}: Alias for {{currentDate}} - both are replaced with the same current date value
  • {{searchResults}}: Formatted search results from the web search, including the page excerpts when enablePageContentFetch is on

Reasoning Markers: Models that output internal thought processes use <think> and </think> markers. The UI extracts and separately displays reasoning content from the final response.

Privacy Settings

Setting Type Default Description
selectedVoiceId string "" Voice for reading answers aloud. piper:<voice> picks a local neural voice, system:<voiceURI> an OS voice, and "" picks one automatically for the browser language. The Voice settings panel lists only the voices of the selected engine, and switching that engine resets this to "". A bare value stored by an earlier version is read as an OS voice whenever the system engine runs; with the local engine it is ignored in favor of a language match
textToSpeechEngine "local" | "system" "local" Which engine reads answers aloud, and which set of voices the Voice settings panel lists. "system" also overrides a local voice picked above, and stops the voice list from being fetched, so it never contacts a third-party host. A profile that had picked an OS voice before this setting existed starts on "system", so the upgrade does not replace that choice with a model download
enableDictation boolean true Shows the microphone button inside the search field and the chat follow-up field, which fills whichever one it sits in while the user speaks. Turning this off hides the button and loads nothing. The button is also hidden outside a secure context, so an instance served over plain HTTP on a LAN will not show it: no browser grants microphone access there
enableLocalDictationModel boolean true only when the browser's primary language is English Whether the dictation button prefers the on-device Moonshine model, which understands English only. With it off, the button uses the browser's built-in SpeechRecognition, which handles other languages but may send the audio to the browser vendor. The preference reorders the two engines and never removes one: on browsers without SpeechRecognition the on-device model is used regardless. The default is derived from navigator.language at first load, so a non-English browser starts on the built-in recognizer with no model download; the Voice settings panel can flip it either way
reasoningStartMarker string "<think>" Marker for start of reasoning content
reasoningEndMarker string "</think>" Marker for end of reasoning content

Docker Configuration

docker-compose.yml (Development)

services:
  development-server:
    environment:
      - HOST=${HOST:-0.0.0.0}
      - PORT=${PORT:-7860}
      - BASIC_SSL=${BASIC_SSL:-false}
      - HMR_PORT=${HMR_PORT:-7861}
    ports:
      - "${PORT:-7860}:7860"
      - "${HMR_PORT:-7861}:7861"
    build:
      dockerfile: Dockerfile
      context: .
    volumes:
      - .:/home/node/app/  # Live code mounting
    command:
      [
        "(cd /usr/local/searxng/searxng-src && /usr/local/searxng/searxng-venv/bin/python -m searx.webapp > /tmp/searxng.log 2>&1) & (npm install && npm run dev)",
      ]

docker-compose.production.yml

Same structure but without volume mounts and with pre-built assets.

Dockerfile Environment

The Dockerfile sets up a single runtime stage:

  • Node.js LTS
  • Python 3 + SearXNG

The app runs under the node user, with the app directory at /home/node/app. The production image starts the app with npm start -- --host (i.e. vite preview), not npm run dev.

Multi-service container runs SearXNG and Node.js concurrently via shell process composition.

Nothing Is Configured at Build Time

The Dockerfile declares no ARG for any of the settings above, and .dockerignore excludes .env files from the build context. Both are deliberate: build args are recorded in docker history, and a copied .env becomes a readable image layer, so either one would publish ACCESS_KEYS and INTERNAL_OPENAI_COMPATIBLE_API_KEY to anyone who pulls the image.

Pass configuration when you start the container, via docker run -e, the environment: block in a compose file, or a secret manager. Building your own image with --build-arg ACCESS_KEYS=... has no effect, and neither does relying on a local .env being baked in; without runtime values the app falls back to the .env.example defaults. See Runtime Configuration for how those values reach the client.

Tuning the Search Engines

The bundled SearXNG instance decides which engines answer a query. MiniSearch ships a thin overlay for it (searxng-settings.yml) that declares two things: the JSON output format that server/webSearchService.ts consumes, and a secret_key that the Dockerfile rewrites with a random value at build time. Everything else about the engine list comes from the pinned SearXNG build, so read the set from the running instance rather than from this page: the command below shows which engines answered and which came back unresponsive. Whatever that set turns out to be, one engine that is rate-limited or CAPTCHA-challenged from your server's IP thins every result page, because the other engines have to carry the query on their own.

Diagnose before you change anything. /status needs no token and publishes searches.unresponsiveEngines, each engine classified blocked, timeout or other, and the server log prints the same names through the always-on debug in server/webSearchService.ts. See Debugging Configuration. To see the engine set itself, ask SearXNG directly, with the same parameters the app sends:

docker exec <container> /usr/local/searxng/searxng-venv/bin/python -c "
import json, urllib.request
from collections import Counter
url = ('http://127.0.0.1:8888/search?q=hiking+near+lisbon&format=json'
       '&categories=general&lang=auto&safesearch=1')
data = json.load(urllib.request.urlopen(url, timeout=60))
print(len(data.get('results', [])), dict(Counter(r.get('engine') for r in data.get('results', []))))
print('unresponsive:', data.get('unresponsive_engines'))
"

Image search asks for categories=images,videos instead, and the same switches below apply to the image engines.

SearXNG reads its settings when the container starts, so you can replace that file without rebuilding the image. It looks at SEARXNG_SETTINGS_PATH first and at /etc/searxng/settings.yml otherwise. A SEARXNG_SETTINGS_PATH that points at a file which is not there is a hard failure rather than a fallthrough: SearXNG exits with EnvironmentError: <path> not exists!. Prefer that variable, because it leaves the image's own file untouched and lets your file be mounted read-only.

The name is also a build ARG in the Dockerfile, where it only selects where the overlay is copied. A build arg does not survive into the container environment, so --build-arg SEARXNG_SETTINGS_PATH=... changes nothing at runtime; it only picks where the overlay is copied during the build.

Two switches turn engines on, and they are not interchangeable: disabled: false is for an engine that ships disabled: true, and inactive: false is for one that ships inactive: true. Which one a given engine needs is written in the pinned upstream settings file inside the container:

docker exec <container> grep -A8 -E '^  - name: (mojeek|startpage|google)$' /usr/local/searxng/searxng-src/searx/settings.yml

Setting the other one fails silently. An engine left inactive is never loaded, and one left disabled is loaded but never queried, so it simply never shows up in results and nothing reports why. Both switches were measured against the published image: with google set to disabled: false it contributed 10 of the results in that run, and with mojeek set to inactive: false it contributed 9 in another. The run totals are further down this section.

# my-searxng-settings.yml
use_default_settings: true

server:
  secret_key: "<your own random value>"

search:
  formats:
    - json

engines:
  - name: mojeek
    inactive: false
  - name: google
    disabled: false

use_default_settings: true keeps the upstream engine list and merges your entries into it by engine name, and search.formats: [json] must stay, because it is how MiniSearch reads the response. The full vocabulary is in the SearXNG settings docs.

docker run -p 7860:7860 \
  -e SEARXNG_SETTINGS_PATH=/etc/searxng-custom/settings.yml \
  -v "$(pwd)/my-searxng-settings.yml:/etc/searxng-custom/settings.yml:ro" \
  ghcr.io/felladrin/minisearch

The compose equivalent:

services:
  minisearch:
    image: ghcr.io/felladrin/minisearch:latest
    ports:
      - "7860:7860"
    environment:
      - SEARXNG_SETTINGS_PATH=/etc/searxng-custom/settings.yml
    volumes:
      - ./my-searxng-settings.yml:/etc/searxng-custom/settings.yml:ro

Two things about that command are required, not optional:

  • The file must be readable by uid 1000. The container runs as node, and a file readable only by its owner on the host is invisible to it.
  • secret_key must be set to your own random value. SearXNG refuses to start on the ultrasecretkey placeholder and logs server.secret_key is not changed. Please use something else instead of ultrasecretkey. The image's own file never carries the placeholder, because the Dockerfile replaces it at build time, but a file you write has to carry a real key.

A SearXNG startup failure will not show in docker logs. The container's CMD sends SearXNG's output to /dev/null, deliberately, so that the app server's log stays clean. To see it, run SearXNG in the foreground inside the container, which is only possible once the original has failed: on a running instance the second process collides with the live one on 127.0.0.1:8888 and reports the address as already in use.

docker exec <container> sh -c 'cd /usr/local/searxng/searxng-src && /usr/local/searxng/searxng-venv/bin/python -m searx.webapp'

To check the override took effect, print the flags of the engines you changed. Printing the names proves nothing, because every engine name is in the upstream list whether or not your entry was applied:

docker exec <container> /usr/local/searxng/searxng-venv/bin/python -c "
import searx.settings_loader as sl
settings, source = sl.load_settings()
print(source)
print([(e['name'], e.get('disabled'), e.get('inactive'))
       for e in settings['engines'] if e['name'] in ('mojeek', 'google')])
"

Then run the query command from the top of this section again and compare which engines answered. In a run against the published image on 2026-09-22, the default settings returned 24 results for one query, all from brave, with duckduckgo reported CAPTCHA and google cse reported as crashed. The same query with mojeek switched on returned 42 results across brave, mojeek and google cse, and a third run with google switched on returned 40 across google, brave and google cse. Treat those as measurements of one host on one day: which engines answer depends on your IP and on the hour, and google cse came back on its own between the first run and the second.

Engine choice does not change the privacy posture. SearXNG still makes the request, from your server's address, so the engine sees the instance and not the user, and no query goes to any third party that SearXNG did not already talk to.

None of this reaches MiniSearch's code. The app asks SearXNG for one JSON document and reranks whatever comes back, so nothing in server/webSearchService.ts changes when you retune the engines, and the circuit breaker, the retries and the unresponsive_engines reporting keep working over the new set.

Runtime Configuration

Client-facing configuration (access keys, inference type, internal API settings) is resolved at runtime via the /api/config endpoint. The client fetches this endpoint on app initialization, so the published Docker image is fully configurable via environment variables at runtime - no rebuild needed.

/api/config Response

{
  "accessKeysEnabled": true,
  "accessKeyTimeoutHours": 24,
  "wllamaDefaultModelId": "littlelamb-290m",
  "internalApiEnabled": true,
  "internalApiName": "Internal API",
  "defaultInferenceType": "browser",
  "searchToken": "<search-token>"
}

Build-Time vs Runtime Configuration

Value Resolved At Notes
VITE_BUILD_DATE_TIME Build time Epoch milliseconds when the build occurred
VITE_COMMIT_SHORT_HASH Build time Git commit hash at build time (if available)
ACCESS_KEYS Runtime Read from /api/config
ACCESS_KEY_TIMEOUT_HOURS Runtime Read from /api/config
WLLAMA_DEFAULT_MODEL_ID Runtime Read from /api/config
INTERNAL_OPENAI_COMPATIBLE_API_* Runtime Read from /api/config (except API_KEY which is server-only)
DEFAULT_INFERENCE_TYPE Runtime Read from /api/config
Search token Runtime Read from /api/config, so a reload picks up the token of whichever server answers

Security Considerations

  • VITE_BUILD_DATE_TIME and VITE_COMMIT_SHORT_HASH are bundled into the client JavaScript as build-time constants (build metadata only)
  • The CSRF token is served at runtime like the rest of the config. It reaches any caller that can reach /api/config, which is the same exposure it had while it was compiled into the bundle
  • All other configuration is fetched at runtime from /api/config and never appears in the bundled JavaScript
  • Server-only variables like INTERNAL_OPENAI_COMPATIBLE_API_KEY are never exposed to the client

Configuration Patterns

Scenario: Private Team Instance

# .env
ACCESS_KEYS="team-alpha-2024,team-beta-2024"
ACCESS_KEY_TIMEOUT_HOURS="8"
DEFAULT_INFERENCE_TYPE="internal"
INTERNAL_OPENAI_COMPATIBLE_API_BASE_URL="https://llm.company.com/v1"
INTERNAL_OPENAI_COMPATIBLE_API_KEY="sk-xxx"
INTERNAL_OPENAI_COMPATIBLE_API_MODEL="llama-3.1-70b"

Scenario: Public Demo (No AI)

# .env - empty, no access keys
# AI disabled by default in settings

Scenario: Browser-Only AI

# .env - minimal or empty
# Users choose the Wllama model in settings (WebGPU used automatically when available)
# Models download to user's browser (no server AI)

Debugging Configuration

MiniSearch logs internal events to an in-app log panel (see the Logs section of the menu), backed by logEntriesPubSub in client/modules/logEntries.ts. There is no separate browser-console debug flag to enable.

Diagnosing empty search results: the client distinguishes a search that returned no results (a no-results alert, state stays completed) from one that failed outright on an outage (a distinct failed state, with a retry on text searches). An empty response that names unresponsive engines is retried within the same budget as a 500 and counts as the second if it is still empty when the retries are spent, since a rate-limited, suspended or CAPTCHA-challenged engine is reported that way and not with a status code. The actual reason is printed to the server's console (via server/webSearchService.ts, always-on debug logging), either the SearXNG engines that failed (timeouts, suspensions, rate limits, from SearXNG's unresponsive_engines field) or a note that all returned results were discarded during processing (missing title, snippet, or media source). Check the server logs (docker compose logs) when troubleshooting failed searches.

Check effective configuration:

// In browser console
console.log('Settings:', JSON.parse(localStorage.getItem('settings') || '{}'));
console.log('Server config:', await fetch('/api/config').then(r => r.json()));

Related Topics

  • AI Integration: docs/ai-integration.md - Detailed inference type configuration
  • Security: docs/security.md - Access control and privacy details
  • Deployment: docs/overview.md - Container architecture and production setup