Skip to content

✨ feat(engine): qwant for the search engine#784

Merged
neon-mmd merged 4 commits intorollingfrom
FEAT/317_qwant-for-the-search-engine
Apr 16, 2026
Merged

✨ feat(engine): qwant for the search engine#784
neon-mmd merged 4 commits intorollingfrom
FEAT/317_qwant-for-the-search-engine

Conversation

@neon-mmd
Copy link
Copy Markdown
Owner

@neon-mmd neon-mmd commented Apr 15, 2026

What does this PR do?

Adds Qwant as a new upstream search engine, querying the Qwant v3 JSON API (api.qwant.com/v3/search/web).

Changes:

  • New engine file src/engines/qwant.rs implementing the SearchEngine trait
  • Uses fetch_json_as_bytes_from_upstream for HTTP fetching (consistent with SepiaSearch)
  • Parses JSON response with serde, filtering for web results only (ignoring ads, related searches, etc.)
  • Surfaces API errors (including captcha / error code 27) with context rather than silently masking
  • Pagination via offset parameter, 10 results per page
  • Safe search mapped to Qwant's safesearch query parameter
  • Unit tests for success response, non-web item filtering, error response, and empty results
  • Registered in mod.rs, the engine match statement in engine.rs, and config.lua

Why is this change important?

Qwant is a privacy-focused search engine that provides an alternative to Google/Bing-based results. Adding it gives websurfx users another upstream option that respects user privacy.

How to test this PR locally?

  1. Enable Qwant in websurfx/config.lua:
    Qwant = true,
  2. Build and run:
    cargo run
  3. Search for any query with Qwant enabled. Results should appear from Qwant.
  4. Run unit tests:
    cargo test --lib engines::qwant

Author's checklist

  • Follows existing engine patterns (struct + SearchEngine trait impl + constructor)
  • All struct fields have doc comments
  • Uses fetch_json_as_bytes_from_upstream for HTTP fetching
  • Query parameter is properly URL-encoded via form_urlencoded
  • API errors are detected and surfaced with context
  • No new dependencies required (form_urlencoded already present from SepiaSearch)
  • Unit tests for normal response, filtering, error, and empty results
  • Bump the app to v1.28.0

Related issues

Takes over #605 #783 (original work by @nrabulinski, and Franz Kafka). Closes #317.

Credit

Based on initial implementation by @nrabulinski in #605 and Franz Kafka in #783 — the serde response models and API structure were ported from their work.

Summary by CodeRabbit

  • New Features

    • Added Qwant as a new upstream search engine option with full result parsing, filtering, and robust error/empty-result handling. Disabled by default in config.
  • Tests

    • Added unit tests covering successful parsing, filtering non-web items, API error handling, and empty-result behavior.
  • Chores

    • Package version bumped to 1.28.0.

neon-mmd and others added 3 commits April 15, 2026 23:28
- Provide the implementation for fetching search results from the `qwant` search engine using the api.
- List the new implementation in the engine models.
- Make the implementation module public.

Co-authored-by: nrabulinski <[email protected]>
Co-authored-by: Franz Kafka <[email protected]>
Co-authored-by: nrabulinski <[email protected]>
Co-authored-by: Franz Kafka
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Apr 15, 2026

📝 Walkthrough

Walkthrough

Adds a new Qwant search engine: implements a Qwant engine module with JSON parsing and request handling, exports and registers it in the engine handler, adds a disabled-by-default config toggle, and bumps the crate version to 1.28.0.

Changes

Cohort / File(s) Summary
Version Management
Cargo.toml
Bumped crate version from 1.26.0 to 1.28.0.
Engine Exports & Registration
src/engines/mod.rs, src/models/engine.rs
Exported new qwant submodule and added qwant branch in EngineHandler::new to instantiate Qwant.
Qwant Engine Implementation
src/engines/qwant.rs
New Qwant struct and SearchEngine impl: builds API requests, sets headers, fetches JSON bytes, deserializes Qwant JSON, filters web mainline items, maps to SearchResult, handles API errors and empty results; includes unit tests.
Configuration
websurfx/config.lua
Added Qwant = false toggle to upstream_search_engines (disabled by default).

Sequence Diagram

sequenceDiagram
    participant Client as Client/User
    participant Engine as Qwant Engine
    participant Upstream as Qwant API
    participant Parser as JSON Parser

    Client->>Engine: results(query, page, user_agent)
    activate Engine
    Engine->>Engine: build URL (q=..., offset=page*10)
    Engine->>Engine: set headers (User-Agent, Referer)
    Engine->>Upstream: HTTP GET (Accept: application/json)
    activate Upstream
    Upstream-->>Engine: JSON bytes
    deactivate Upstream
    Engine->>Parser: parse_json_response(bytes)
    activate Parser
    Parser->>Parser: deserialize JSON -> check errorCode
    Parser->>Parser: extract result.items.mainline where type == "web"
    Parser-->>Engine: Vec<(url, SearchResult)> or EngineError
    deactivate Parser
    Engine-->>Client: Result<Vec<(String, SearchResult)>, EngineError>
    deactivate Engine
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Possibly related PRs

Suggested labels

🕹 aspect: interface

Suggested reviewers

  • jfvillablanca
  • spencerjibz
  • alamin655

Poem

🐰 A new engine hops on the trail tonight,
Qwant brings results in JSON bright,
I parse and filter with whiskered care,
Return the links with a joyful flair,
Websurfx grows — a rabbit's delight! 🌿

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly describes the main change: adding Qwant as a new search engine to the codebase.
Linked Issues check ✅ Passed All requirements from issue #317 are met: Qwant engine struct created, SearchEngine trait implemented with parser logic, engine registered in match statement, and added to config.lua with proper tests.
Out of Scope Changes check ✅ Passed All changes are within scope: version bump (v1.28.0) supports the new feature release, and all modifications directly relate to Qwant engine implementation and configuration.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch FEAT/317_qwant-for-the-search-engine

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@neon-mmd neon-mmd moved this from Todo to Under Review in Task list for v2.0.0 release cycle Apr 15, 2026
@neon-mmd neon-mmd requested a review from spencerjibz April 15, 2026 20:44
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/engines/qwant.rs`:
- Around line 146-148: The calculation let offset = page * count can overflow
for large page values; replace the direct multiplication with a checked or
saturating operation on the page/count symbols (e.g. use
page.checked_mul(count).ok_or(...) to return an error or use
page.saturating_mul(count) to cap the value) and handle the overflow case
appropriately (return an error or clamp) so offset is never produced by a
wrapped u32; update the code that uses offset to expect the error or clamped
value as needed.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 414a54e3-42ef-48b6-ac60-cbc119f5a8e5

📥 Commits

Reviewing files that changed from the base of the PR and between 44a7471 and 304dd43.

⛔ Files ignored due to path filters (1)
  • Cargo.lock is excluded by !**/*.lock
📒 Files selected for processing (5)
  • Cargo.toml
  • src/engines/mod.rs
  • src/engines/qwant.rs
  • src/models/engine.rs
  • websurfx/config.lua

Comment thread src/engines/qwant.rs
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (1)
src/engines/qwant.rs (1)

158-162: Refactor HeaderMap construction to avoid unnecessary HashMap allocation in per-request hot path.

Building a temporary HashMap<String, String> via TryFrom adds allocation and runtime parsing overhead that repeats for every request. Construct HeaderMap directly using the typed constants and HeaderValue::from_str() for dynamic headers instead.

♻️ Suggested refactor
-use reqwest::{Client, header::HeaderMap};
+use reqwest::{
+    Client,
+    header::{HeaderMap, HeaderValue, REFERER, USER_AGENT},
+};
-use std::collections::HashMap;
@@
-        let header_map = HeaderMap::try_from(&HashMap::from([
-            ("User-Agent".to_string(), user_agent.to_string()),
-            ("Referer".to_string(), "https://www.qwant.com/".to_string()),
-        ]))
-        .change_context(EngineError::UnexpectedError)?;
+        let mut header_map = HeaderMap::new();
+        header_map.insert(
+            USER_AGENT,
+            HeaderValue::from_str(user_agent).change_context(EngineError::UnexpectedError)?,
+        );
+        header_map.insert(REFERER, HeaderValue::from_static("https://www.qwant.com/"));
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/engines/qwant.rs` around lines 158 - 162, The HeaderMap is being built
via HeaderMap::try_from(&HashMap::from(...)) which allocates a temporary HashMap
per request; replace this by constructing a HeaderMap directly (the variable
header_map) and insert headers using typed header names and
HeaderValue::from_str(user_agent) for the dynamic user_agent and
HeaderValue::from_static("https://www.qwant.com/") (or HeaderValue::from_str if
preferred), propagating any HeaderValue::from_str errors into
EngineError::UnexpectedError just like the current change_context; update the
code around HeaderMap::try_from, HashMap::from, and user_agent usage to avoid
the temporary allocation.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@src/engines/qwant.rs`:
- Around line 158-162: The HeaderMap is being built via
HeaderMap::try_from(&HashMap::from(...)) which allocates a temporary HashMap per
request; replace this by constructing a HeaderMap directly (the variable
header_map) and insert headers using typed header names and
HeaderValue::from_str(user_agent) for the dynamic user_agent and
HeaderValue::from_static("https://www.qwant.com/") (or HeaderValue::from_str if
preferred), propagating any HeaderValue::from_str errors into
EngineError::UnexpectedError just like the current change_context; update the
code around HeaderMap::try_from, HashMap::from, and user_agent usage to avoid
the temporary allocation.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 1aac47fd-ec20-4c46-995f-30b257496ac0

📥 Commits

Reviewing files that changed from the base of the PR and between 304dd43 and a8b9fa2.

📒 Files selected for processing (1)
  • src/engines/qwant.rs

@neon-mmd neon-mmd merged commit e203267 into rolling Apr 16, 2026
11 checks passed
@neon-mmd neon-mmd deleted the FEAT/317_qwant-for-the-search-engine branch April 16, 2026 09:30
@github-project-automation github-project-automation Bot moved this from Under Review to Done in Task list for v2.0.0 release cycle Apr 16, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Development

Successfully merging this pull request may close these issues.

Qwant for the search engine

1 participant