webfind 0.5.0

Self-hosted native search engine for AI agents — pure CLI, zero cost
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
# WebFind Enhancement Catalog

> **Purpose:** Comprehensive catalog of proven, production-ready alternatives that enhance every layer of the WebFind stack — from crawling to ranking to query understanding.
> **Date:** 2026-08-08
> **Scope:** Each enhancement is evaluated against the current implementation with migration effort, impact, and risk assessment.

---

## Executive Summary

| Layer | Current | Recommended Enhancement | Impact | Effort |
|-------|---------|------------------------|--------|--------|
| **Embedding** | all-MiniLM-L6-v2 (384d) | bge-small-en-v1.5 (384d) or nomic-embed-text-v2 (768d) | +5-10% retrieval@10 | Low |
| **Content Extraction** | readability-rust + html2text | rs-trafilatura or readex | +15-20% F1 score | Medium |
| **Query Spelling** | None | symspell_rs | Handles 10-15% misspelled queries | Low |
| **Vector Search** | Brute-force DashMap | diskann-rs (disk-backed) or Turso native DiskANN | 6-10× lower RAM, 3-30× faster | Medium |
| **Reranking** | None (RRF only) | lunaris-rerank (bge-reranker-v2-m3) | +5-15% MRR | Medium |
| **Deduplication** | SimHash only | txtfp (MinHash + LSH + SimHash) | Better near-dup detection | Low |
| **Query Intent** | None | rust_memex::query or rule-based classifier | Route queries to optimal strategy | Low |
| **FTS Engine** | Tantivy (external) | Turso native FTS or SeekStorm | Unified storage + search | High |

---

## 1. Embedding Model Upgrade

### Current State

WebFind uses `fastembed` with `all-MiniLM-L6-v2` (22.7M params, 384-dim, 256-token context). This is the default for many RAG systems but is no longer state-of-the-art.

### Options

| Model | Params | Dim | Context | CPU Speed | Retrieval@10 | License | Best For |
|-------|--------|-----|---------|-----------|-------------|---------|----------|
| **all-MiniLM-L6-v2** (current) | 22.7M | 384 | 256 | 2,849 eps | ~82% | Apache-2.0 | Throughput-first |
| **bge-small-en-v1.5** | 33M | 384 | 512 | ~2,500 eps | ~88% | MIT | Best quality/size ratio |
| **nomic-embed-text-v2** | 475M (305M active) | 768 | 8192 | 580 chunks/s | 88% | Apache-2.0 | CPU-only, multilingual |
| **jina-embeddings-v3** | 570M | 1024 (→256 Matryoshka) | 8192 | 220 chunks/s | **92%** | CC-BY-NC 4.0 | Best overall accuracy |
| **bge-m3** | 568M | 1024 | 8192 | ~200 chunks/s | ~89% | MIT | Dense + sparse + ColBERT |
| **Qwen3-Embedding-0.6B** | 0.6B | 1024 (→32 MRL) | 32768 | ~150 chunks/s | **93%** | Apache-2.0 | Best quality-per-VRAM |

### Recommendation

**Tier 1 (Drop-in replacement):** `bge-small-en-v1.5`
- Same 384 dimensions → no index rebuild needed
- 33M params (vs 22.7M) → negligible speed difference on CPU
- +5-6% retrieval@10 over MiniLM
- MIT license (commercial-safe)
- Already supported by `fastembed` crate: `EmbeddingModel::BGESmallENV15`

**Tier 2 (Best quality):** `nomic-embed-text-v2`
- 768 dimensions → requires index rebuild
- 580 chunks/s on CPU (5× faster than 1024-dim alternatives)
- 88% retrieval@10, multilingual (100+ languages)
- Apache-2.0 license
- Mixture-of-experts: activates only 305M of 475M params per token

**Tier 3 (Maximum accuracy):** `jina-embeddings-v3`
- 1024 dimensions with Matryoshka truncation to 256/512/768
- 92% retrieval@10 (best in class)
- 89-language support
- **Warning:** CC-BY-NC 4.0 license — not commercial-safe without paid license

### Migration Path

```rust
// Current (fastembed)
let model = TextEmbedding::try_new(
    TextInitOptions::new(EmbeddingModel::AllMiniLML6V2)
)?;

// Tier 1: Drop-in replacement
let model = TextEmbedding::try_new(
    TextInitOptions::new(EmbeddingModel::BGESmallENV15)
)?;

// Tier 2: Best CPU performance
let model = TextEmbedding::try_new(
    TextInitOptions::new(EmbeddingModel::NomicEmbedTextV2)
)?;
```

**Source:** [PromptQuorum benchmark](https://www.promptquorum.com/power-local-llm/best-embedding-models-local-rag-2026), [rust-embedding-bench](https://github.com/jerrythomas/rust-embedding-bench), [RunLocalAI embeddings](https://www.runlocalai.co/embeddings)

---

## 2. Content Extraction Upgrade

### Current State

WebFind uses `readability-rust` + `html2text` for content extraction. This is a basic approach that handles simple articles well but struggles with forums, product pages, listings, and documentation.

### Options

| Crate | Algorithms | F1 Score | Speed | Page Types | Metadata | License |
|-------|-----------|----------|-------|------------|----------|---------|
| **readability-rust** (current) | Mozilla Readability | ~0.80 | Fast | Articles only | Basic | MIT |
| **rs-trafilatura** | Trafilatura (ML) | **0.966** | 46 files/s | 7 types | Rich | MIT |
| **readex** | Readability + Trafilatura + htmldate | **0.932** | Fast | All types | ~15 fields | Apache-2.0 |

### Recommendation

**Primary: `rs-trafilatura`**
- F1 = 0.966 on ScrapingHub benchmark (vs ~0.80 for readability-rust)
- ML page-type classification (article, forum, product, collection, listing, documentation, service)
- Per-type extraction profiles (12 forum platforms, 4 documentation frameworks)
- Extraction quality predictor (0.0-1.0 confidence) — pages below 0.80 can trigger LLM fallback
- Native `spider-rs` integration (WebFind already uses `spider` crate)
- GitHub: [Murrough-Foley/rs-trafilatura]https://github.com/Murrough-Foley/rs-trafilatura

**Alternative: `readex`**
- Combines Mozilla Readability + Trafilatura cascade + htmldate
- ~15 metadata fields (title, byline, language, dates, categories, tags, image, license, hostname)
- Differential parity testing against Python Trafilatura on every release
- Apache-2.0 license
- GitHub: [0x4D44/readex]https://github.com/0x4D44/readex

### Migration Path

```toml
# Cargo.toml
[dependencies]
rs-trafilatura = { version = "0.2", features = ["spider"] }
```

```rust
// Replace readability-rust + html2text with:
use rs_trafilatura::spider_integration::extract_page;

// In the crawler pipeline:
if let Ok(result) = extract_page(page) {
    content.title = result.metadata.title.unwrap_or_default();
    content.content_text = result.content_text;
    content.content_markdown = result.content_markdown;
    content.author = result.metadata.author;
    content.published_at = result.metadata.date;
    // Use extraction_quality for confidence threshold
    if result.extraction_quality < 0.80 {
        // Trigger LLM fallback or flag for review
    }
}
```

**Source:** [rs-trafilatura GitHub](https://github.com/Murrough-Foley/rs-trafilatura), [readex docs.rs](https://docs.rs/readex/latest/readex/), [contextractor](https://github.com/contextractor/contextractor)

---

## 3. Query Spelling Correction

### Current State

WebFind has no spelling correction. 10-15% of web search queries contain misspelled terms, leading to zero results or irrelevant matches.

### Recommendation: `symspell_rs`

- **1 million times faster** than Norvig's algorithm
- Symmetric Delete algorithm: only deletes needed (no transposes/replaces/inserts)
- Compound-aware multi-word correction
- Word segmentation for noisy text
- Language independent
- 0.033ms per word (edit distance 2) on single core
- MIT license
- GitHub: [wolfgarbe/symspell_rs]https://github.com/wolfgarbe/symspell_rs

### Integration

```toml
[dependencies]
symspell_rs = "6.8"
```

```rust
use symspell_rs::{SymSpell, Verbosity};

struct QueryCorrector {
    symspell: SymSpell,
}

impl QueryCorrector {
    fn new() -> Self {
        let mut symspell = SymSpell::new(2, 7, 1);
        // Load from WebFind's indexed terms (build dictionary from Tantivy/Turso terms)
        symspell.load_dictionary("terms.txt", 0, 1, " ");
        Self { symspell }
    }

    fn correct(&self, query: &str) -> String {
        let suggestions = self.symspell.lookup_compound(query, 2);
        suggestions.first().map(|s| s.term.clone()).unwrap_or_else(|| query.to_string())
    }

    // For autocomplete: return top-N suggestions per prefix
    fn suggest(&self, prefix: &str, count: usize) -> Vec<String> {
        self.symspell.lookup(prefix, Verbosity::Top, 2)
            .into_iter()
            .take(count)
            .map(|s| s.term)
            .collect()
    }
}
```

**Source:** [symspell_rs docs.rs](https://docs.rs/symspell_rs/latest/), [SeekStorm blog](https://seekstorm.com/blog/sub-millisecond-compound-aware-automatic-spelling-correction/)

---

## 4. Vector Search Engine Upgrade

### Current State

WebFind uses a brute-force `DashMap<String, Vec<f32>>` with cosine similarity scan. This is O(n) per query and requires all vectors in RAM. The `hnsw_rs` dependency exists but is unused.

### Options

| Approach | Latency | Memory | Scale | Build | License | Notes |
|----------|---------|--------|-------|-------|---------|-------|
| **DashMap brute-force** (current) | O(n) scan | All in RAM | <100K | None | - | Simple, correct |
| **hnsw_rs** (already dep) | Sub-ms | All in RAM | <1M | Medium | MIT | In-memory HNSW |
| **diskann-rs** | 55µs | **6-10× lower** | 10M-1B+ | 1.6× slower | MIT | Disk-backed, incremental |
| **rust-diskann** | ~100µs | Low | 1M+ | Medium | MIT | Pure Rust DiskANN |
| **Turso native DiskANN** | <15ms | In-file | 1M+ | Automatic | - | Via libsql_vector_idx |

### Recommendation

**For <100K vectors:** Keep brute-force or use `hnsw_rs` (already a dependency)
- Brute-force is correct and fast enough at this scale
- `hnsw_rs` gives sub-ms latency with minimal code change

**For 100K-10M vectors:** `diskann-rs`
- 6-10× lower memory than HNSW (disk-backed with mmap)
- Incremental updates (add/delete without rebuild)
- SIMD acceleration (AVX2, SSE4.1, NEON)
- Product Quantization (64× compression)
- Filtered search with metadata predicates
- GitHub: [diskann-rs]https://crates.io/crates/diskann-rs

**For 10M+ vectors:** Turso native DiskANN
- Automatic index maintenance
- No separate vector store process
- Query via `vector_top_k()` SQL function

### Migration Path (diskann-rs)

```toml
[dependencies]
diskann-rs = "0.1"
anndists = "0.1"
```

```rust
use diskann_rs::{DiskAnnIndex, IndexBuilder};
use anndists::dist::DistL2;

struct VectorIndex {
    index: DiskAnnIndex<f32, DistL2>,
}

impl VectorIndex {
    fn new(dimension: usize, index_path: &Path) -> Self {
        let index = IndexBuilder::new(dimension, DistL2)
            .with_max_degree(64)
            .with_search_list_size(128)
            .build(index_path)
            .expect("build DiskANN index");
        Self { index }
    }

    fn insert(&mut self, id: u32, vector: &[f32]) {
        self.index.add_vector(id, vector).expect("add vector");
    }

    fn search(&self, query: &[f32], k: usize) -> Vec<(u32, f32)> {
        self.index.search(query, k).expect("search")
    }
}
```

**Source:** [diskann-rs crates.io](https://crates.io/crates/diskann-rs), [DiskANN vs HNSW benchmark](https://github.com/JohnDouglasJDX/diskann-vs-hnsw-benchmark), [ann-search-rs benchmarks](https://github.com/GregorLueg/ann-search-rs)

---

## 5. Cross-Encoder Reranking

### Current State

WebFind uses Reciprocal Rank Fusion (RRF) to combine BM25 and vector scores. No learned reranking stage exists.

### Options

| Crate | Model | Latency | Size | License | Status |
|-------|-------|---------|------|---------|--------|
| **lunaris-rerank** | bge-reranker-v2-m3 | 12ms p50 (CPU) | 1.1GB | MIT | Production |
| **lunaris-rerank-native** | bge-reranker-v2-m3 (candle) | 40-80ms (CPU) | 2.3GB | MIT | Production |
| **rerank-rs** | ms-marco-MiniLM-L6-v2 | ~12ms | 90MB | MIT | Production |
| **axil-rerank** | answerai-colbert-small-v1 | ~80ms | 33M params | MIT | Opt-in only |

### Recommendation

**Primary: `lunaris-rerank` with `fastembed` backend**
- BGE-Reranker-v2-m3 (568M params, XLM-RoBERTa architecture)
- 12ms p50 latency on CPU (batch of 30 candidates)
- Multilingual (89 languages)
- Apache-2.0 license
- NoopReranker fallback when model unavailable
- GitHub: [lunaris-rerank]https://docs.rs/lunaris-rerank/latest/

**Lightweight alternative: `rerank-rs`**
- ms-marco-MiniLM-L6-v2 (22M params)
- ~12ms per pair on CPU
- Much smaller model (90MB vs 1.1GB)
- Good for latency-sensitive deployments
- GitHub: [bhk97/rerank-rs]https://github.com/bhk97/rerank-rs

### Integration

```rust
use lunaris_rerank::{Reranker, BgeRerankerV2M3, NoopReranker};

struct SearchPipeline {
    reranker: Box<dyn Reranker>,
}

impl SearchPipeline {
    fn new() -> Self {
        // Try to load BGE reranker, fall back to no-op
        let reranker: Box<dyn Reranker> = match BgeRerankerV2M3::try_new_from_default_cache() {
            Ok(r) => Box::new(r),
            Err(_) => {
                tracing::warn!("reranker model unavailable, using no-op");
                Box::new(NoopReranker)
            }
        };
        Self { reranker }
    }

    async fn search(&self, query: &str, candidates: Vec<SearchResult>) -> Vec<SearchResult> {
        // 1. Get top-30 candidates via RRF (BM25 + vector)
        let rrf_results = self.rrf_search(query, 30).await;

        // 2. Rerank with cross-encoder
        let docs: Vec<String> = rrf_results.iter()
            .map(|r| r.snippet.clone())
            .collect();
        let reranked = self.reranker.rerank(query, docs).await?;

        // 3. Return top-k (typically 5-10)
        reranked.into_iter()
            .zip(rrf_results)
            .map(|(score, mut result)| {
                result.scores.rerank = Some(score);
                result
            })
            .take(10)
            .collect()
    }
}
```

**Source:** [lunaris-rerank docs.rs](https://docs.rs/lunaris-rerank/latest/), [rerank-rs GitHub](https://github.com/bhk97/rerank-rs), [Ralph Hero reranker research](https://github.com/cdubiel08/ralph-hero/blob/main/thoughts/shared/research/2026-04-26-GH-0901-local-cross-encoder-reranker-m5-pro.md)

---

## 6. Deduplication & Near-Duplicate Detection

### Current State

WebFind uses SimHash for near-duplicate detection. This is a single-algorithm approach that misses many near-duplicate cases.

### Options

| Crate | Algorithms | Speed | Use Case | License |
|-------|-----------|-------|----------|---------|
| **txtfp** | MinHash + LSH + SimHash + TLSH + Embeddings | 9K docs/s (MinHash) | Full dedup pipeline | MIT |
| **gaoya** | MinHash + SimHash | Fast | Dedup + clustering | MIT |
| **sketchir** | MinHash + SimHash + LSH | Fast | Near-dup detection | MIT |

### Recommendation: `txtfp`

- **MinHash + LSH:** Sub-linear near-duplicate lookup (O(1) avg query)
- **SimHash:** Bit-LSH near-duplicate detection (Hamming distance)
- **TLSH:** Byte-level locality-sensitive hash
- **Semantic embeddings:** ONNX local embedding similarity
- **Unicode-correct canonicalization:** NFKC + casefold + Bidi strip (defends against Trojan Source)
- **Streaming + offline fingerprinters:** Both whole-doc and chunk-fed variants
- **Byte-stable hash layouts:** `repr(C)` `bytemuck::Pod`, semver-frozen
- GitHub: [themankindproject/txtfp]https://github.com/themankindproject/txtfp

### Integration

```toml
[dependencies]
txtfp = "0.3"  # minhash + simhash + lsh (default features)
```

```rust
use txtfp::{
    Canonicalizer, Fingerprinter, MinHashFingerprinter, ShingleTokenizer,
    WordTokenizer, jaccard,
};

struct DeduplicationIndex {
    canonicalizer: Canonicalizer,
    fingerprinter: MinHashFingerprinter<ShingleTokenizer<WordTokenizer>, 128>,
    // LSH index for sub-linear lookup
    lsh_index: txtfp::LshIndex,
}

impl DeduplicationIndex {
    fn new() -> Self {
        let canonicalizer = Canonicalizer::default();
        let tokenizer = ShingleTokenizer { k: 5, inner: WordTokenizer };
        let fingerprinter = MinHashFingerprinter::<_, 128>::new(canonicalizer.clone(), tokenizer);
        let lsh_index = txtfp::LshIndexBuilder::for_threshold(0.8).build();
        Self { canonicalizer, fingerprinter, lsh_index }
    }

    /// Check if a document is a near-duplicate of anything already indexed
    fn is_near_duplicate(&self, text: &str) -> Option<(usize, f64)> {
        let canonical = self.canonicalizer.canonicalize(text);
        let signature = self.fingerprinter.fingerprint(&canonical).ok()?;
        let candidates = self.lsh_index.query(&signature);
        candidates.into_iter()
            .map(|(id, ref_sig)| (id, jaccard(&signature, ref_sig)))
            .max_by(|a, b| a.1.partial_cmp(&b.1).unwrap())
            .filter(|(_, sim)| *sim > 0.8)
    }
}
```

**Source:** [txtfp GitHub](https://github.com/themankindproject/txtfp), [gaoya crates.io](https://crates.io/crates/gaoya), [sketchir](https://github.com/arclabs561/sketchir)

---

## 7. Query Intent Detection & Routing

### Current State

WebFind treats all queries identically — BM25 + vector + graph fusion regardless of intent. A query like "when did X happen" (temporal) gets the same treatment as "what is X" (definitional).

### Options

| Approach | Latency | Accuracy | Complexity | License |
|----------|---------|----------|------------|---------|
| **Rule-based heuristics** | <1ms | ~70% | Low | - |
| **rust_memex::query** | <1ms | ~75% | Low | MIT |
| **sqry-nl classifier** | 2-5ms | **99.75%** | Medium | MIT |
| **computer-says-no** | ~5ms | 85-87% | Medium | Apache-2.0 |

### Recommendation

**Tier 1 (Immediate): Rule-based intent detection**
- Zero dependencies, <1ms latency
- Detect: temporal ("when", "latest"), navigational ("official", "homepage"), transactional ("download", "buy")
- Route to appropriate search strategy

**Tier 2 (Enhanced): `sqry-nl` approach**
- Fine-tuned all-MiniLM-L6-v2 for 8 intent classes
- ONNX INT8 quantized (57MB)
- 99.75% accuracy, 2.1ms p50 latency
- 4-tier response: Execute / Confirm / Disambiguate / Reject
- GitHub: [sqry-nl]https://docs.rs/sqry-nl/latest/

### Integration

```rust
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum QueryIntent {
    Factual,      // "what is X", "define Y"
    Temporal,     // "when did X", "latest news about Y"
    Navigational, // "official site of X", "Y homepage"
    Transactional,// "download X", "buy Y"
    Comparative,  // "X vs Y", "difference between"
    Procedural,   // "how to X", "steps for Y"
    Local,        // "X near me", "Y in [city]"
    Hybrid,       // Multiple intents detected
}

pub struct QueryRouter;

impl QueryRouter {
    /// Fast heuristic-based intent detection (<1ms)
    pub fn detect_intent(query: &str) -> QueryIntent {
        let q = query.to_lowercase();
        let words: Vec<&str> = q.split_whitespace().collect();

        // Temporal signals
        if words.iter().any(|w| matches!(*w, "when" | "latest" | "recent" | "today" | "yesterday" | "ago")) {
            return QueryIntent::Temporal;
        }

        // Navigational signals
        if words.iter().any(|w| matches!(*w, "official" | "homepage" | "website" | "url")) {
            return QueryIntent::Navigational;
        }

        // Transactional signals
        if words.iter().any(|w| matches!(*w, "download" | "buy" | "price" | "purchase" | "free")) {
            return QueryIntent::Transactional;
        }

        // Comparative signals
        if q.contains(" vs ") || q.contains(" versus ") || q.contains("difference between") {
            return QueryIntent::Comparative;
        }

        // Procedural signals
        if words.first().map_or(false, |w| *w == "how") || q.contains("steps to") {
            return QueryIntent::Procedural;
        }

        // Default: factual/hybrid
        QueryIntent::Factual
    }

    /// Route query to optimal search strategy
    pub fn route(&self, query: &str) -> SearchStrategy {
        match Self::detect_intent(query) {
            QueryIntent::Temporal => SearchStrategy {
                bm25_weight: 0.3,
                vector_weight: 0.3,
                freshness_weight: 0.4,  // Prioritize recency
                ..Default::default()
            },
            QueryIntent::Navigational => SearchStrategy {
                bm25_weight: 0.6,       // Exact match matters
                vector_weight: 0.1,
                domain_boost: true,     // Boost official domains
                ..Default::default()
            },
            QueryIntent::Factual => SearchStrategy {
                bm25_weight: 0.4,
                vector_weight: 0.4,
                graph_weight: 0.2,
                ..Default::default()
            },
            _ => SearchStrategy::default(),
        }
    }
}
```

**Source:** [rust_memex::query](https://docs.rs/rust-memex/latest/rust_memex/query/), [sqry-nl docs.rs](https://docs.rs/sqry-nl/latest/), [computer-says-no GitHub](https://github.com/srobroek/computer-says-no)

---

## 8. Alternative Full-Text Search Engines

### Current State

WebFind uses Tantivy as the primary BM25 index. This is a separate on-disk index that must be kept in sync with the graph store.

### Options

| Engine | Type | Latency | Scale | Integration | License |
|--------|------|---------|-------|-------------|---------|
| **Tantivy** (current) | Standalone library | <5ms | 100M+ docs | Separate index | Mature |
| **Turso native FTS** | In-process SQL | <5ms | 1M+ docs | Unified with storage | Beta |
| **SeekStorm** | Rust search server | <1ms | 100M+ docs | HTTP API | AGPL/MIT |

### Recommendation

**For Turso migration:** Use Turso's native FTS (Tantivy-powered)
- Same Tantivy engine, but index lives inside the Turso file
- No separate index directory to manage
- Transactional consistency with data
- `fts_match()`, `fts_score()`, `fts_highlight()` functions

**For advanced use cases:** SeekStorm
- Sub-millisecond full-text search
- Multi-tenancy support
- Built-in SymSpell spelling correction
- BM25 + vector + graph in one server
- GitHub: [SeekStorm]https://github.com/SeekStorm/SeekStorm

**Source:** [Turso FTS docs](https://docs.turso.tech/sql-reference/functions/fts), [SeekStorm](https://github.com/SeekStorm/SeekStorm)

---

## 9. Additional Enhancements

### 9.1 Query Expansion with Synonyms

**Problem:** BM25 misses results that use synonyms (e.g., "car" vs "automobile").

**Solution:** Static synonym map with FTS query expansion.

```rust
/// Expand query tokens with synonyms for FTS5/Turso FTS
fn expand_query(query: &str) -> String {
    let synonyms: &[(&[&str], &[&str])] = &[
        (&["buy", "purchase", "bought"], &["buy", "purchase", "bought", "acquire"]),
        (&["movie", "film"], &["movie", "film", "cinema"]),
        (&["doctor", "physician"], &["doctor", "physician", "dr"]),
    ];

    let mut expanded = Vec::new();
    for token in query.split_whitespace() {
        expanded.push(token.to_string());
        for (terms, expansions) in synonyms {
            if terms.contains(&token.to_lowercase().as_str()) {
                for exp in *expansions {
                    if !terms.contains(exp) {
                        expanded.push(exp.to_string());
                    }
                }
            }
        }
    }
    expanded.join(" OR ")
}
```

**Source:** [mag query expansion PR](https://github.com/George-RD/mag/pull/49)

### 9.2 Content Classification

**Problem:** WebFind indexes all content equally. A forum post and a research paper get the same treatment.

**Solution:** Use `rs-trafilatura`'s page-type classification to apply different indexing strategies:

```rust
match result.metadata.page_type {
    PageType::Article => {
        // Full indexing: BM25 + vector + graph
        index_full(content);
    }
    PageType::Forum => {
        // Thread-level indexing, lower priority
        index_with_priority(content, Priority::Low);
    }
    PageType::Product => {
        // Structured extraction: price, rating, specs
        index_structured(content);
    }
    PageType::Documentation => {
        // Section-level chunking for better retrieval
        index_chunked(content, ChunkStrategy::BySection);
    }
    _ => {
        // Default indexing
        index_basic(content);
    }
}
```

### 9.3 Streaming Index Updates

**Problem:** WebFind indexes in batch. New content is not searchable until the next batch commit.

**Solution:** Use Tantivy's `IndexWriter::commit()` with a short interval (e.g., 5 seconds) for near-real-time indexing:

```rust
// In the crawler pipeline:
let mut writer = index.writer(50_000_000)?;
writer.add_document(doc)?;
// Commit every 5 seconds or every 100 documents
if Instant::now() - last_commit > Duration::from_secs(5) {
    writer.commit()?;
    last_commit = Instant::now();
}
```

### 9.4 Query Result Caching

**Problem:** Repeated queries (especially popular ones) re-execute the full search pipeline.

**Solution:** LRU cache for search results with TTL:

```rust
use moka::sync::Cache;

struct SearchCache {
    cache: Cache<String, SearchResponse>,
}

impl SearchCache {
    fn new() -> Self {
        let cache = Cache::builder()
            .max_capacity(10_000)
            .time_to_live(Duration::from_secs(300))  // 5 minute TTL
            .time_to_idle(Duration::from_secs(60))   // 1 minute idle
            .build();
        Self { cache }
    }

    fn get_or_compute<F>(&self, query: &str, f: F) -> SearchResponse
    where F: FnOnce() -> SearchResponse {
        if let Some(cached) = self.cache.get(query) {
            return cached;
        }
        let result = f();
        self.cache.insert(query.to_string(), result.clone());
        result
    }
}
```

### 9.5 Async Embedding with `ort` Directly

**Problem:** `fastembed` is synchronous and blocks the async runtime during embedding.

**Solution:** Use `ort` (ONNX Runtime) directly with `tokio::task::spawn_blocking`:

```rust
use ort::{Environment, Session, Value};
use tokio::task;

async fn embed_batch(texts: &[&str]) -> Result<Vec<Vec<f32>>> {
    let texts = texts.to_vec();
    task::spawn_blocking(move || {
        let session = Session::builder()?
            .with_model_from_file("model.onnx")?;
        // Batch inference
        let outputs = session.run(ort::inputs!["input_ids" => ...])?;
        // Extract embeddings
        Ok(extract_embeddings(outputs))
    }).await?
}
```

**Source:** [rust-embedding-bench](https://github.com/jerrythomas/rust-embedding-bench) — shows `ort` fp32 at 3,052 eps vs fastembed at 2,849 eps on Apple M4 Max.

---

## 10. Prioritized Implementation Roadmap

### Phase 1 — Quick Wins (1-2 weeks each)

| # | Enhancement | Impact | Effort | Dependencies |
|---|------------|--------|--------|--------------|
| 1 | Embedding upgrade to `bge-small-en-v1.5` | +5% retrieval | Low | None (fastembed already supports it) |
| 2 | Query spelling correction with `symspell_rs` | Handles 10-15% queries | Low | Build dictionary from indexed terms |
| 3 | Query intent detection (rule-based) | Better routing | Low | None |
| 4 | Query result caching with `moka` | Faster repeat queries | Low | Add moka dependency |

### Phase 2 — Medium Effort (2-4 weeks each)

| # | Enhancement | Impact | Effort | Dependencies |
|---|------------|--------|--------|--------------|
| 5 | Content extraction upgrade to `rs-trafilatura` | +15% F1 score | Medium | Replace readability-rust |
| 6 | Deduplication with `txtfp` (MinHash + LSH) | Better dedup | Low | Add txtfp dependency |
| 7 | Vector search upgrade to `diskann-rs` | 6-10× lower RAM | Medium | Replace DashMap vector store |
| 8 | Query expansion with synonyms | Better recall | Low | Static synonym map |

### Phase 3 — Advanced (4-8 weeks each)

| # | Enhancement | Impact | Effort | Dependencies |
|---|------------|--------|--------|--------------|
| 9 | Cross-encoder reranking with `lunaris-rerank` | +5-15% MRR | Medium | Model download (1.1GB) |
| 10 | Query intent classifier (ONNX) | 99.75% accuracy | Medium | Train on WebFind query logs |
| 11 | Content classification by page type | Better indexing | Medium | rs-trafilatura integration |
| 12 | Async embedding with `ort` directly | Non-blocking | Medium | ort dependency |

---

## Appendix: Research Sources

### Embedding Models
- [PromptQuorum: Best Local Embedding Models 2026]https://www.promptquorum.com/power-local-llm/best-embedding-models-local-rag-2026 — 6 models tested on 4 document types, 100 queries each
- [rust-embedding-bench]https://github.com/jerrythomas/rust-embedding-bench — Apples-to-apples benchmark: fastembed vs ort vs candle vs ollama vs llama-cpp-2
- [RunLocalAI Embeddings]https://www.runlocalai.co/embeddings — Model comparison matrix with MTEB scores
- [D-Central: Local Embedding Models]https://d-central.tech/local-embedding-models/ — Quality-per-VRAM analysis

### Content Extraction
- [rs-trafilatura GitHub]https://github.com/Murrough-Foley/rs-trafilatura — F1=0.966, 7 page types, spider-rs integration
- [readex docs.rs]https://docs.rs/readex/latest/readex/ — Readability + Trafilatura + htmldate cascade
- [contextractor]https://github.com/contextractor/contextractor — Production deployment of rs-trafilatura

### Vector Search
- [diskann-rs crates.io]https://crates.io/crates/diskann-rs — Disk-backed ANN with incremental updates
- [DiskANN vs HNSW Benchmark]https://github.com/JohnDouglasJDX/diskann-vs-hnsw-benchmark — 3.38M vectors, 3.3× QPS advantage
- [ann-search-rs]https://github.com/GregorLueg/ann-search-rs — 150K samples, all standard algorithms

### Reranking
- [lunaris-rerank docs.rs]https://docs.rs/lunaris-rerank/latest/ — BGE-Reranker-v2-m3, 12ms p50
- [rerank-rs GitHub]https://github.com/bhk97/rerank-rs — Lightweight cross-encoder
- [Ralph Hero reranker research]https://github.com/cdubiel08/ralph-hero/blob/main/thoughts/shared/research/2026-04-26-GH-0901-local-cross-encoder-reranker-m5-pro.md — M5 Pro latency analysis

### Deduplication
- [txtfp GitHub]https://github.com/themankindproject/txtfp — MinHash + LSH + SimHash + TLSH + embeddings
- [gaoya crates.io]https://crates.io/crates/gaoya — MinHash + SimHash, Python bindings
- [sketchir]https://github.com/arclabs561/sketchir — Sketching primitives for IR

### Query Understanding
- [rust_memex::query]https://docs.rs/rust-memex/latest/rust_memex/query/ — Intent detection and routing
- [sqry-nl docs.rs]https://docs.rs/sqry-nl/latest/ — NL to query translation, 99.75% accuracy
- [computer-says-no GitHub]https://github.com/srobroek/computer-says-no — ~5ms intent classification

### Spelling Correction
- [symspell_rs docs.rs]https://docs.rs/symspell_rs/latest/ — 1M× faster than Norvig
- [SeekStorm blog]https://seekstorm.com/blog/sub-millisecond-compound-aware-automatic-spelling-correction/ — SymSpell integration guide

---

*End of Enhancement Catalog*