xz-search
External search abstraction — multi-engine aggregation + content extraction.
Overview
xz-search provides a unified async interface over multiple web search backends (Tavily, SerpAPI) and content extraction services (Jina). It handles engine routing, result merging, deduplication, caching, rate limiting, query rewriting, and feedback collection — all behind a single SearchRouter.
Features
- Multi-engine aggregation — query multiple search backends in parallel, merge and rank results
- Content extraction — fetch and extract clean markdown from result URLs (Jina Reader)
- Result caching — in-memory cache with TTL and LRU eviction
- Deduplication — exact URL dedup + near-duplicate detection via MinHash/LSH
- Rate limiting — token-bucket rate limiter per engine (QPS + daily quota)
- Batch search — concurrent bulk query processing with semaphore-controlled parallelism
- Query rewriting — heuristic keyword extraction, multi-perspective expansion, decomposition
- Feedback collection — record user clicks/irrelevance to influence future ranking
Feature flags
| Flag | Enables |
|---|---|
tavily |
TavilyEngine backend |
serpapi |
SerpApiEngine backend |
jina |
JinaExtractor |
All three pull in reqwest as a dependency. By default, no flags are enabled.
Quick start
use *;
async
If you omit engines (empty vec), the router uses all registered engines. Register real backends with feature flags:
[]
= { = ["tavily", "jina"] }
use *;
async
Core API
SearchRouter
The central orchestrator. Register engines, attach an extractor, configure caching and dedup:
let router = new
.with_timeout
.with_dedup
.with_cache;
Primary method: router.aggregated_search(query, &config, &options).
SearchConfig
Data-plane parameters (dead simple — Default works out of the box):
| Field | Type | Default |
|---|---|---|
max_results |
usize |
10 |
max_tokens |
usize |
1024 |
engines |
Vec<String> |
[] (all) |
sources |
Vec<String> |
["web"] |
region |
Option<String> |
None |
time_range |
Option<TimeRange> |
None |
enable_cache |
bool |
true |
auto_extract |
bool |
false |
safe_search |
Option<SafeSearchLevel> |
Moderate |
offset |
usize |
0 |
SearchResult
| Field | Type |
|---|---|
query |
String |
items |
Vec<SearchItem> |
total_results |
u64 |
latency_ms |
u64 |
cached |
bool |
engines_used |
Vec<String> |
rewritten_query |
Option<String> |
SearchItem
| Field | Type |
|---|---|
title |
String |
url |
String |
snippet |
String |
source |
String |
published_at |
Option<u64> |
score |
f32 |
domain |
String |
extracted_content |
Option<ExtractedContent> |
Content extraction
Attach a JinaExtractor (requires jina feature) and set auto_extract: true:
let router = new
.with_extractor;
Or use the extractor directly:
let extractor = new;
let content = extractor.extract.await?;
println!;
let batch = extractor.extract_batch.await?;
Batch search
Process many queries concurrently with batch_search_with_arc:
let router = new;
let results = batch_search_with_arc.await;
Caching
let cache = new;
let router = new.with_cache;
Cache stats via trait CacheStats — hits, misses, size_bytes, entry_count.
Rate limiting
Gate engine calls with a token-bucket limiter:
let limiter = new; // 5 QPS, 1000/day
limiter.acquire.await?;
Query rewriting
Heuristic rewriting without LLM dependency:
let rewriter = new;
let rewritten = rewriter
.rewrite_with_template
.await?;
// → ["best practices rust async"]
Feedback
let feedback = new;
feedback.record_click.await;
feedback.record_irrelevant.await;
let weight = feedback.get_url_weight.await;
Error handling
All operations return Result<_, SearchError>:
| Variant | Description |
|---|---|
Api |
Backend API error |
Network |
Transport failure |
RateLimit |
Rate limit hit |
Extraction |
Content extraction failure |
Config |
Invalid configuration |
Auth |
Authentication failure |
AllEnginesFailed |
No engine returned results |
Timeout |
Request exceeded deadline |
EngineUnavailable |
Engine not reachable |
SearchError::is_retryable() returns true for Network, RateLimit, and Timeout.
License
Licensed under either of
- Apache License, Version 2.0 (LICENSE-APACHE or http://www.apache.org/licenses/LICENSE-2.0)
- MIT license (LICENSE-MIT or http://opensource.org/licenses/MIT)
at your option.