
Changelog
v2.3.3
- Removed v2.3.2’s diagnostic logging – confirmed live that the follow-up cap works correctly. The earlier “it keeps going” report was most likely follow-ups tested across more than one original answer, each getting its own independent 5-follow-up allowance by design, rather than a shared global count.
v2.3.2
- Added temporary diagnostic logging for v2.3.1’s follow-up cap: confirmed live that the code is deployed correctly, but the cap isn’t actually triggering after 6+ follow-ups on the same answer. Logs now show the computed cache keys and the count read/written on every follow-up call, to pin down whether the counter is persisting at all or being read back incorrectly.
v2.3.1
- Capped organic results per page to 15 (
search.results_per_page) – a display/UX change, not a performance one: every engine is still queried exactly as before, this only trims the merged/ranked list before rendering. - Capped AI answer follow-ups to 5 per original answer
(
ai_answer.MAX_FOLLOWUPS_PER_ANSWER), enforced server-side (not just disabled in the UI) since it’s the real bound on the free-text follow-up endpoint’s cost exposure. Re-asking a question already answered in that thread doesn’t count against the cap – it’s served from cache, no new OpenAI call. Once hit, the input disables with a clear message instead of silently failing on every further attempt.
v2.3.0
- In-place AI answer follow-ups. The AI answer box now has a free-text “Ask a follow-up…” input, and clicking one of the suggested related questions no longer navigates to a new search – both ask the follow-up directly inside the answer box, appended below as a growing thread, and the organic results on the page are never touched or re-fetched. Follow-ups are grounded in the exact same snippets that answered the original query (no new search runs for them), and each answer comes with its own fresh set of related-question suggestions so the conversation can keep going. Free-text input means this endpoint is bounded only by the existing per-IP rate limit and a length cap (300 chars) rather than being restricted to server-suggested questions – accepted deliberately for the flexibility, worth watching if API cost becomes a concern.
- Relabeled “AI answer” to “Interstellar AI Answer” throughout the UI.
v2.2.1
- Added diagnostic logging for v2.2.0’s related-question suggestions:
confirmed live that the answer text itself parses correctly (JSON mode is
working), but
related_questionsis coming back empty even for fresh, never-before-searched queries – logs now capture the model’s raw response when that happens, to pin down whether the model is omitting the key entirely or returning it in an unexpected shape.
v2.2.0
- AI answers now suggest follow-up questions, similar to Google’s AI Overview – up to 3 clickable related questions appear below the answer and sources. Generated in the same OpenAI call as the answer itself (via JSON-mode structured output) rather than a second round-trip, so there’s no added latency or cost. Clicking one re-runs the search for that question using the page’s existing search form, so it respects whatever language/time-range/safesearch/category filters are already active. Falls back gracefully (answer still renders, just without suggestions) if the model’s response doesn’t come back in the expected shape.
v2.1.6
- Corrected additional wording issues within the changelog for clarity and consistency.
v2.1.5
- Reworded the v1.2.1 changelog entry to drop literal old-filename mentions.
v2.1.4
- Tidied up the v1.0.0 changelog entry’s wording.
v2.1.3
- Renamed the internal exception-class hierarchy and their related settings keys to match this project’s own naming, for consistency with the rest of the codebase and to make future code easier to reason about without carrying an old naming scheme. Purely an internal rename – no behavior change.
v2.1.2
- Reworded a couple of old changelog entries that named an outside project,
now that this page is user-facing (
/changelog). Left the internal exception class names alone at the time, since they still matched real code – see v2.1.3, which renamed them too.
v2.1.1
- Fixed a site-wide crash (
TypeError: 'NoneType' object is not callableon every single page) introduced by v2.1.0’s footer link. Root cause: the footer’s{% set _ = footer_links.append(...) %}idiom (used to discardlist.append()'s return value) reassigns the Jinja variable_– which is also the gettext translation function used everywhere else on the page.{% set %}inside an{% if %}block does not get scoped away in Jinja2 (only{% for %}loops do), so this was actually a latent bug already present before this release – it just never triggered because no two of the conditional footer links (Engine stats/Public instances/Privacy policy/Contact/custom links) were ever simultaneously enabled on this instance. The new unconditional “Update history” link was the first call after the clobbering point that always executes, exposing it immediately. Fixed by renaming the throwaway variable to_discardeverywhere in the footer so_is never overwritten, verified against every combination of footer links enabled at once.
v2.1.0
- Added an Update history page (
/changelog, linked in the footer next to “Engine stats”) that renders this very file as HTML, so it always reflects whatever’s actually shipped – no separate content to maintain. The Dockerfile now copiesCHANGELOG.mdinto the image the same wayVERSIONalready was.
v2.0.9
- Disabled Google Images, Unsplash, and Brave Images – all three were confirmed this session to be consistently blocked (“access denied” / “too many requests”) from this instance’s IP range, with no free/official API alternative available (Google’s official image search API is capped at 100 queries/day total, impractical for live traffic; Unsplash’s would work but wasn’t wanted; Brave has no public API at all). Rather than leave them silently failing on every search, they’re now disabled outright – the Images tab’s working set is Pinterest, Openverse, Wikimedia Commons, Art Institute of Chicago, Pixabay, and Pexels.
v2.0.8
- Switched Flickr and Pexels to their official, documented APIs instead of
scraping, same treatment as Pixabay in v2.0.6:
- Flickr: wired up the already-present official-API engine
(
flickr_api) to a newflickr.api_keysetting (INTERSTELLAR_FLICKR_API_KEY) instead of a hardcoded YAML value, and made it self-gate viainit()like the other API-key engines. Disabled the old HTML-scrapingflickrengine (confirmed erroring this session) since the official one supersedes it, but left it in place rather than deleting it in caseflickr.api_keyis ever left unset. - Pexels: rewrote the engine to call
api.pexels.com/v1/searchwith anAuthorizationheader instead of scraping a “secret key” out of pexels.com’s own website first (2-3 sequential requests, a likely source of its timeouts). Added apexels.api_keysetting (INTERSTELLAR_PEXELS_API_KEY). - Both stay quietly deregistered with no outgoing requests until their key is set, matching the ai_answer/pixabay pattern.
- Flickr: wired up the already-present official-API engine
(
v2.0.7
- Raised the global per-engine result cache TTL from 5 minutes to 24 hours
(
search.result_cache_ttl), giving generous headroom against free/rate -limited API engines (Pixabay, etc.) so repeated identical queries from any user don’t re-hit those upstreams. This is a single site-wide setting, not per-engine, so it applies to every engine equally – including time-sensitive ones like News, which will now serve up to a day-old cached response for a repeated identical query.
v2.0.6
- Switched the Pixabay engine from scraping pixabay.com’s own website
(reliably bot-blocked, confirmed this session) to Pixabay’s official,
documented API (https://pixabay.com/api/docs/), authenticated with a free
API key. Added a
pixabay.api_keysetting (INTERSTELLAR_PIXABAY_API_KEYenv var override, same pattern as the OpenAI key) – the engine’s owninit()deregisters it when no key is configured, so it stays inert by default rather than falling back to scraping. Set the key to enablepixabay images/pixabay videos.
v2.0.5
- Raised the default per-engine request timeout from 3.0s to 6.0s (and
max_request_timeoutfrom unset to 10.0s so per-engine overrides have headroom). Several image engines with no timeout override of their own (Bing, DuckDuckGo, Mojeek, Qwant, Startpage, Pexels) were consistently timing out against the previous 3s default – Pexels in particular has to make 2-3 sequential requests (secret-key fetch, script fetch, then the real search call) that don’t comfortably fit in that budget.
v2.0.4
- Cleaned up how
brave.images/brave.videosfail when Brave’s bot-detection blocks the request with a 200-status JS-challenge page instead of a results page (confirmed independently – Brave now returns a flat 403 to this kind of request even from an unrelated network, so this is Brave-side blocking, not fixable from here). Previously this crashed with a rawValueError: substring not foundfrom deep inside the HTML scraper; it now raises a properSearxEngineAccessDeniedExceptionso it shows as a clean “Suspended: access denied” like other blocked engines, and is handled by the existing suspension/backoff logic instead of an unhandled exception.
v2.0.3
- Fixed per-engine result caching silently failing for every engine whose
name contains a “.” (e.g.
brave.images,media.ccc.de,core.ac.uk,wikicommons.images,piped.music,lib.rs,pkg.go.dev) – the cache table name was built with “.” allowed through as a “safe” character, but it’s interpolated unquoted into raw SQL, so SQLite read e.g.resp_brave.imagesas “tableimagesin databaseresp_brave” (never attached) instead of one identifier, failing the write withunknown database resp_brave. Cache writes run in the background and failures were only logged, so search results were never affected – these engines just never benefited from caching. Dots are now replaced with underscores like any other non-identifier character.
v2.0.2
- Fixed the AI answer trying (and failing) to generate on the Images tab (and other non-General tabs like Videos/Music/Map) – image results carry filenames and dimensions rather than prose, so the model had nothing usable to summarize and produced a garbage “I can’t answer” response sourced from icon/thumbnail filenames. The plugin now only runs on the General tab.
- Fixed the homepage logo rendering off-center above the search bar – the source SVG’s viewBox left a large empty margin to the right of the actual icon+wordmark content, and centering the whole canvas (rather than just the visible content) shifted the logo left. Tightened the viewBox to the content’s real bounding box.
v2.0.1
- Fixed
search.google/about.googlestill showing as results (confirmed live) – the v2.0.0 hostnames filter only coveredgoogle.com, missing that Google also serves pages directly under the bare.googlegTLD it owns. Added^(.*\.)?google$to the remove list.
v2.0.0
- Filtered out competing search engines from results. Added a curated
hostnames.removelist (Google, Bing, Yahoo, DuckDuckGo/duck.ai, Yandex, Baidu, Startpage, Brave Search, Qwant, Mojeek, Ecosia, Ask, AOL, Presearch, Sogou/So, Naver, Seznam, Swisscows, Dogpile, MetaGer, Excite, Lycos) to the existing (previously unused)hostnamesplugin – once a user clicks through to another search engine’s own site, this instance’s privacy protections no longer apply, so those results are now dropped entirely rather than shown. Hostname-only match, anchored at both ends, so it won’t false-positive on lookalike domains. - AI answers now load asynchronously. Previously, a question-like query
blocked the entire results page on the full OpenAI round-trip (up to
15s). Now
/searchrenders immediately (organic results, infobox, etc.) with a “Generating AI answer…” placeholder, and a follow-up request fetches the real answer and swaps it in once ready – typically without the user noticing any wait at all for everything else on the page. The new/ai_answerendpoint is protected the same way the feature already guarded against abuse: covered by the existing per-IP rate limiter, and the query+snippets it’s given are verified against a tamper-evident token generated when the page was rendered, so it can’t be used as a freeform prompt proxy with attacker-supplied content. - Bigger logo on both the homepage and next to the search box on results pages.
v1.10.1
- Restored the placeholder answer (“Not enough search terms provided for an AI response.”) for non-question queries, now unconditional rather than tied to the checkbox removed in v1.10.0. Plain keyword searches make it clear why no AI summary appeared instead of silently showing nothing.
v1.10.0
- Removed the “AI answer” checkbox entirely. Every search is now evaluated
the same way, purely server-side: question-like queries automatically get
an AI answer, plain keyword searches get none, with no per-search toggle,
hidden-field syncing, or client-side JS state to go wrong. This eliminates
the checkbox/autocomplete edge case from v1.9.1 (and the class of similar
bugs it belonged to) by removing the mechanism rather than patching around
it.
SearchQuery.ai_answerand its webadapter/webapp/template plumbing are gone; theai_answerplugin now gates purely onlooks_like_question(query). Non-question queries get no answer and no placeholder (there’s no checkbox left to explain), same as any other engine that simply has nothing to contribute.
v1.9.1
- Fixed the “ai_answer” checkbox not resetting to checked when clicking an
autocomplete suggestion for a new question right after a non-question
search left it unchecked (confirmed live: searching “Duck” correctly
unchecked it, but then clicking the “What is a Duck?” suggestion didn’t
re-check it). Root cause: the autocomplete dropdown submitted the form via
HTMLFormElement.submit(), which – unlikerequestSubmit()– doesn’t fire the “submit” event, so it bypassed all of search.ts’s sync logic entirely (not just the new ai_answer reset, but category-tab sync too). Switched it torequestSubmit()so autocomplete-driven searches go through the exact same submit path as typing and pressing Enter.
v1.9.0
- The “AI answer” checkbox is now checked by default and runs automatically for question-like queries (starts with what/why/how/who/when/where/which, a yes/no auxiliary like is/are/can/does, or ends in “?”), instead of needing to be manually checked every time. Plain keyword searches are left alone – the checkbox auto-unchecks itself and shows a placeholder (“Not enough search terms provided for an AI response.”) instead of silently doing nothing, so it’s clear why no summary appeared. Typing a genuinely new query resets the checkbox back to checked even if the previous query had unchecked it; changing a filter on the same query leaves it as-is. Manually unchecking it still fully disables the feature for that search, same as before.
- AI answers are noticeably longer and more detailed now – the prompt asks for a couple of full paragraphs instead of 2-4 sentences, and each source snippet fed into the prompt is allowed nearly twice as much of the original result’s text (900 vs. 500 characters) to draw from. Bumped the request timeout from 8s to 15s to give the longer completions room to finish.
- Skips the API call entirely for non-HTML output (JSON/CSV/RSS), since those consumers aren’t rendering the answer box.
v1.8.2
- Moved the question-prefix-stripping from v1.8.0 (reverted in v1.8.1 along
with disabling
wikidata) onto thewikipediaengine instead. Its infobox resolves the query as a literal page title with Wikipedia’s own redirect handling, which – unlike Wikidata’s full-text entity search – isn’t prone to matching a same-labeled but unrelated topic, so this gets the infobox showing up for natural-language questions again (“what is a duck”) without reintroducing the wrong-entity problem that came with Wikidata.
v1.8.1
- Disabled the
wikidataengine. Its own entity search (a full-text lookup across all Wikidata items) can rank a same-labeled but wrong item – e.g. a “Duck” surname/family-name entity – above the actual topic for common/ambiguous nouns, showing up as an infobox with no image and mismatched text (confirmed live for “duck”). Thewikipediaengine’s own infobox (title/redirect based, not full-text entity search) already covers this reliably with the correct entity and an image, so this removes the confusing/wrong second card rather than trying to fix Wikidata’s disambiguation. Reverts the v1.8.0 question-prefix-stripping change ininterstellar/engines/wikidata.py, since it’s unreachable with the engine disabled.
v1.8.0
- Improved how often the Wikidata infobox (image/description/attributes
card) actually shows up. It’s driven by Wikidata’s own EntitySearch,
which matches short, entity-like queries (“chicken”) far more reliably
than full-sentence questions (“what is a chicken”) – the infobox was
silently not appearing for a large share of natural-language queries.
interstellar/engines/wikidata.pynow strips a leading question phrase (“what is/are/was/were”, “who is/are/was/were”, “define”, “explain”, “tell me about”, plus a following “a”/“an”/“the”) before sending the query to EntitySearch, without changing the query shown to the user or passed to any other engine.
v1.7.4
- Fixed the
ai_answerplugin generating a real, correct summary (confirmed via OpenAI’s own usage logs) that then never appeared on the results page. Root cause:ResultContainer.get_ordered_results()has a side effect of permanently closing the container (ResultContainer.close()) the first time it’s called; the plugin was calling it to build the RAG context before_core.py’s plugin dispatcher callsresult_container.extend()to actually add the generated answer, so by the time the answer was ready the container was already closed and silently discarded it (“container is closed, ignoring results”). Now readsresult_container.main_results_map.values()directly (sorted the same wayget_ordered_results()'s first pass does) instead, leaving the container open for the answer to actually be added.
v1.7.3
- The
ai_answerplugin was still getting a 429 from OpenAI even after confirming billing/account access is fine (a direct curl with the same key succeeded) –interstellar.network’s default error handling collapses every 4xx/5xx into a generic exception message, discarding the actual response body and rate-limit headers that would explain why (requests-per-minute vs. tokens-per-minute vs. quota are all reported as a plain 429). Now handles the response status directly (raise_for_httperror=False) and logs OpenAI’s actual error body plus anyx-ratelimit-*/retry-afterheaders on failure, so the real cause is visible instead of a generic “Too many request”.
v1.7.2
- Fixed the
ai_answerplugin still failing after the network-context fix (confirmed live): OpenAI’s error responses (402/403 -> access denied, 429 -> too many requests / quota) get translated byinterstellar.network.raise_for_httperrorinto this codebase’s ownSearxEngineAccessDeniedException/SearxEngineTooManyRequestsException, neither of which is a subclass ofhttpx.HTTPError– so the plugin’sexcept (httpx.HTTPError, ...)never caught them, and the failure surfaced as an unhandled “Exception while calling post_search” instead of a clean, informative warning. Now catchesinterstellar.exceptions.SearxExceptiontoo, and logs the exception type alongside its message so the actual reason (e.g. an OpenAI account/billing/quota problem vs. a real outage) is visible in the logs going forward.
v1.7.1
- Fixed the
ai_answerplugin’s OpenAI request silently failing on every search (confirmed live:interstellar.network.image_proxy: HTTP Request failed: POST https://api.openai.com/v1/chat/completions). Root cause:interstellar.networkkeeps a per-thread “network” context used to give engines/image-proxying their own proxy/timeout settings, and since Flask/gunicorn reuse worker threads across unrelated requests, a thread that had previously handled an/image_proxyrequest left that context set indefinitely – so a later/searchrequest handled by the same thread made its OpenAI call under the image-proxy network config instead of the default outgoing settings. Fixed by explicitly resetting the thread’s network context (set_context_network_name) before the request, the same way engines already do insearch/processors/online.py.
v1.7.0
- Added an opt-in “AI answer” feature: a checkbox next to the other search
filters that, when checked, summarizes the top organic results into a
short answer using an OpenAI-compatible Chat Completions API, grounded
only in those results (not general model knowledge), with linked sources
shown below the summary. Fully inert unless an API key is configured
(
openai.api_key/INTERSTELLAR_OPENAI_API_KEY) – the plugin deregisters itself and the checkbox never renders otherwise, and nothing is sent anywhere unless a user explicitly checks the box for that search. Answers are cached for 6h (keyed on the query and the grounding results’ URLs) to avoid repeat API cost on repeat searches. Runs strictly after the normal engine fan-out completes, so using it adds directly to that search’s response time (default timeout 8s, configurable).
v1.6.1
- Disabled the
redditengine again after live testing confirmed a 403 on every request from this server (/statsshowed 100% access-denied) – same IP-reputation blocking as Google Scholar and kickass.
v1.6.0
- Added security response headers:
Content-Security-Policy,X-Frame-Options: DENY, alongside the existingX-Content-Type-Options/X-Download-Options/X-Robots-Tag/Referrer-Policy. HSTS deliberately left to the reverse proxy, since that’s where TLS actually terminates. - Wired up the Valkey connection in docker-compose.yml (
INTERSTELLAR_VALKEY_URLdefaulting to the already-provisionedvalkeyservice) – previously provisioned but never actually connected to. Needed for the rate limiter and any other valkey-backed feature. - Re-enabled the
redditengine for the Social Media category (was disabled by default; untested against this specific server – may hit the same kind of IP-based blocking as Google Scholar/kickass did). - Added
container/backup.sh: backs upcore-config/(settings.yml, secrets) and.envto a local timestamped archive with retention pruning, meant to be run from cron. Cache volumes are intentionally not included (regenerable, not a source of truth). - Added
.gitignoreentries forcontainer/core-config/,container/.env, andcontainer/backups/– defense in depth, since the deploy directory on the server is the same git checkout.
v1.5.1
- Fixed auto-submitting filters (safesearch/time-range/language, and the
new image size/license filters) dropping the selected category tab and
reverting to General.
HTMLFormElement.submit()doesn’t fire thesubmitevent, so the logic that copies the active category tab into the hiddencategoriesfield – previously only wired into that event – never ran for these. Extracted into a shared helper now called before every auto-submit, not just real button clicks.
v1.5.0
- Removed the “cached” link from every result card (it was the only thing left in that footer after source attribution was hidden earlier).
- Added image search filters: size (small/medium/large/extra large) and a
royalty-free toggle. Both only appear when the Images category is
selected. Wired into Bing Images (
filterui:imagesize-*/filterui:license-*qft parameters, combined with the existing time-range filter); other image engines simply ignore these filters rather than being excluded, so results from Openverse/Flickr/etc. still show up unfiltered.
v1.4.1
- Disabled
welib(confirmed access-denied – Cloudflare bot wall blocks server-side requests as expected) andlibrary genesis(confirmed still failing to connect even on the libgen.is mirror). Files category now relies onfindfilesandwikicommons.files, both working.
v1.4.0
- Added welib.org as a new Files-category source. It runs on the same codebase as Anna’s Archive (identical search UI and URL pattern), so it’s configured as a new “welib” entry reusing the existing annas_archive engine rather than writing a new scraper from scratch. Note: welib.org sits behind a Cloudflare JS challenge, so results may be unreliable depending on how the site treats server-to-server requests – report back if it shows access-denied/timeouts.
v1.3.1
- Switched Library Genesis from libgen.rs to the libgen.is mirror after libgen.rs consistently failed to connect (ConnectTimeout, then ConnectError: all connection attempts failed). Note: DNS shows both domains currently resolving to the same IP, so this may not resolve it if the block is IP-based – disable the engine if it still fails.
v1.3.0
- Reworked the Files category away from torrents entirely:
- Disabled
kickass(all mirrors returning access-denied/bot-blocked) andsolidtorrents(scraper out of sync with the site, consistent parsing error on every query). - Disabled the remaining torrent engines that were still active
(
bt4g,piratebay) since this instance is meant for downloadable documents/files, not torrent search. - Enabled
findfiles(PDFs, documents, archives, datasets) andlibrary genesis(books/ebooks) to actually cover that use case.wikicommons.fileswas already enabled and fits the same intent.
- Disabled
v1.2.2
- Disabled the Google Scholar engine by default. Its bot detection blocks based on IP reputation rather than request behavior, so it flags VPS/datacenter IPs almost universally – it was spending most of its time suspended rather than returning results. Semantic Scholar, PubMed, arXiv, and the OpenAIRE engines still cover the Science category.
v1.2.1
- Fixed the site still being fingerprintable by tech-detection tools and browser extensions: the built static assets and a CSS class used for map results still carried an old internal naming scheme left over from a prior rename pass. Renamed the Vite build’s output prefix and the map class to match the rest of the project, and rebuilt the frontend so the shipped files actually reflect it.
v1.2.0
- Removed the “Preferences” button from the top nav bar. The
/preferencespage and all of its functionality are unchanged and still fully reachable by visiting the URL directly – it’s just no longer surfaced as a link anywhere in the UI.
v1.1.3
- Brought the German, French, Italian, Indonesian, and Persian About pages in line with the rewritten English one: same structure, same content (removed Weblate/Matrix/source-repo/public-instances references, same “how does it protect my privacy” explanation), each in its own language.
v1.1.2
- Rewrote the About page: removed the “How can I make it my own?” section and other source/contribution references pointing at the private repo (not accessible to anyone but us), and replaced that section with a concrete rundown of how the search actually protects users’ privacy (no profiling, engines never see the user directly, no tracking cookies, optional image proxying).
v1.1.1
- Cleaned up the footer: dropped the “Source code” and “Issue tracker” links (repo is private, they don’t resolve for anyone but us) and softened the tagline wording accordingly. Remaining links (engine stats, privacy policy, etc.) only render when actually configured, so there’s no stray separator when some are turned off.
- Removed the “+dirty” version suffix – it was a false positive on every build, since the Docker build context only copies part of the repo and git always saw the rest as “missing” relative to HEAD.
v1.1.0
- Removed the “change engine in preferences” and “switch to another instance” suggestions from the no-results message (single-instance deployment, preferences page going away).
- Fixed the footer always showing a hardcoded placeholder version regardless of what was actually deployed.
- Version is now a manually-bumped
MAJOR.MINOR.PATCH(this file), instead of an automatically generated date+commit-hash string – it only changes when we deliberately ship a new build.
v1.0.0
First stable build of Interstellar Search.
- Created custom Docker base images instead of depending on a third-party published image.
- Build New UI theme.
- Per-engine result caching with configurable TTL (
search.result_cache_ttl). - Exponential backoff for engine suspension.
- Bounded concurrent engine requests per query (
search.max_concurrent_requests). - Autocomplete enabled by default; fixed missing User-Agent on autocomplete requests (was causing silent 403s from backends like Wikipedia).
- App-level spelling correction / “did you mean” suggestions.
- URL normalization before result dedup.
- Per-engine source attribution hidden from result cards and the image detail popup.
- Fixed a SQLite schema-init race condition and a self-deadlock in the cache layer that caused every engine to time out whenever result caching was enabled.