# whoosh-ng : Full Technical Documentation



## DOCUMENT: Analysis

# Analysis API

Classes and functions for turning text into indexable "tokens" (usually words).
Analysis is the first step in the indexing pipeline: an analyzer tokenizes text
and applies zero or more filters to the resulting token stream.

## Overview

Three general categories of objects make up the analysis pipeline:

- **Tokenizers** split text into individual tokens (words, n-grams, identifiers).
  Every tokenizer is callable: `tokenizer(text) -> iterator of Token objects`.
- **Filters** transform one token stream into another. Common operations include
  lowercasing, stop-word removal, stemming, and synonym expansion. Every filter
  is callable: `filter(token_generator) -> token_generator`.
- **Analyzers** compose a tokenizer and zero or more filters into a single unit.
  Every analyzer is callable and can be used directly as a field's `analyzer`
  argument.

Tokenizers and filters are combined using the `|` operator:

```python
my_analyzer = RegexTokenizer() | LowercaseFilter() | StopFilter()
```

The first item must be a tokenizer; subsequent items must be filters.

## Composition

### Composable

```python
class whoosh.analysis.Composable
```

Base class for tokenizers and filters, providing `|` composition.

**Attributes:**
- `is_morph (bool)`: Whether this object performs morphological transformation
  (e.g. stemming). Defaults to `False`.

**Methods:**

#### `__or__(self, other)`

Combines this object with `other` using `CompositeAnalyzer`.

```python
analyzer = RegexTokenizer() | LowercaseFilter() | StopFilter()
```

### CompositeAnalyzer

```python
class whoosh.analysis.CompositeAnalyzer
```

Composed analyzer created by chaining a tokenizer and filters with `|`.

**Example:**
```python
from whoosh.analysis import RegexTokenizer, LowercaseFilter, StopFilter

analyzer = RegexTokenizer() | LowercaseFilter() | StopFilter()
tokens = list(analyzer("Hello world, this is a test"))
```

## Token

```python
class whoosh.analysis.Token
```

Represents a single token (usually a word) extracted from source text.
Tokenizers yield the **same** `Token` object repeatedly (for performance), so
consumers must not hold references between iterations.

**Slots:**

| Attribute | Type | Description |
|-----------|------|-------------|
| `text` | `str` | The text of this token |
| `pos` | `int` | Token position (if `positions=True`) |
| `startchar` | `int` | Start character offset (if `chars=True`) |
| `endchar` | `int` | End character offset (if `chars=True`) |
| `original` | `str` | Original text before filters (if `keeporiginal=True`) |
| `positions` | `bool` | Whether position info was requested |
| `chars` | `bool` | Whether character offsets were requested |
| `stopped` | `bool` | Set by `StopFilter` |
| `boost` | `float` | Token boost factor (default `1.0`) |
| `removestops` | `bool` | Whether stop words should be removed |
| `mode` | `str` | `'index'` or `'query'` |
| `boosts` | `dict` | Per-position boost values (if requested) |
| `tokenize` | `bool` | Whether tokenization should proceed |
| `matched` | `bool` | Used during highlighting |
| `fieldname` | `str` | Field name for this token |

**Methods:**

#### `copy()`

Returns a new `Token` with the same attribute values. Use this if you need to
retain a token between iterations.

```python
def remove_duplicates(stream):
    last = None
    for t in stream:
        if last != t.text:
            yield t
        last = t.text
```

## Utility Functions

### entoken

```python
whoosh.analysis.entoken(
    textstream,
    positions=False,
    chars=False,
    start_pos=0,
    start_char=0,
    **kwargs
) -> Iterator[Token]
```

Converts a sequence of strings into a stream of `Token` objects.

### unstopped

```python
whoosh.analysis.unstopped(tokenstream) -> Iterator[Token]
```

Removes tokens where `token.stopped` is `True`.

## Analyzers

### Analyzer (Base)

```python
class whoosh.analysis.Analyzer
```

Abstract base class for all analyzers. Subclasses implement `__call__`.

### CompositeAnalyzer

Created automatically when you use `|` to compose tokenizers and filters.

### Predefined Analyzers

#### IDAnalyzer

```python
whoosh.analysis.IDAnalyzer(lowercase=False) -> Analyzer
```

Yields the entire input as a single token. Deprecated; use `IDTokenizer` directly.

- `lowercase (bool)`: If True, add a `LowercaseFilter`.

#### KeywordAnalyzer

```python
whoosh.analysis.KeywordAnalyzer(
    lowercase=False,
    commas=False
) -> Analyzer
```

Splits on whitespace or commas. Suitable for field values that are lists of
keywords.

- `lowercase (bool)`: Lowercase each token.
- `commas (bool)`: Split on commas instead of whitespace.

**Example:**
```python
from whoosh.analysis import KeywordAnalyzer

an = KeywordAnalyzer(lowercase=True, commas=True)
list(an("Hello, WORLD, test"))
# => ["hello", "world", "test"]
```

#### RegexAnalyzer

```python
whoosh.analysis.RegexAnalyzer(
    expression=r"\w+(\.?\w+)*",
    gaps=False
) -> Analyzer
```

Deprecated; use `RegexTokenizer` directly.

#### SimpleAnalyzer

```python
whoosh.analysis.SimpleAnalyzer(
    expression=default_pattern,
    gaps=False
) -> Analyzer
```

Composes `RegexTokenizer` with `LowercaseFilter`.

- `expression`: Regex pattern for tokens.
- `gaps`: If True, split on the expression instead of matching it.

**Example:**
```python
an = SimpleAnalyzer()
list(an("Hello there, this is a TEST"))
# => ["hello", "there", "this", "is", "a", "test"]
```

#### StandardAnalyzer

```python
whoosh.analysis.StandardAnalyzer(
    expression=default_pattern,
    stoplist=STOP_WORDS,
    minsize=2,
    maxsize=None,
    gaps=False
) -> Analyzer
```

Composes `RegexTokenizer`, `LowercaseFilter`, and optional `StopFilter`.

- `expression`: Regex pattern for tokens.
- `stoplist`: Words to remove (set to `None` to disable).
- `minsize`: Minimum token length (default `2`).
- `maxsize`: Maximum token length (default `None`, no limit).
- `gaps`: If True, split on the expression instead of matching it.

**Example:**
```python
an = StandardAnalyzer()
list(an("Testing is testing and testing"))
# => ["testing", "testing", "testing"]
```

#### StemmingAnalyzer

```python
whoosh.analysis.StemmingAnalyzer(
    expression=default_pattern,
    stoplist=STOP_WORDS,
    minsize=2,
    maxsize=None,
    gaps=False,
    stemfn=stem,
    ignore=None,
    cachesize=50000
) -> Analyzer
```

Composes `RegexTokenizer`, `LowercaseFilter`, optional `StopFilter`, and
`StemFilter`.

- `expression`: Regex pattern for tokens.
- `stoplist`: Words to remove (set to `None` to disable).
- `minsize`: Minimum token length (default `2`).
- `maxsize`: Maximum token length.
- `gaps`: If True, split on the expression instead of matching it.
- `stemfn`: Stemming function (default: Porter stemmer for English).
- `ignore`: Words to not stem (set).
- `cachesize`: Stem cache size (default `50000`). Use `-1` for unbounded,
  `None` for no cache.

**Example:**
```python
an = StemmingAnalyzer()
list(an("Testing is testing and testing"))
# => ["test", "test", "test"]
```

#### FancyAnalyzer

```python
whoosh.analysis.FancyAnalyzer(
    expression=r"\s+",
    stoplist=STOP_WORDS,
    minsize=2,
    gaps=True,
    splitwords=True,
    splitnums=True,
    mergewords=False,
    mergenums=False
) -> Analyzer
```

Composes `RegexTokenizer`, `IntraWordFilter`, `LowercaseFilter`, and `StopFilter`.
Splits on whitespace and breaks compound words into subwords.

**Example:**
```python
an = FancyAnalyzer()
list(an("Should I call getInt or get_real?"))
# => ["should", "call", "get", "int", "get", "real"]
```

#### LanguageAnalyzer

```python
whoosh.analysis.LanguageAnalyzer(
    lang,
    expression=default_pattern,
    gaps=False,
    cachesize=50000
) -> Analyzer
```

Configures a language-specific analyzer with `LowercaseFilter`, `StopFilter`,
and `StemFilter`.

- `lang`: Language code (e.g., `"en"`, `"es"`, `"fr"`).
- `expression`: Regex pattern for tokens.
- `gaps`: If True, split on the expression instead of matching it.
- `cachesize`: Stem cache size.

Available languages: `ar`, `da`, `nl`, `en`, `fi`, `fr`, `de`, `hu`, `it`,
`no`, `pt`, `ro`, `ru`, `es`, `sv`, `tr`.

See `whoosh.lang` for `has_stemmer()` and `has_stopwords()` helper functions.

## Tokenizers

All tokenizers inherit from `Tokenizer`.

### Tokenizer

```python
class whoosh.analysis.Tokenizer
```

Base class for tokenizers. Each tokenizer is callable and yields `Token`
objects.

### RegexTokenizer

```python
class whoosh.analysis.RegexTokenizer(
    expression=default_pattern,
    gaps=False
)
```

Uses a regular expression to extract tokens from text. Each match of the
expression equals one token; group 0 (the entire match) is used as the text.

- `expression`: Compiled regex or pattern string.
- `gaps`: If True, split on the expression rather than matching it.

**Example:**
```python
from whoosh.analysis import RegexTokenizer

rext = RegexTokenizer()
list(rext("hi there 3.141 big-time under_score"))
# => ["hi", "there", "3.141", "big", "time", "under_score"]
```

### IDTokenizer

```python
class whoosh.analysis.IDTokenizer
```

Yields the entire input string as a single token. Used for indexed but
untokenized fields (e.g., document paths).

### CharsetTokenizer

```python
class whoosh.analysis.CharsetTokenizer(charmap)
```

Tokenizes and translates text according to a character mapping dictionary.
Characters that map to `None` are treated as token break characters.

- `charmap`: Mapping from integer character codes to unicode characters
  (as used by `unicode.translate()`).

### PathTokenizer

```python
class whoosh.analysis.PathTokenizer(expression="[^/]+")
```

Tokenizes path strings into hierarchical prefixes. Given `"/a/b/c"`, yields
`["/a", "/a/b", "/a/b/c"]`.

### NgramTokenizer

```python
class whoosh.analysis.NgramTokenizer(minsize, maxsize=None)
```

Splits input text into N-grams instead of words. Unlike `RegexTokenizer`, this
tokenizer does not use a regex, so grams may include whitespace and punctuation.

- `minsize`: Minimum N-gram size.
- `maxsize`: Maximum N-gram size (defaults to `minsize`).

**Example:**
```python
from whoosh.analysis import NgramTokenizer

ngt = NgramTokenizer(4)
list(ngt("hi there"))
# => ["hi t", "i th", " the", "ther", "here"]
```

### CachedRegexTokenizer

```python
class whoosh.analysis.CachedRegexTokenizer(
    expression=default_pattern,
    gaps=False,
    maxsize=8192
)
```

A `RegexTokenizer` wrapper that caches tokenization results for repeated
strings, trading memory for speed.

- `expression`: Regex pattern.
- `gaps`: If True, split on the expression.
- `maxsize`: Maximum cache size (LRU eviction when exceeded).

### SpaceSeparatedTokenizer

```python
whoosh.analysis.SpaceSeparatedTokenizer() -> RegexTokenizer
```

Returns a `RegexTokenizer` that splits on whitespace.

### CommaSeparatedTokenizer

```python
whoosh.analysis.CommaSeparatedTokenizer() -> CompositeAnalyzer
```

Returns a composed analyzer that splits on commas and strips whitespace.

## Filters

All filters inherit from `Filter`.

### Filter

```python
class whoosh.analysis.Filter
```

Base class for filters. Subclasses implement `__call__(self, tokens)` which
takes a token generator and returns a token generator.

- `is_morph (bool)`: Set to `True` for morphological filters (e.g., stemming).
  This allows the filter to be bypassed during query analysis if desired.

### STOP_WORDS

```python
whoosh.analysis.STOP_WORDS
```

A frozenset of common English stop words: `"a"`, `"an"`, `"and"`, `"the"`, etc.
Used as the default stoplist for `StopFilter` and `StandardAnalyzer`.

### url_pattern

```python
whoosh.analysis.url_pattern
```

A compiled regex useful for URL filtering.

### LowercaseFilter

```python
class whoosh.analysis.LowercaseFilter
```

Lowercases token text using `unicode.lower()`.

**Example:**
```python
rext = RegexTokenizer() | LowercaseFilter()
list(rext("This is a TEST"))
# => ["this", "is", "a", "test"]
```

### StopFilter

```python
class whoosh.analysis.StopFilter(
    stoplist=STOP_WORDS,
    minsize=2,
    maxsize=None,
    renumber=True,
    lang=None
)
```

Marks and optionally removes stop words from the token stream.

- `stoplist`: Set of words to filter out (defaults to `STOP_WORDS`).
- `minsize`: Minimum token length; shorter tokens are removed (default `2`).
- `maxsize`: Maximum token length; longer tokens are removed (default `None`).
- `renumber`: Renumber positions to account for removed tokens (default `True`).
- `lang`: If set, loads stop words for the given language code.

**Example:**
```python
from whoosh.analysis import RegexTokenizer, StopFilter

stopper = RegexTokenizer() | StopFilter()
list(stopper("this is a test"))
# => ["test"]
```

### StripFilter

```python
class whoosh.analysis.StripFilter
```

Calls `unicode.strip()` on each token's text.

### CharsetFilter

```python
class whoosh.analysis.CharsetFilter(charmap)
```

Translates token text using `unicode.translate()` with the given character map.
Useful for case folding and accent folding.

- `charmap`: Dictionary mapping character ordinals to unicode characters.

**Example:**
```python
from whoosh.support.charset import accent_map

rext = RegexTokenizer() | CharsetFilter(accent_map)
list(rext("café"))
# => ["cafe"]
```

### DelimitedAttributeFilter

```python
class whoosh.analysis.DelimitedAttributeFilter(
    delimiter="^",
    attribute="boost",
    default=1.0,
    type=float
)
```

Looks for delimiter characters in token text and extracts data after the
delimiter into a named token attribute.

- `delimiter`: Separator character (default `"^"`).
- `attribute`: Attribute name on the token (default `"boost"`).
- `default`: Default value if no delimiter is found (default `1.0`).
- `type`: Type to cast the extracted value (default `float`).

**Example:**
```python
from whoosh.analysis import RegexTokenizer, DelimitedAttributeFilter

daf = DelimitedAttributeFilter()
an = RegexTokenizer(r"\S+") | daf
for t in an(u"image 3.14^2 render"):
    print(t.text, t.boost)
# image 1.0
# 3.14 2.0
# render 1.0
```

### SubstitutionFilter

```python
class whoosh.analysis.SubstitutionFilter(pattern, replacement)
```

Performs regex substitution on token text using `re.sub()`.

- `pattern`: Pattern string or compiled regex.
- `replacement`: Replacement text.

**Example:**
```python
from whoosh.analysis import RegexTokenizer, SubstitutionFilter

# Remove hyphens
ana = RegexTokenizer(r"\S+") | SubstitutionFilter("-", "")
```

### MultiFilter

```python
class whoosh.analysis.MultiFilter(**kwargs)
```

Selects between two or more sub-filters based on the `mode` attribute of the
token stream. Useful for using different filters during indexing vs. querying.

- Keyword arguments map mode names to filter instances.

**Example:**
```python
from whoosh.analysis import MultiFilter, IntraWordFilter

iwf_index = IntraWordFilter(mergewords=True, mergenums=True)
iwf_query = IntraWordFilter(mergewords=False, mergenums=False)
mf = MultiFilter(index=iwf_index, query=iwf_query)
```

### TeeFilter

```python
class whoosh.analysis.TeeFilter(*filters)
```

Interleaves the results of two or more filter chains. Requires at least two
filters. Note: this filter is slow because it creates token copies.

**Example:**
```python
# Lowercase in one branch, reverse in another
f1 = LowercaseFilter()
f2 = ReverseTextFilter()
ana = RegexTokenizer(r"\S+") | TeeFilter(f1, f2)
```

### ReverseTextFilter

```python
class whoosh.analysis.ReverseTextFilter
```

Reverses the text of each token.

**Example:**
```python
an = RegexTokenizer() | ReverseTextFilter()
list(an("hello there"))
# => ["olleh", "ereht"]
```

### PassFilter

```python
class whoosh.analysis.PassFilter
```

Identity filter; passes tokens through unchanged.

### LoggingFilter

```python
class whoosh.analysis.LoggingFilter(logger=None)
```

Prints debug log entries for every token that passes through.

- `logger`: Logger instance (defaults to `whoosh.analysis` logger).

## Intraword Filters

### IntraWordFilter

```python
class whoosh.analysis.IntraWordFilter(
    delims="-_'\"()!@#$%^&*[]{}<>\\|;:,./?`~+=",
    splitwords=True,
    splitnums=True,
    mergewords=False,
    mergenums=False
)
```

Splits words into subwords and performs optional merging. Based on
WordDelimiterFilter in Solr.

- `delims`: String of delimiter characters.
- `splitwords`: Split at case transitions (e.g., `PowerShot` → `Power`, `Shot`).
- `splitnums`: Split at letter-number transitions (e.g., `SD500` → `SD`, `500`).
- `mergewords`: Merge consecutive alphabetic subwords.
- `mergenums`: Merge consecutive numeric subwords.

### CompoundWordFilter

```python
class whoosh.analysis.CompoundWordFilter(wordset, keep_compound=True)
```

Breaks compound tokens into their constituent parts if they match words in the
given wordset. Useful for agglutinative languages and trademarks.

- `wordset`: A set (or any `__contains__` object) of known words.
- `keep_compound`: If True, keep the original compound token in the stream.

### BiWordFilter

```python
class whoosh.analysis.BiWordFilter(sep="-")
```

Merges adjacent tokens into bigram tokens. Useful for pseudo-phrase searching.

- `sep`: Separator string for bigrams.

### ShingleFilter

```python
class whoosh.analysis.ShingleFilter(size=2, sep="-")
```

Merges N adjacent tokens into multi-word tokens (shingles).

- `size`: Number of tokens to combine.
- `sep`: Separator string.

**Note:** For `size=2`, `BiWordFilter` is faster.

## Morphological Filters

### StemFilter

```python
class whoosh.analysis.StemFilter(
    stemfn=stem,
    lang=None,
    ignore=None,
    cachesize=50000
)
```

Stems tokens using the Porter stemming algorithm (or a language-specific
stemmer if `lang` is specified).

- `stemfn`: Stemming function (default: Porter stemmer).
- `lang`: Language code to override `stemfn` with a Snowball stemmer.
- `ignore`: Set of words to not stem (defaults to stemming all words).
- `cachesize`: Cache size for stemmed words. Use `-1` for unbounded,
  `None` for no cache.

**Example:**
```python
from whoosh.analysis import RegexTokenizer, StemFilter

stemmer = RegexTokenizer() | StemFilter()
list(stemmer("fundamentally willows"))
# => ["fundament", "willow"]
```

### PyStemmerFilter

```python
class whoosh.analysis.PyStemmerFilter(
    lang="english",
    ignore=None,
    cachesize=10000
)
```

Subclass of `StemFilter` that uses the third-party `py-stemmer` library.
Requires the py-stemmer package to be installed.

**Methods:**
- `algorithms()`: Returns available stemming algorithms from py-stemmer.

### DoubleMetaphoneFilter

```python
class whoosh.analysis.DoubleMetaphoneFilter(
    primary_boost=1.0,
    secondary_boost=0.5,
    combine=False
)
```

Encodes tokens using Lawrence Philips's Double Metaphone algorithm. Useful
for phonetic matching of names and places.

- `primary_boost`: Boost factor for the primary code token.
- `secondary_boost`: Boost factor for the secondary code token.
- `combine`: If True, keep the original token alongside the encoded tokens.

## N-gram Filters and Analyzers

### NgramFilter

```python
class whoosh.analysis.NgramFilter(minsize, maxsize=None, at=None)
```

Splits token text into N-grams of varying sizes.

- `minsize`: Minimum N-gram size.
- `maxsize`: Maximum N-gram size (defaults to `minsize`).
- `at`: `'start'` for prefix grams, `'end'` for suffix grams, or `None`
  for all position grams.

### NgramAnalyzer

```python
whoosh.analysis.NgramAnalyzer(minsize, maxsize=None) -> Analyzer
```

Composes `NgramTokenizer` with `LowercaseFilter`.

### NgramWordAnalyzer

```python
whoosh.analysis.NgramWordAnalyzer(
    minsize,
    maxsize=None,
    tokenizer=None,
    at=None
) -> Analyzer
```

Composes `RegexTokenizer`, `LowercaseFilter`, and `NgramFilter`. Use this
when you want sub-word n-grams (without whitespace) rather than raw
character n-grams.


## DOCUMENT: Automata

# Automata API

Module for constructing and manipulating finite state automata (FSAs),
including NFAs, DFAs, finite state transducers (FSTs), Levenshtein
automata, and regular expression automata. Used internally for spelling
correction, fuzzy term queries, and term dictionary operations.

The automata module is a refactored package with submodules. All classes
and functions are importable directly from `whoosh.automata`.

## Module Functions

### `parse_glob`

```python
whoosh.automata.parse_glob(pattern, _glob_multi="*", _glob_single="?", _glob_range1="[", _glob_range2="]") -> NFA
```

Parses a glob-style pattern string and returns an NFA that matches strings
matching the pattern.

**Parameters:**
- `pattern`: Glob pattern string (`*` matches any sequence, `?` matches any
  single character).
- `_glob_multi`, `_glob_single`: Override the wildcard characters.
- `_glob_range1`, `_glob_range2`: Override the range syntax brackets.

### `glob_automaton`

```python
whoosh.automata.glob_automaton(pattern) -> NFA
```

Convenience function that parses a glob pattern and returns an NFA.

## FSA (Finite State Automaton) Classes

### `FSA`

```python
class whoosh.automata.FSA(initial)
```

Base class for finite state automata.

**Constructor:**
- `initial`: The initial state.

**Attributes:**
- `initial`: Initial state.
- `transitions`: Dict mapping source states to dicts mapping labels to
  target states.
- `final_states`: Set of accepting (final) states.

**Methods:**
- `__eq__(other)`: Compares initial state, final states, and transitions.
- `all_states()`: Returns a set of all states reachable in the automaton.
- `all_labels()`: Returns a set of all transition labels.
- `get_labels(src)`: Yields all labels leaving state `src`.
- `generate_all(state=None, sofar="")`: Yields all strings accepted by the
  automaton.
- `move(state, label)`: Returns the state reached by following `label` from
  `state`, or `None`.
- `moves(state, labels)`: Yields `(label, next_state)` pairs.
- `next(state)`: Yields target states reachable from `state` via any label.
- `is_final(state)`: Returns `True` if `state` is a final state.
- `start()`: Returns the initial state.
- `has_path_to(target)`: Returns `True` if there is a path to `target`.

### `Marker`

```python
class whoosh.automata.Marker(name)
```

Marker object used as a special transition label in NFAs (e.g., `ANY`,
`EPSILON`).

### `EPSILON`

```python
whoosh.automata.EPSILON = Marker("EPSILON")
```

Special marker representing an epsilon transition (no input consumed).

### `ANY`

```python
whoosh.automata.ANY = Marker("ANY")
```

Special marker representing a transition that matches any input character.

### `NFA`

```python
class whoosh.automata.NFA(initial)
```

Nondeterministic Finite Automaton. Extends `FSA` with epsilon transitions
and NFA-specific construction methods.

**Methods:**
- `add_transition(src, label, dst)`: Adds a transition from `src` to `dst`
  consuming `label`.
- `add_final_state(state, final=True)`: Marks `state` as a final/accepting
  state.
- `epsilon_closure(state)`: Returns the set of states reachable from `state`
  via epsilon transitions.
- `to_dfa()`: Converts this NFA to an equivalent DFA and returns it.

### `DFA`

```python
class whoosh.automata.DFA(initial)
```

Deterministic Finite Automaton. Extends `FSA` with DFA-specific operations.

**Methods:**
- `next_valid_string(string)`: Finds the lexicographically smallest string
  accepted by the DFA that is greater than or equal to `string`.
- `to_dfa()`: Returns self (already a DFA).

### `renumber_dfa`

```python
whoosh.automata.renumber_dfa(dfa, base=0) -> DFA
```

Renumerates the states of a DFA to integers starting at `base`.

### `u_to_utf8`

```python
whoosh.automata.u_to_utf8(dfa, base=0) -> DFA
```

Converts a Unicode DFA to a UTF-8 DFA.

### `find_all_matches`

```python
whoosh.automata.find_all_matches(dfa, lookup_func, first=unull)
```

Yields all strings accepted by the DFA, using `lookup_func` to determine
which strings exist in the dictionary.

**Parameters:**
- `dfa`: A deterministic finite automaton.
- `lookup_func`: Function called with each candidate string; returns the
  string if found in the dictionary.
- `first`: First string to start matching from (default `chr(0)`).

### `reverse_nfa`

```python
whoosh.automata.reverse_nfa(n) -> NFA
```

Returns the reverse of an NFA (reversed transitions, swapped initial
and final states).

### `product`

```python
whoosh.automata.product(dfa1, op, dfa2) -> DFA
```

Computes the product of two DFAs using a binary operation.

**Parameters:**
- `dfa1`, `dfa2`: Input DFAs.
- `op`: A function `(set1, set2) -> set` computing the output final states
  from the two input final state sets.

### `intersection`

```python
whoosh.automata.intersection(dfa1, dfa2) -> DFA
```

Returns the intersection of two DFAs.

### `union`

```python
whoosh.automata.union(dfa1, dfa2) -> DFA
```

Returns the union of two DFAs.

### `epsilon_nfa`

```python
whoosh.automata.epsilon_nfa() -> NFA
```

Returns an NFA that accepts only the empty string.

### `dot_nfa`

```python
whoosh.automata.dot_nfa() -> NFA
```

Returns an NFA that accepts any single character.

### `basic_nfa`

```python
whoosh.automata.basic_nfa(label) -> NFA
```

Returns an NFA that accepts exactly the string `label`.

### `charset_nfa`

```python
whoosh.automata.charset_nfa(labels) -> NFA
```

Returns an NFA that accepts any single character in `labels`.

### `string_nfa`

```python
whoosh.automata.string_nfa(string) -> NFA
```

Returns an NFA that accepts exactly `string`.

### `choice_nfa`

```python
whoosh.automata.choice_nfa(n1, n2) -> NFA
```

Returns an NFA that accepts strings accepted by either `n1` or `n2`.

### `concat_nfa`

```python
whoosh.automata.concat_nfa(n1, n2) -> NFA
```

Returns an NFA that accepts the concatenation of `n1` and `n2`.

### `star_nfa`

```python
whoosh.automata.star_nfa(n) -> NFA
```

Returns an NFA that accepts zero or more repetitions of `n`.

### `plus_nfa`

```python
whoosh.automata.plus_nfa(n) -> NFA
```

Returns an NFA that accepts one or more repetitions of `n`.

### `optional_nfa`

```python
whoosh.automata.optional_nfa(n) -> NFA
```

Returns an NFA that accepts zero or one occurrence of `n`.

### `strings_dfa`

```python
whoosh.automata.strings_dfa(strings) -> DFA
```

Constructs a minimal DFA that accepts exactly the given strings.

### `add_suffix`

```python
whoosh.automata.add_suffix(dfa, nodes, last, downto, seen)
```

Internal function for adding suffixes to a trie during DFA construction.

## Levenshtein Automata

### `levenshtein_automaton`

```python
whoosh.automata.levenshtein_automaton(term, k, prefix=0) -> NFA
```

Constructs an NFA that matches all strings within edit distance `k` of
`term`. This is the core function for fuzzy term queries and spelling
suggestions.

**Parameters:**
- `term`: The reference string to compute edit distance from.
- `k`: Maximum edit distance (number of insertions, deletions, or
  substitutions).
- `prefix`: If positive, require matched strings to share this length of
  prefix with `term` (speeds up matching significantly).

**Returns:** An NFA that can be converted to a DFA via `.to_dfa()`.

```python
from whoosh.automata import levenshtein_automaton

nfa = levenshtein_automaton("hello", k=1, prefix=0)
dfa = nfa.to_dfa()
```

## RegEx

### `parse`

```python
whoosh.automata.parse(pattern) -> NFA
```

Parses a regular expression pattern string and returns an NFA.

**Parameters:**
- `pattern`: A regex pattern string (Python `re`-style syntax).

### `RegexBuilder`

```python
class whoosh.automata.RegexBuilder(pattern)
```

Helper class for building NFAs from regex patterns.

## FST (Finite State Transducer) Classes

### `Values`

```python
class whoosh.automata.Values
```

Abstract base class for value types stored in FST arcs.

### `IntValues`

```python
class whoosh.automata.IntValues
```

Stores integer values in FST arcs.

### `SequenceValues`

```python
class whoosh.automata.SequenceValues
```

Base class for value types that store sequences of values.

### `BytesValues`

```python
class whoosh.automata.BytesValues
```

Stores byte string values in FST arcs.

### `ArrayValues`

```python
class whoosh.automata.ArrayValues
```

Stores arrays of values in FST arcs.

### `IntListValues`

```python
class whoosh.automata.IntListValues
```

Stores lists of integers in FST arcs.

### `Node`

```python
class whoosh.automata.Node
```

Base class for nodes in an FST.

### `ComboNode`

```python
class whoosh.automata.ComboNode
```

Base class for nodes that combine multiple sub-nodes (intersection, union).

### `UnionNode`

```python
class whoosh.automata.UnionNode
```

A node that represents the union of multiple sub-nodes.

### `IntersectionNode`

```python
class whoosh.automata.IntersectionNode
```

A node that represents the intersection of multiple sub-nodes.

### `BaseCursor`

```python
class whoosh.automata.BaseCursor
```

Base class for cursors that iterate over FST contents.

### `Cursor`

```python
class whoosh.automata.Cursor
```

Concrete cursor for iterating over an FST, supporting `next()`, `find()`,
`text()`, and other navigation methods.

### `UncompiledNode`

```python
class whoosh.automata.UncompiledNode
```

Represents an FST node that has not yet been compiled into a binary
representation. Used during FST construction.

### `Arc`

```python
class whoosh.automata.Arc
```

Represents a single arc in an FST, with a label, target node, and associated
value.

### `GraphWriter`

```python
class whoosh.automata.GraphWriter
```

Writes an FST to a binary file on disk or to an in-memory buffer.

### `BaseGraphReader`

```python
class whoosh.automata.BaseGraphReader
```

Base class for reading FSTs from disk.

### `GraphReader`

```python
class whoosh.automata.GraphReader
```

Concrete reader for FSTs stored on disk. Supports `find()`, `next()`, and
`text()` for navigating the graph.

### `to_labels`

```python
whoosh.automata.to_labels(key)
```

Converts a key (string, int, etc.) into a list of FST arc labels.

### `within`

```python
whoosh.automata.within(graph, text, k=1, prefix=0, address=None)
```

Uses a pre-built FST and a Levenshtein automaton to find all keys in the
graph within edit distance `k` of `text`.

**Parameters:**
- `graph`: A `GraphReader` instance.
- `text`: The search term.
- `k`: Maximum edit distance.
- `prefix`: Required shared prefix length.
- `address`: Optional starting address in the graph.

### `dump_graph`

```python
whoosh.automata.dump_graph(graph, address=None, tab=0, out=None)
```

Debug utility that prints the structure of an FST to stdout or a file.

### `FileVersionError`

```python
class whoosh.automata.FileVersionError
```

Raised when reading an FST file with an incompatible version.

### `InactiveCursor`

```python
class whoosh.automata.InactiveCursor
```

Raised when operating on a cursor that is not at a valid position.


## DOCUMENT: Backends

# Backends API

Storage backend abstractions.

## Backend (ABC)

```python
class whoosh.backends.abc.Backend
```

Abstract base class for storage backends.

### Methods

#### `create()`

Create a new segment.

#### `open()`

Open an existing segment.

#### `close()`

Close the backend.

#### `commit()`

Commit changes.

#### `startup()`

Called on backend startup.

#### `shutdown()`

Called on backend shutdown.

---

## FileBackend

```python
class whoosh.backends.file.FileBackend
```

Default backend storing index as files.

```python
from whoosh.backends.file import FileBackend
from whoosh.store.filestore import FileStorage

storage = FileStorage("indexdir")
backend = FileBackend(storage=storage)
```

---

## SQLiteBackend

```python
class whoosh.backends.sqlite.SQLiteBackend
```

Stores index in SQLite database.

```python
from whoosh.backends.sqlite import SQLiteBackend
from whoosh.store.sqlite import SQLiteStorage

storage = SQLiteStorage("index.db")
backend = SQLiteBackend(storage=storage)
```

---

## MemoryBackend

```python
class whoosh.backends.memory.MemoryBackend
```

In-memory backend for testing.

```python
from whoosh.backends.memory import MemoryBackend

backend = MemoryBackend()
```

---

## BackendRegistry

```python
class whoosh.registry.BackendRegistry
```

Register backends:

```python
BackendRegistry.register("my_backend", MyBackendClass, "my_package")
backend = BackendRegistry.get("my_backend")
```


## DOCUMENT: Classify

# Classify API

Classes and functions for classifying and extracting information from
documents. This module provides query expansion models, similarity
functions (shingling, simhash), and clustering algorithms.

## Expansion Models

### `ExpansionModel`

```python
class whoosh.classify.ExpansionModel(doc_count, field_length)
```

Abstract base class for query expansion models. Subclass to implement custom
expansion scoring.

**Constructor:**
- `doc_count`: Total number of documents in the collection.
- `field_length`: Total length of the field across all documents.

**Computed Attributes:**
- `N`: Document count.
- `collection_total`: Total field length.
- `mean_length`: Average field length (`collection_total / N`).

**Methods:**
- `normalizer(maxweight, top_total)`: Returns a normalization factor.
- `score(weight_in_top, weight_in_collection, top_total)`: Returns the
  expansion score for a term.

### `Bo1Model`

```python
class whoosh.classify.Bo1Model(doc_count, field_length)
```

Bayesian One-Poisson expansion model. One of the standard query expansion
models.

### `Bo2Model`

```python
class whoosh.classify.Bo2Model(doc_count, field_length)
```

Bayesian Two-Poisson expansion model. Another standard query expansion model.

### `KLModel`

```python
class whoosh.classify.KLModel(doc_count, field_length)
```

Kullback-Leibler divergence-based expansion model.

## Expander

### `Expander`

```python
class whoosh.classify.Expander(
    ixreader,
    fieldname,
    model=Bo1Model
)
```

Uses an `ExpansionModel` to expand the set of query terms based on the top N
result documents.

**Constructor:**
- `ixreader`: An `IndexReader` object.
- `fieldname`: The name of the field to expand terms from.
- `model`: An `ExpansionModel` class or instance. Defaults to `Bo1Model`.

**Methods:**

#### `add(vector)`

Adds forward-index information about one of the "top N" documents.

- `vector`: A series of `(text, weight)` tuples, such as is returned by
  `Reader.vector_as("weight", docnum, fieldname)`.

#### `add_document(docnum)`

Adds a document's term vector to the expander. If the field has a term vector,
uses it; otherwise falls back to stored field text.

#### `add_text(string)`

Adds a text string by indexing it with the field's analyzer.

#### `expanded_terms(number, normalize=True)`

Returns the N most important terms in the vectors added so far, ranked by
the expansion model's score.

- `number`: Number of terms to return.
- `normalize`: Whether to normalize weights.
- Returns: List of `(term, weight)` tuples, sorted by weight descending.

```python
from whoosh.classify import Expander, Bo1Model

expander = Expander(ix.reader(), "content")
for docnum in results.ids()[:10]:
    expander.add_document(docnum)

for word, weight in expander.expanded_terms(5):
    print(word, weight)
```

## Similarity Functions

### `shingles`

```python
whoosh.classify.shingles(input, size=2) -> iterable
```

Generates `(shingle, frequency)` pairs from a string by sliding a window of
the given size over the input.

**Parameters:**
- `input`: The input string.
- `size`: The shingle size (default `2`).

```python
from whoosh.classify import shingles

for shingle, freq in shingles("hello world", size=2):
    print(shingle, freq)
```

### `simhash`

```python
whoosh.classify.simhash(features, hashbits=32) -> int
```

Computes a simhash (perceptual hash) from a sequence of weighted features.
Simhashes that are similar produce similar hash values, allowing fast
near-duplicate detection via Hamming distance.

**Parameters:**
- `features`: Iterable of `(feature, weight)` tuples.
- `hashbits`: Number of bits in the hash (default `32`).
- Returns: An integer hash value.

```python
from whoosh.classify import shingles, simhash

h1 = simhash(shingles(text1))
h2 = simhash(shingles(text2))
from whoosh.classify import hamming_distance
dist = hamming_distance(h1, h2)
```

### `hamming_distance`

```python
whoosh.classify.hamming_distance(first_hash, other_hash, hashbits=32) -> int
```

Computes the Hamming distance between two hash values. A small distance
indicates high similarity.

**Parameters:**
- `first_hash`: First hash integer.
- `other_hash`: Second hash integer.
- `hashbits`: Number of bits in the hashes (default `32`).

## Clustering

### `kmeans`

```python
whoosh.classify.kmeans(
    data,
    k,
    t=0.0001,
    distfun=None,
    maxiter=50,
    centers=None
) -> (labels, centroids)
```

One-dimensional K-means clustering. Assigns each data point to the nearest
of `k` centroids and returns cluster labels and final centroids.

**Parameters:**
- `data`: List of data points (numeric values).
- `k`: Number of clusters.
- `t`: Tolerance; stops if centroid changes are below this value.
- `distfun`: Optional distance function (unused if `None`).
- `maxiter`: Maximum iterations (default `50`).
- `centers`: Optional list of initial centroids. If `None`, selects `k`
  random points from `data`.

**Returns:** A tuple `(labels, centroids)` where `labels` is a list of
cluster assignments per data point and `centroids` is the list of final
centroid positions.

### `two_pass_variance`

```python
whoosh.classify.two_pass_variance(data) -> float
```

Computes the sample variance of a data list using the two-pass algorithm
(first pass computes the mean, second pass accumulates squared deviations).

### `weighted_incremental_variance`

```python
whoosh.classify.weighted_incremental_variance(data_weight_pairs) -> float
```

Computes the weighted variance incrementally from a sequence of
`(value, weight)` pairs.

### `swin`

```python
whoosh.classify.swin(data, size) -> list
```

Sliding window clustering that groups data points where the range (max - min)
within a window of `size` is below a threshold. Uses variance for ranking.

**Parameters:**
- `data`: Sorted list of data points.
- `size`: Maximum window range (max - min) for clustering.

**Returns:** A list of `(left, right, count, variance)` tuples representing
clusters, sorted by count descending then by variance ascending.


## DOCUMENT: Codecs

# Codecs API

Classes and interfaces for how Whoosh writes and reads the inverted index,
postings, and per-document values. The codecs module is a refactored package
exposing the same public API as the former monolithic module.

## Module Functions

### `default_codec`

```python
whoosh.codec.default_codec(*args, **kwargs) -> Codec
```

Returns the default codec used by the index. Currently returns a
`W3Codec` instance.

```python
from whoosh.codec import default_codec
codec = default_codec()
```

## Exceptions

### `OutOfOrderError`

```python
whoosh.codec.OutOfOrderError
```

Raised when documents are added to a field out of order. Fields must
receive documents in ascending docnum order.

## Base Classes

### `Codec`

```python
class whoosh.codec.Codec
```

Abstract base class for index codecs. Subclasses implement methods for
writing and reading the index format.

**Class Attributes:**
- `length_stats (bool)`: If `True`, the codec stores per-document field
  length statistics. Default `True`.

**Methods:**

#### `per_document_writer(storage, segment)`

Abstract. Returns a `PerDocumentWriter` for writing per-document values
(columns, term vectors) to the given segment.

#### `field_writer(storage, segment)`

Abstract. Returns a `FieldWriter` for writing postings to the given segment.

#### `postings_writer(dbfile, byteids=False)`

Abstract. Returns a `PostingsWriter` for writing posting lists to `dbfile`.

#### `postings_reader(dbfile, terminfo, format_, term=None, scorer=None)`

Abstract. Returns a `Matcher` for reading postings from `dbfile`.

#### `automata(storage, segment)`

Returns an `Automata` instance for spelling correction using automata-based
edit distance. Default returns a base `Automata()` object.

#### `terms_reader(storage, segment)`

Abstract. Returns a `TermsReader` for reading the term dictionary and
postings of the given segment.

#### `per_document_reader(storage, segment)`

Abstract. Returns a `PerDocumentReader` for reading per-document values
from the given segment.

#### `new_segment(storage, indexname)`

Abstract. Creates and returns a new `Segment` object for the given storage
and index name.

### `WrappingCodec`

```python
class whoosh.codec.WrappingCodec(child)
```

A `Codec` that delegates all operations to a child codec. Useful for
creating codec wrappers that modify or intercept specific operations.

**Constructor:**
- `child`: The underlying `Codec` instance to wrap.

All methods delegate to the child codec:
`per_document_writer()`, `field_writer()`, `postings_writer()`,
`postings_reader()`, `automata()`, `terms_reader()`, `per_document_reader()`,
`new_segment()`.

## Writer Classes

### `PerDocumentWriter`

```python
class whoosh.codec.PerDocumentWriter
```

Abstract base class for writing per-document values (columns, term vectors).

**Methods:**

#### `start_doc(docnum)`

Abstract. Called when starting to write a new document.

#### `add_field(fieldname, fieldobj, value, length)`

Abstract. Adds a field value to the current document.

#### `add_column_value(fieldname, columnobj, value)`

Abstract. Adds a column value. Raises `NotImplementedError` if the codec
doesn't support columns.

#### `add_vector_items(fieldname, fieldobj, items)`

Abstract. Adds term vector items.

#### `add_vector_matcher(fieldname, fieldobj, vmatcher)`

Convenience method that reads items from a `Matcher` and calls
`add_vector_items()`.

#### `finish_doc()`

Called when finishing a document. Default does nothing.

#### `close()`

Called when done writing. Default does nothing.

### `FieldWriter`

```python
class whoosh.codec.FieldWriter
```

Abstract base class for writing postings (inverted index) data.

**Methods:**

#### `add_postings(schema, lengths, items)`

Translates a generator of `(fieldname, btext, docnum, weight, vbytes)`
tuples into calls to `start_field()`, `start_term()`, `add()`,
`finish_term()`, and `finish_field()`.

**Parameters:**
- `schema`: The `Schema` object.
- `lengths`: Optional `FieldLengthTable` for document field lengths.
- `items`: Iterable of posting tuples.

#### `start_field(fieldname, fieldobj)`

Abstract. Called when starting a new field.

#### `start_term(text)`

Abstract. Called when starting a new term within a field.

#### `add(docnum, weight, vbytes, length=None)`

Abstract. Adds a posting to the current term.

#### `add_spell_word(fieldname, text)`

Called to add a word to the spelling index. Default does nothing.

#### `finish_term()`

Abstract. Called when finishing a term.

#### `finish_field()`

Called when finishing a field. Default does nothing.

#### `close()`

Called when done writing. Default does nothing.

### `PostingsWriter`

```python
class whoosh.codec.PostingsWriter
```

Abstract base class for writing posting lists (the inverted index).

**Methods:**

#### `start_postings(format_, terminfo)`

Abstract. Starts writing postings for a new term.

#### `add_posting(id_, weight, vbytes, length=None)`

Abstract. Adds a posting to the current term.

#### `finish_postings(allow_compact=True)`

Called when finished writing postings. Default does nothing.

#### `written()`

Abstract. Returns `True` if this writer has already written to disk.

## Reader Classes

### `FieldCursor`

```python
class whoosh.codec.FieldCursor
```

Abstract base class for iterating over terms in a field.

**Methods:**
- `first()`: Move to the first term.
- `find(string)`: Find a term matching or closest to `string`.
- `next()`: Move to the next term.
- `term()`: Returns the current term's text.

### `EmptyCursor`

```python
class whoosh.codec.EmptyCursor
```

A `FieldCursor` representing an empty field. All methods return `None` or
`False`.

### `TermsReader`

```python
class whoosh.codec.TermsReader
```

Abstract base class for reading the term dictionary and postings of a
segment.

**Methods:**
- `__contains__(term)`: Returns `True` if the term exists.
- `cursor(fieldname, fieldobj)`: Returns a `FieldCursor`.
- `terms()`: Yields `(fieldname, text)` tuples for all terms.
- `terms_from(fieldname, prefix)`: Yields terms from `fieldname` starting
  with `prefix`.
- `items()`: Yields `((fieldname, text), TermInfo)` tuples.
- `items_from(fieldname, prefix)`: Like `items()` but filtered by prefix.
- `term_info(fieldname, text)`: Returns a `TermInfo` for the term.
- `frequency(fieldname, text)`: Returns the total frequency.
- `doc_frequency(fieldname, text)`: Returns the document frequency.
- `matcher(fieldname, text, format_, scorer=None)`: Returns a `Matcher`.
- `indexed_field_names()`: Yields names of indexed fields.
- `close()`: Close the reader.

### `PerDocumentReader`

```python
class whoosh.codec.PerDocumentReader
```

Abstract base class for reading per-document values (columns, term vectors,
stored fields).

**Methods:**
- `close()`: Close the reader.
- `doc_count()`: Returns number of non-deleted documents.
- `doc_count_all()`: Returns total document count (including deleted).
- `has_deletions()`: Returns `True` if any documents are deleted.
- `is_deleted(docnum)`: Returns `True` if docnum is deleted.
- `deleted_docs()`: Yields docnums of deleted documents.
- `all_doc_ids()`: Yields docnums of all non-deleted documents.
- `supports_columns()`: Returns `True` if column storage is supported.
- `has_column(fieldname)`: Returns `True` if field has a column.
- `list_columns()`: Yields names of available columns.
- `column_reader(fieldname, column)`: Returns a column reader.
- `doc_field_length(docnum, fieldname)`: Returns field length for docnum.
- `field_length(fieldname)`: Returns total field length.
- `min_field_length(fieldname)`: Returns minimum field length.
- `max_field_length(fieldname)`: Returns maximum field length.
- `has_vector(docnum, fieldname)`: Returns `True` if docnum has a vector.
- `vector(docnum, fieldname, format_)`: Returns a `Matcher` for the vector.
- `stored_fields(docnum)`: Returns dict of stored field values.
- `all_stored_field()`: Yields stored fields for all documents.

### `MultiPerDocumentReader`

```python
class whoosh.codec.MultiPerDocumentReader(readers, offset=0)
```

Combines multiple `PerDocumentReader` instances into one for multi-segment
indices.

**Constructor:**
- `readers`: List of `PerDocumentReader` instances.
- `offset`: Base document offset (usually `0`).

## Automata

### `Automata`

```python
class whoosh.codec.Automata
```

Provides static methods for automata-based term matching, used by the
spelling corrector.

**Static Methods:**

#### `levenshtein_dfa(uterm, maxdist, prefix=0)`

Returns a deterministic finite automaton (DFA) that matches all edit-distance
variants of `uterm` within `maxdist` edits, optionally requiring a minimum
shared prefix of length `prefix`.

#### `find_matches(dfa, cur)`

Given a DFA and a `FieldCursor`, yields all matching terms.

**Methods:**

#### `terms_within(fieldcur, uterm, maxdist, prefix=0)`

Returns an iterator of matching terms within the given edit distance of
`uterm`.

## Segment

### `Segment`

```python
class whoosh.codec.Segment
```

Represents a segment of the index. Instances are pickled into the TOC file
to describe on-disk files.

**Class Attributes:**
- `COMPOUND_EXT = ".seg"`: Extension for compound segment files.

**Instance Attributes:**
- `indexname`: Base name of the segment.
- `segid`: Random unique ID string.
- `compound (bool)`: Whether this segment uses compound file format.

**Methods:**
- `make_filename(ext)`: Returns `f"{segment_id()}{ext}"`.
- `list_files(storage)`: Lists all files belonging to this segment.
- `create_file(storage, ext, **kwargs)`: Creates a new file for this segment.
- `open_file(storage, ext, **kwargs)`: Opens a file for this segment.
- `create_compound_file(storage)`: Combines all segment files into a
  compound `.seg` file.
- `open_compound_file(storage)`: Opens the compound segment file.
- `doc_count_all()`: Abstract. Returns total document count.
- `doc_count()`: Returns non-deleted document count.
- `set_doc_count(doccount)`: Sets the document count.
- `has_deletions()`: Returns `True` if any documents are deleted.
- `deleted_count()`: Abstract. Returns number of deleted documents.
- `deleted_docs()`: Abstract. Yields docnums of deleted documents.
- `delete_document(docnum, delete=True)`: Abstract. Deletes/undeletes a
  document.
- `is_deleted(docnum)`: Abstract. Returns `True` if docnum is deleted.
- `should_assemble()`: Returns `True` by default. Override to control
  compound file behavior.
- `validate(storage)`: Checks on-disk integrity of this segment.
- `segment_id()`: Returns the unique segment identifier string.
- `is_compound()`: Returns `True` if this segment uses compound file format.

### `WrappingSegment`

```python
class whoosh.codec.WrappingSegment(child)
```

A `Segment` that delegates all operations to a child segment.

**Constructor:**
- `child`: The underlying `Segment` instance to wrap.

## W3 Codec (Default)

The `W3` codec ("Whoosh 3") is the default index format, storing postings in
compressed blocks for efficient reading and skipping.

### `W3Codec`

```python
class whoosh.codec.whoosh3.W3Codec(blocklimit=128, compression=3, inlinelimit=1)
```

The default codec. Uses compressed blocks and term inlining for efficient
storage and fast lookups.

**Constructor:**
- `blocklimit`: Number of postings per block (default `128`).
- `compression`: zlib compression level (default `3`, `0` = no compression).
- `inlinelimit`: Maximum number of postings to inline directly in the term
  info (default `1`).

**File Extensions:**
- `.trm`: Term dictionary
- `.pst`: Postings
- `.vps`: Vector postings
- `.col`: Per-document value columns

### `W3PerDocWriter`

Writer for per-document values using the W3 format. Handles columns,
stored fields, term vectors, and field lengths.

### `W3FieldWriter`

Writer for the inverted term index using the W3 format. Uses a
`OrderedHashWriter` for the term dictionary and posts to a postings file.

### `W3LeafMatcher`

```python
class whoosh.codec.whoosh3.W3LeafMatcher(postfile, startoffset, length, format_, term=None, byteids=None, scorer=None)
```

Reads on-disk postings from the postings file and presents the
`Matcher` interface. Supports block-level skipping and lazy block loading.

**Optimization methods:**
- `block_min_id()`: Returns the first doc ID in the current block.
- `block_max_id()`: Returns the last doc ID in the current block.
- `block_min_length()`: Returns the minimum field length in the current block.
- `block_max_length()`: Returns the maximum field length in the current block.
- `block_max_weight()`: Returns the maximum weight in the current block.
- `skip_to_quality(minquality)`: Skips blocks exceeding a quality threshold.

### `W3TermsReader`

Reader for the term dictionary using the W3 format. Uses an
`OrderedHashReader` for fast lookups.

### `W3TermInfo`

```python
class whoosh.codec.whoosh3.W3TermInfo
```

Stores term statistics and posting location information. Supports inlining
small posting sets directly in the term dictionary for fast lookups.

**Flags:**
- `_FLAG_OFFSET` (0): Postings stored at an offset in the postings file.
- `_FLAG_INLINE_PICKLE` (1): Postings inlined as a pickled tuple.
- `_FLAG_INLINE_COMPACT` (2): Single posting compactly inlined.
- `_FLAG_INLINE_COMPACT_SHORT` (3): Multiple postings compactly inlined.

**Methods:**
- `add_block(block)`: Merges block statistics into this term info.
- `set_extent(offset, length)`: Sets offset and length of postings in file.
- `extent()`: Returns `(offset, length)`.
- `set_inlined(ids, weights, values)`: Sets inlined posting data.
- `set_compact_inline(id_, weight, value)`: Sets single inlined posting.
- `set_compact_short_inline(ids, weights, values)`: Sets multiple compact
  inlined postings.
- `is_inlined()`: Returns `True` if postings are inlined.
- `inlined_postings()`: Returns `(ids, weights, values)` tuples for inlined
  postings.
- `to_bytes()` / `from_bytes()`: Serialize/deserialize.

### `W3Segment`

```python
class whoosh.codec.whoosh3.W3Segment(codec, indexname, doccount=0, segid=None, deleted=None)
```

Segment class for the W3 codec. Stores a reference to the codec, document
count, and deleted document set.

## Plain Text Codec (Debugging)

### `PlainTextCodec`

```python
class whoosh.codec.plaintext.PlainTextCodec
```

A codec that stores the index as human-readable plain text. Intended for
debugging and manual inspection, not for production use.

**Class Attributes:**
- `length_stats = False`

**File extensions:**
- `.dcs`: Document (stored fields, columns, vectors)
- `.trm`: Term dictionary (plain text)

### `PlainPerDocWriter`

Plain text writer for per-document values.

### `PlainPerDocReader`

Plain text reader for per-document values.

### `PlainFieldWriter`

Plain text writer for the inverted index.

### `PlainTermsReader`

Plain text reader for the term dictionary.

### `PlainSegment`

```python
class whoosh.codec.plaintext.PlainSegment(indexname)
```

Segment class for the plain text codec. Does not support compound files
(`should_assume()` returns `False`).

## Memory Codec

### `MemoryCodec`

```python
class whoosh.codec.memory.MemoryCodec
```

An in-memory-only codec for testing. Stores all data in Python objects
rather than on disk.

**Class Attributes:**
- `storage`: A `RamStorage` instance.
- `segment`: A `MemSegment` instance.

**Methods:**
- `writer(schema)`: Returns a `MemWriter`.
- `reader(schema)`: Returns a `SegmentReader`.

### `MemWriter`

```python
class whoosh.codec.memory.MemWriter
```

A `SegmentWriter` subclass that commits immediately without merging.

### `MemPerDocWriter`

In-memory writer for per-document values.

### `MemPerDocReader`

In-memory reader for per-document values.

### `MemFieldWriter`

In-memory writer for the inverted index.

### `MemTermsReader`

In-memory reader for the term dictionary.

### `MemSegment`

```python
class whoosh.codec.memory.MemSegment(codec, indexname)
```

In-memory segment storing all data in Python dictionaries (inverted index,
stored fields, lengths, vectors, term infos). Uses a `Lock` for thread-safe
access.


## DOCUMENT: Collectors

# Collectors API

Classes and functions for gathering search results. Collectors are used
internally by `Searcher.search()` to collect matching documents and build
`Results` objects. The collectors module is a refactored package exposing the
same public API as the former monolithic module.

## Overview

A `Collector` iterates over matching documents in an index, collects
information about them, and produces a `Results` object. The base `Collector`
class defines the interface; specialized subclasses implement different
collection strategies (top-N, unlimited, sorting, filtering, faceting, etc.).

## Core Classes

### `Collector`

```python
class whoosh.collectors.Collector
```

Abstract base class for all collectors. Subclasses must implement `collect()`
and `results()`.

**Methods:**

#### `prepare(top_searcher, q, context)`

Called before a search begins. Sets up `self.top_searcher`, `self.q`,
`self.context`, `self.starttime`, and `self.docset`.

#### `run()`

Iterates over sub-searchers, calling `set_subsearcher()` and
`collect_matches()` for each, then calls `finish()`.

#### `set_subsearcher(subsearcher, offset)`

Called when moving to a new sub-searcher. Sets `self.subsearcher`,
`self.offset`, and `self.matcher`.

#### `collect(sub_docnum)`

Called for every matched document. Must add the document to results and
return a sort key. Subclasses must implement this.

- `sub_docnum`: Segment-relative document number. Add `self.offset` to get
  the top-level document number.

#### `sort_key(sub_docnum)`

Returns a sort key for the current match without the side effect of adding
the document to results. Subclasses must implement this.

#### `collect_matches()`

Calls `matches()` and then `collect()` for each matched document.

#### `matches()`

Yields segment-relative document numbers for matches in the current
sub-searcher.

#### `count()`

Returns the total number of matching documents.

#### `all_ids()`

Returns a sequence of docnums matched in this collector.

#### `computes_count()`

Returns `True` if the collector naturally computes the exact count of
matching documents.

#### `finish()`

Called after the search completes. Sets `self.runtime`.

#### `remove(global_docnum)`

Removes a document from the collector using its global docnum.

#### `results()`

Returns a `Results` object. Subclasses must implement this.

### `ilen`

```python
whoosh.collectors.ilen(iterator) -> int
```

Counts the number of items in an iterator without loading it all into memory.

## Scored Collectors

### `ScoredCollector`

```python
class whoosh.collectors.ScoredCollector(replace=10)
```

Base class for collectors that sort by document score.

**Constructor:**
- `replace`: Number of matches between attempts to replace the matcher with
  a more efficient version.

### `TopCollector`

```python
class whoosh.collectors.TopCollector(
    limit=10,
    usequality=True,
    **kwargs
)
```

A collector that returns only the top N scored results.

**Constructor:**
- `limit`: Maximum number of results to return.
- `usequality`: Whether to use block-quality optimizations for faster
  search. Can be set to `False` for debugging.

**Notes:**
- When `usequality=True`, `computes_count()` returns `False` and
  `all_ids()` requires re-searching.
- Uses a min-heap to efficiently track the top N documents.

### `UnlimitedCollector`

```python
class whoosh.collectors.UnlimitedCollector(reverse=False)
```

A collector that returns **all** scored results. Sorts by score (descending
by default).

**Constructor:**
- `reverse`: If `True`, sort results in ascending order (lowest scores first).

### `UnsortedCollector`

```python
class whoosh.collectors.UnsortedCollector
```

A collector that returns results in document order (no sorting). Used when
the search weighting is `None`.

## Wrapping Collectors

### `WrappingCollector`

```python
class whoosh.collectors.WrappingCollector(child)
```

Base class for collectors that wrap other collectors. Delegates most
operations to the child collector while adding additional behavior.

**Constructor:**
- `child`: The collector to wrap.

**Methods** (all delegated to child):
`top_searcher`, `context`, `prepare`, `set_subsearcher`, `all_ids`,
`count`, `collect_matches`, `sort_key`, `collect`, `remove`, `matches`,
`finish`, `results()`

### `SortingCollector`

```python
class whoosh.collectors.SortingCollector(
    sortedby,
    limit=10,
    reverse=False
)
```

A collector that returns results sorted by a `FacetType` object.

**Constructor:**
- `sortedby`: A `FacetType` or field name to sort by.
- `limit`: Maximum number of results (0 for no limit).
- `reverse`: If `True`, reverse the overall sort order.

### `FilterCollector`

```python
class whoosh.collectors.FilterCollector(
    child,
    allow=None,
    restrict=None
)
```

A collector that allows and/or restricts certain document numbers in
results.

A document is discarded if:
- `allow` is set and the docnum is not in the allowed set, or
- `restrict` is set and the docnum is in the restricted set.

**Constructor:**
- `child`: The collector to wrap.
- `allow`: A query, `Results` object, or set-like of allowed docnums.
  `None` means everything is allowed.
- `restrict`: A query, `Results` object, or set-like of disallowed docnums.
  `None` means nothing is disallowed.

**Attributes:**
- `filtered_count`: Number of documents filtered out.

### `FacetCollector`

```python
class whoosh.collectors.FacetCollector(child, groupedby, maptype=None)
```

A collector that creates groups of documents based on facet objects. Used
when `groupedby` is specified in `Searcher.search()`.

**Constructor:**
- `child`: The collector to wrap.
- `groupedby`: A field name, `FacetType`, dict, or `Facets` object.
- `maptype`: Default `FacetMap` class for facets that don't specify one.

**Attributes:**
- `facetmaps`: Dictionary of facet name to `FacetMap` objects.

### `CollapseCollector`

```python
class whoosh.collectors.CollapseCollector(
    child,
    keyfacet,
    limit=1,
    order=None
)
```

A collector that eliminates all but the top N results sharing the same facet
key. Useful for "dedup" or grouped result views.

**Constructor:**
- `child`: The collector to wrap.
- `keyfacet`: A `FacetType` to collapse on. All but the top N documents
  sharing a key are eliminated.
- `limit`: Maximum documents to keep per key (default `1`).
- `order`: Optional `FacetType` to determine which documents are "top" within
  each group. Defaults to the results order (e.g., highest score).

**Attributes:**
- `collapsed_counts`: Dictionary mapping keys to the number of documents
  eliminated.

### `TimeLimitCollector`

```python
class whoosh.collectors.TimeLimitCollector(
    child,
    timelimit,
    greedy=False,
    use_alarm=True
)
```

A collector that raises a `TimeLimit` exception if the search exceeds a
time limit. Partial results are still available via `results()`.

**Constructor:**
- `child`: The collector to wrap.
- `timelimit`: Maximum search time in seconds.
- `greedy`: If `True`, finish adding the current hit before raising.
- `use_alarm`: If `True` (default), use `signal.SIGALRM` on Unix for
  immediate interruption. On Windows, time is only checked between
  documents.

```python
from whoosh.searching import TimeLimit

uc = collectors.UnlimitedCollector()
tlc = TimeLimitCollector(uc, timelimit=5.8)
try:
    searcher.search_with_collector(myquery, tlc)
except TimeLimit:
    print("Search timed out!")
# Still get partial results:
print(tlc.results())
```

### `TermsCollector`

```python
class whoosh.collectors.TermsCollector(child, settype=set)
```

A collector that records which terms appeared in which matched documents.
Used when `terms=True` in `Searcher.search()`.

**Constructor:**
- `child`: The collector to wrap.
- `settype`: Set type to use for docnum collections (default `set`).

**Attributes:**
- `termdocs`: Dict mapping `(fieldname, text)` tuples to arrays of docnums.
- `docterms`: Dict mapping docnums to lists of `(fieldname, text)` tuples.

## Exceptions

### `TimeLimit`

```python
from whoosh.searching import TimeLimit
```

Raised by `TimeLimitCollector` when the search exceeds the time limit.
Partial results are still available from the collector.


## DOCUMENT: Columns

# Columns API

Classes for storing per-document values (column-oriented storage) used for
fast sorting, faceting, and filtering. Columns are the mechanism by which
Whoosh stores field values alongside the inverted index, in a column-oriented
layout for efficient range access.

The default column type for most fields is `VarBytesColumn`, although numeric
and date fields use `NumericColumn`. Expert users may use other column types
that may be faster or more storage-efficient based on the field contents.

A `Column` object stores configuration information and provides two important
methods: `writer()` to return a `ColumnWriter` and `reader()` to return a
`ColumnReader`.

## Module Functions

### `bytes_column`

```python
whoosh.columns.bytes_column
```

A default `VarBytesColumn` instance used as the column type for string fields.

### `numeric_column`

```python
whoosh.columns.numeric_column
```

A default `NumericColumn` instance used as the column type for numeric fields.

## Base Classes

### `Column`

```python
class whoosh.columns.Column
```

Base class for all column types.

**Class Attributes:**
- `reversible (bool)`: Whether values can be reversed for descending sort.
  Default `False`.

**Methods:**
- `writer(dbfile)`: Returns a `ColumnWriter` for this column type.
- `reader(dbfile, basepos, length, doccount)`: Returns a `ColumnReader` for
  this column type.
- `default_value(reverse=False)`: Returns the default value for documents
  without a column value at index time.
- `stores_lists()`: Returns `True` if the column stores a list of values per
  document instead of a single value.

### `ColumnWriter`

```python
class whoosh.columns.ColumnWriter(dbfile)
```

Base class for writing column values to disk.

**Constructor:**
- `dbfile`: The `StructFile` to write to.

**Methods:**
- `fill(docnum)`: Fills any gap in docnums up to `docnum` with default values.
- `add(docnum, value)`: Adds a value for the given docnum.
- `finish(docnum)`: Called when done writing. Default does nothing.

### `ColumnReader`

```python
class whoosh.columns.ColumnReader(dbfile, basepos, length, doccount)
```

Base class for reading column values from disk.

**Constructor:**
- `dbfile`: The `StructFile` to read from.
- `basepos`: The offset within the file at which the column starts.
- `length`: The length in bytes the column occupies in the file.
- `doccount`: The number of rows (documents) in the column.

**Methods:**
- `__getitem__(docnum)`: Returns the value for the given docnum.
- `sort_key(docnum)`: Returns the value for sorting (defaults to
  `__getitem__`).
- `__iter__()`: Yields values for all documents.
- `load()`: Returns a list of all values.
- `set_reverse()`: Prepares the reader for reverse iteration.

## Concrete Column Types

### `VarBytesColumn`

```python
class whoosh.columns.VarBytesColumn(
    allow_offsets=True,
    write_offsets_cutoff=2**15
)
```

Stores variable-length byte strings. The default value for documents without
a value is `b''` (empty bytes).

**Constructor:**
- `allow_offsets`: Whether to write offsets for faster lookup when there are
  many rows. Default `True`.
- `write_offsets_cutoff`: Write offsets when there are more than this many
  rows (default `2**15`).

### `FixedBytesColumn`

```python
class whoosh.columns.FixedBytesColumn(blocksize, default=emptybytes)
```

Stores fixed-length byte strings, saving space by not storing the length of
each value.

**Constructor:**
- `blocksize`: Fixed size of each value in bytes.
- `default`: Default value for documents without a value.

### `RefBytesColumn`

```python
class whoosh.columns.RefBytesColumn(
    cachesize=1000,
    stable=True,
    default=emptybytes
)
```

Stores references to unique values rather than the values themselves, saving
space when the field has few unique values. Uses a `DocIdSet` to track which
documents contain each value.

**Constructor:**
- `cachesize`: Size of the LRU cache for value lookups (default `1000`).
- `stable`: Whether to use a stable sort of references (default `True`).
- `default`: Default value for missing documents.

### `NumericColumn`

```python
class whoosh.columns.NumericColumn(
    typecode,
    default=None,
    nullable=False
)
```

Stores numbers (int, float, datetime) encoded as binary values. Extends
`FixedBytesColumn`.

**Constructor:**
- `typecode`: A `struct` typecode string (e.g., `"I"` for unsigned int,
  `"q"` for long, `"d"` for float).
- `default`: Default numeric value (None for the type's zero value).
- `nullable`: Whether `None` values are allowed.

### `BitColumn`

```python
class whoosh.columns.BitColumn
```

Stores boolean values as a bitmap. Each value is either `True` (1) or
`False` (0). Uses a `BitSet` internally.

### `CompressedBytesColumn`

```python
class whoosh.columns.CompressedBytesColumn(default=emptybytes)
```

Wraps a `VarBytesColumn` with zlib compression for the value bytes.

### `CompressedBlockColumn`

```python
class whoosh.columns.CompressedBlockColumn
```

Stores values with block-level zlib compression. More efficient for large
columns.

### `StructColumn`

```python
class whoosh.columns.StructColumn(struct, name)
```

Wraps a `FixedBytesColumn` to store structured binary data (e.g., tuples
encoded with `struct`).

**Constructor:**
- `struct`: A `struct.Struct` object defining the format.
- `name`: Field name for error messages.

### `EmptyColumnReader`

```python
class whoosh.columns.EmptyColumnReader(default, doccount)
```

A `ColumnReader` that returns a constant default value for every document.
Used when a field has no column.

### `MultiColumnReader`

```python
class whoosh.columns.MultiColumnReader(readers)
```

Combines multiple `ColumnReader` instances into one for multi-segment indices.

**Constructor:**
- `readers`: List of `ColumnReader` instances (one per segment).

### `TranslatingColumnReader`

```python
class whoosh.columns.TranslatingColumnReader(child, translator)
```

Wraps a `ColumnReader` to apply a translation function to the values.

**Constructor:**
- `child`: The underlying `ColumnReader`.
- `translator`: Function that maps sort keys to human-readable values.

### `WrappedColumn`

```python
class whoosh.columns.WrappedColumn(child)
```

Base class for column wrappers that adapt another column type.

### `WrappedColumnWriter`

```python
class whoosh.columns.WrappedColumnWriter(child)
```

Base class for column writer wrappers.

### `WrappedColumnReader`

```python
class whoosh.columns.WrappedColumnReader(child)
```

Base class for column reader wrappers.

### `ClampedNumericColumn`

```python
class whoosh.columns.ClampedNumericColumn(child, clampfn)
```

Wraps a `NumericColumn` to clamp values to a valid range before sorting.

**Constructor:**
- `child`: The wrapped `NumericColumn`.
- `clampfn`: Function that clamps a value to the valid range.

### `PickleColumn`

```python
class whoosh.columns.PickleColumn(child, ...)
```

Wraps another column to store pickled Python objects.

### `ListColumn`

```python
class whoosh.columns.ListColumn(child)
```

Base class for columns that store multiple values per document.

### `ListColumnReader`

```python
class whoosh.columns.ListColumnReader(child)
```

Reader for list-valued columns.

### `VarBytesListColumn`

```python
class whoosh.columns.VarBytesListColumn
```

A `ListColumn` variant of `VarBytesColumn` that stores lists of byte strings.

### `FixedBytesListColumn`

```python
class whoosh.columns.FixedBytesListColumn(blocksize)
```

A `ListColumn` variant of `FixedBytesColumn` that stores lists of fixed-size
byte strings.


## DOCUMENT: Core

# Core API

The core module provides the main `Index` class and related functions for managing indexes.

## Functions

### create_in

```python
whoosh.index.create_in(
    dirname: str,
    schema: Schema,
    indexname: str = "MAIN",
    create: bool = True,
    **kwargs
) -> FileIndex
```

Create a new index in the given directory.

**Args:**
- `dirname (str)`: Path to the directory where the index will be stored.
- `schema (Schema)`: The `Schema` object defining the index fields.
- `indexname (str)`: Name of the index. Allows multiple indexes in the same directory.
- `create (bool)`: If True, create the index even if it already exists (clears existing).

**Returns:**
- `FileIndex`: A new index object.

**Example:**
```python
from whoosh.index import create_in
from whoosh.fields import Schema, TEXT

schema = Schema(title=TEXT(stored=True), content=TEXT)
index = create_in("indexdir", schema)
```

---

### open_dir

```python
whoosh.index.open_dir(
    dirname: str,
    indexname: str = "MAIN",
    readonly: bool = False,
    **kwargs
) -> FileIndex
```

Open an existing index.

**Args:**
- `dirname (str)`: Path to the index directory.
- `indexname (str)`: Name of the index to open.
- `readonly (bool)`: If True, open in read-only mode.

**Returns:**
- `FileIndex`: An index object.

**Example:**
```python
from whoosh.index import open_dir
index = open_dir("indexdir")
```

---

### exists_in

```python
whoosh.index.exists_in(
    dirname: str,
    indexname: str = "MAIN",
    **kwargs
) -> bool
```

Check if a valid index exists in the given directory.

**Returns:**
- `bool`: True if the index exists.

---

### create_index

```python
whoosh.index.create_index(
    schema: Schema,
    storage: Storage,
    indexname: str = "MAIN",
    create: bool = True,
    **kwargs
) -> Index
```

Low-level index creation. Use `create_in` instead unless you need custom storage.

---

### open_index

```python
whoosh.index.open_index(
    storage: Storage,
    indexname: str = "MAIN",
    readonly: bool = False,
    **kwargs
) -> Index
```

Low-level index opening. Use `open_dir` instead unless you need custom storage.

## Classes

### Index (Base Class)

```python
class whoosh.index.Index
```

Abstract base class for index objects. Provides common methods for reading and writing.

**Methods:**

#### `writer()`

```python
writer = ix.writer(
    timeout: float = 0.0,
    delay: float = 0.1,
    limitmb: int = 128,
    **kwargs
) -> IndexWriter
```

Return a writer for this index.

**Args:**
- `timeout (float)`: Max seconds to wait for write lock.
- `delay (float)`: Seconds between lock retries.
- `limitmb (int)`: Maximum size of posting pool runs.

**Returns:**
- `IndexWriter`: A writer object.

**Example:**
```python
writer = ix.writer()
writer.add_document(title="Hello", content="World")
writer.commit()
```

---

#### `searcher()`

```python
searcher = ix.searcher(
    weighting: WeightingModel = None,
    **kwargs
) -> Searcher
```

Return a searcher for the current index state.

**Returns:**
- `Searcher`: A searcher object.

**Example:**
```python
with ix.searcher() as searcher:
    results = searcher.search("query")
```

---

#### `reader()`

```python
reader = ix.reader() -> IndexReader
```

Return a reader for the current index state.

---

#### `commit()`

```python
ix.commit(mergetype=None, optimize=None, merge=None)
```

Convenience method: create a writer, call commit, and close.

---

#### `optimize()`

```python
ix.optimize()
```

Merge all segments into a single segment.

---

#### `add_field()`

```python
ix.add_field(fieldname: str, fieldtype, **kwargs)
```

Add a field to the index schema.

---

#### `remove_field()`

```python
ix.remove_field(fieldname: str, **kwargs)
```

Remove a field from the index schema.

---

#### `doc_count()`

```python
count = ix.doc_count() -> int
```

Return the number of documents in the index.

---

#### `doc_count_all()`

```python
count = ix.doc_count_all() -> int
```

Return the total number of documents (including deleted).

---

#### `lock()`

```python
lock = ix.lock(name: str) -> Lock
```

Acquire a named lock on the index.

## FileIndex

The concrete implementation returned by `create_in` and `open_dir`.

All `Index` methods are available. Additional methods:

### `_read_toc()`

Read the table of contents.

### `_write_toc()`

Write the table of contents.

## Exceptions

### LockError

Raised when the index is locked by another writer.

```python
from whoosh.index import LockError

try:
    writer = ix.writer(timeout=5.0)
except LockError:
    print("Index is locked, try again later")
```

### IndexMissingError

Raised when trying to open a non-existent index.

## Constants

### IndexVersion

Current index format version.

---

# Index API

## Index

```python
class whoosh.index.Index
```

Base index class providing reading and writing access.

### Methods

- `writer(**kwargs)` -> `IndexWriter`
- `searcher(**kwargs)` -> `Searcher`
- `reader()` -> `IndexReader`
- `commit(mergetype=None, optimize=None, merge=None)`
- `optimize()`
- `add_field(fieldname, fieldtype, **kwargs)`
- `remove_field(fieldname, **kwargs)`
- `doc_count() -> int`
- `doc_count_all() -> int`
- `lock(name) -> Lock`

## IndexingError

```python
class whoosh.writing.IndexingError(Exception)
```

Raised when an indexing operation fails.

## Exceptions

```python
class whoosh.index.LockError(Exception)
class whoosh.index.IndexMissingError(Exception)
```


## DOCUMENT: Events

# Event Bus & Hooks API

Loose coupling through events and lightweight hooks.

## Event Bus

```python
class whoosh.event_bus.EventBus
```

Publish/subscribe event system supporting both synchronous and asynchronous
listeners. The module-level singleton `event_bus` is available for use:

```python
from whoosh.event_bus import event_bus
```

### Methods

#### `subscribe()`

```python
@event_bus.subscribe(DocumentIndexed)
async def handler(event: DocumentIndexed):
    print(f"Indexed document: {event.document_id}")

# Or without decorator
event_bus.subscribe(DocumentIndexed)(handler)
```

Register a handler function (synchronous or async) for a specific event type.
The decorator takes the event class as an argument. Returns the handler
unchanged so it can be used normally.

---

#### `publish()`

```python
from whoosh.event_bus import event_bus, DocumentIndexed

event_bus.publish(DocumentIndexed(document_id="doc123"))
```

Publish an event to all subscribers. If listeners are async coroutines and no
event loop is running, they are executed via `asyncio.run()`. If an event
loop is running, tasks are scheduled on it. Exceptions in listeners are
swallowed.

---

#### `clear()`

```python
event_bus.clear()
```

Remove all subscribers.

---

## Events

Events are immutable dataclasses.

### `Event`

```python
@dataclass(frozen=True)
class Event:
    pass
```

Base class for all events.

---

### `DocumentIndexed`

```python
@dataclass(frozen=True)
class DocumentIndexed(Event):
    document_id: str
```

Published when a document is indexed. Contains the document ID.

---

### `SearchExecuted`

```python
@dataclass(frozen=True)
class SearchExecuted(Event):
    query: str
```

Published after a search is executed. Contains the query string.

---

## Hooks

Hook system for cross-cutting concerns. Hooks are registered globally using a
module-level registry.

### `hookimpl`

```python
from whoosh.hooks import hookimpl

@hookimpl
def before_search(context):
    context.query = optimize_query(context.query)
    return context
```

Decorator that marks a function as a hook implementation. Returns a `HookImpl`
wrapper.

### `register_hook()`

```python
from whoosh.hooks import register_hook, hookimpl

@hookimpl
def before_search(context):
    ...

register_hook("before_search", before_search)
```

Register a `HookImpl` under a named hook. Multiple hooks can be registered
per name; they are called in registration order.

### `call_hook()`

```python
from whoosh.hooks import call_hook

results = await call_hook("before_search", context)
```

Async function that calls all hooks registered under the given name. Returns
a list of results from each hook's execution. Exceptions in individual hooks
are logged but do not stop execution.

---
## Example: Event Bus

```python
from whoosh.event_bus import event_bus, DocumentIndexed, SearchExecuted

@event_bus.subscribe(DocumentIndexed)
async def on_document_indexed(event: DocumentIndexed):
    print(f"Document indexed: {event.document_id}")

@event_bus.subscribe(SearchExecuted)
async def on_search_executed(event: SearchExecuted):
    print(f"Search executed: {event.query}")

# Publish events
event_bus.publish(DocumentIndexed(document_id="doc123"))
event_bus.publish(SearchExecuted(query="hello world"))
```

## Example: Hooks

```python
from whoosh.hooks import hookimpl, register_hook, call_hook
import asyncio

@hookimpl
def before_search(context):
    print(f"Searching for: {context['query']}")
    return context

register_hook("before_search", before_search)

# Call hooks
context = {"query": "hello"}
results = asyncio.run(call_hook("before_search", context))
```


## DOCUMENT: Fields

# Fields API

Define the structure of your index with field types.

## Schema

```python
class whoosh.fields.Schema
```

The `Schema` class defines the fields available in an index.

### Constructor

```python
schema = Schema(
    title=TEXT(stored=True),
    content=TEXT,
    path=ID(stored=True, unique=True),
    tags=KEYWORD(lowercase=True),
    rating=NUMERIC(float, stored=True),
    published=DATETIME(stored=True),
    active=BOOLEAN
)
```

### Methods

#### `add()`

```python
schema.add(
    fieldname: str,
    fieldtype,
    glob: bool = False,
    **kwargs
)
```

Add a field to the schema. If `glob=True`, the fieldname is treated as a glob pattern.

#### `remove()`

```python
schema.remove(fieldname: str, **kwargs)
```

Remove a field from the schema.

#### `items()`

```python
for name, field in schema.items():
    print(name, field)
```

Return a list of (fieldname, field object) pairs.

#### `names()`

```python
names = schema.names()
```

Return a list of field names.

## FieldType Base Class

```python
class whoosh.fields.FieldType
```

Base class for all field types.

### Attributes

| Attribute | Type | Description |
|-----------|------|-------------|
| `format` | `Format` | Defines how the field is indexed |
| `vector` | `Format` or None | Optional per-document vector format |
| `scorable` | `bool` | Whether field length is stored (for BM25F) |
| `stored` | `bool` | Whether field value is stored in index |
| `unique` | `bool` | Whether field uniquely identifies documents |

### Methods

#### `index()`

Convert a value into indexed items.

#### `indexable()`

Check if the value can be indexed.

#### `spelling_fieldname()`

Return the field name used for spelling data.

#### `spellable_words()`

Generate spellable words from a value.

## Built-in Field Types

### TEXT

```python
whoosh.fields.TEXT(
    stored: bool = False,
    unique: bool = False,
    phrase: bool = True,
    analyzer: Analyzer = None,
    field_boost: float = 1.0,
    **kwargs
)
```

Full-text field with tokenization and optional phrase search.

**Example:**
```python
title = TEXT(stored=True)
body = TEXT(analyzer=StemmingAnalyzer(), phrase=False)
```

---

### ID

```python
whoosh.fields.ID(
    stored: bool = False,
    unique: bool = False,
    field_boost: float = 1.0,
    **kwargs
)
```

Untokenized identifier field. Stores the entire value as a single term.

**Example:**
```python
path = ID(stored=True, unique=True)
slug = ID(stored=True)
```

---

### KEYWORD

```python
whoosh.fields.KEYWORD(
    stored: bool = False,
    lowercase: bool = False,
    commas: bool = False,
    scorable: bool = False,
    field_boost: float = 1.0,
    **kwargs
)
```

Space or comma-separated keywords. Phrase search is not supported.

**Example:**
```python
tags = KEYWORD(lowercase=True, commas=True, stored=True)
```

---

### STORED

```python
whoosh.fields.STORED(
    stored: bool = True,
    unique: bool = False,
    **kwargs
)
```

Stored-only field. Not indexed or searchable.

**Example:**
```python
icon = STORED()
description = STORED()
```

---

### NUMERIC

```python
whoosh.fields.NUMERIC(
    numtype: type = int,
    stored: bool = False,
    unique: bool = False,
    field_boost: float = 1.0,
    **kwargs
)
```

Numeric field for integers or floats.

**Example:**
```python
rating = NUMERIC(float, stored=True)
count = NUMERIC(int)
price = NUMERIC(float, stored=True, sortable=True)
```

---

### DATETIME

```python
whoosh.fields.DATETIME(
    stored: bool = False,
    unique: bool = False,
    field_boost: float = 1.0,
    **kwargs
)
```

Date/time field. Stores `datetime` objects.

**Example:**
```python
published = DATETIME(stored=True)
updated = DATETIME()
```

---

### BOOLEAN

```python
whoosh.fields.BOOLEAN(
    stored: bool = False,
    unique: bool = False,
    field_boost: float = 1.0,
    **kwargs
)
```

Boolean field. Searchable with `yes`, `no`, `true`, `false`, `1`, `0`, `t`, `f`.

**Example:**
```python
published = BOOLEAN(stored=True)
```

---

### NGRAM

```python
whoosh.fields.NGRAM(
    minsize: int = 2,
    maxsize: int = 5,
    stored: bool = False,
    field_boost: float = 1.0,
    **kwargs
)
```

Character n-gram field.

---

### NGRAMWORDS

```python
whoosh.fields.NGRAMWORDS(
    minsize: int = 2,
    maxsize: int = 5,
    stored: bool = False,
    field_boost: float = 1.0,
    **kwargs
)
```

Word-level n-gram field.

---

### VectorField

```python
whoosh.fields.VectorField(
    dimensions: int,
    metric: str = "cosine",
    provider: str = "numpy",
    stored: bool = False,
    **kwargs
)
```

Field for storing and searching vector embeddings.

**Args:**
- `dimensions (int)`: Embedding dimension (e.g., 384 for all-MiniLM-L6-v2).
- `metric (str)`: Similarity metric: `"cosine"`, `"euclidean"`, `"dot"`.
- `provider (str)`: Vector provider name from registry.

**Example:**
```python
embedding = VectorField(dimensions=384, metric="cosine", stored=True)
```

## SchemaBuilder

Fluent API for building schemas:

```python
from whoosh.fields import SchemaBuilder

schema = (
    SchemaBuilder()
    .field("title", TEXT(stored=True))
    .field("path", ID(stored=True, unique=True))
    .field("content", TEXT)
    .field("tags", KEYWORD(lowercase=True))
    .field("published", DATETIME(stored=True))
    .build()
)
```

## Constants

- `whoosh.fields.STORED`: Stored-only field type
- `whoosh.fields.TEXT`: Full-text field
- `whoosh.fields.ID`: Identifier field
- `whoosh.fields.KEYWORD`: Keyword field
- `whoosh.fields.NUMERIC`: Numeric field
- `whoosh.fields.DATETIME`: Date/time field
- `whoosh.fields.BOOLEAN`: Boolean field


## DOCUMENT: Filedb Storage

# File DB / Storage API

Classes for storing and retrieving index data on disk or in memory. The
`Storage` class is the main entry point for persisting an index.

## Storage Classes

### `Storage`

```python
class whoosh.filedb.filestore.Storage(path=None)
```

Abstract base class for storage backends. A `Storage` manages a filesystem-
or memory-based location where index files can be created, read, and
manipulated.

**Constructor:**
- `path`: Optional path string. Subclasses may use this to set the storage
  location.

**Methods:**

#### `create_file(name, **kwargs)`

Creates and returns a file object for writing.

#### `open_file(name, **kwargs)`

Opens and returns a file object for reading.

#### `list()`

Returns a list of all filenames in this storage.

#### `exists(name)`

Returns `True` if a file/named item exists in the storage.

#### `file_exists(name)`

Alias for `exists()`.

#### `file_length(name)`

Returns the length of file `name` in bytes.

#### `rename(src, dst)`

Renames a file from `src` to `dst`.

#### `delete_file(name)`

Deletes file `name` from storage.

#### `destroy()`

Deletes all files and the storage itself.

#### `temp_storage()`

Creates and returns a temporary isolated `Storage` for scratch space.

#### `supports_mmap`

Returns `True` if this storage supports memory-mapped file access.

**Properties:**
- `schema`: The `Schema` for this storage (if it holds an index).
- `lock`: The lock object used for this storage.

### `FileStorage`

```python
class whoosh.filedb.filestore.FileStorage(
    path,
    cachesize_limit=40,
    supports_mmap=None,
    **kwargs
)
```

A `Storage` subclass that uses the operating system's filesystem.

**Constructor:**
- `path`: A `Path` (or string path) to the directory where files are stored.
- `cachesize_limit`: Maximum number of open file handles to cache.
- `supports_mmap`: If `None`, auto-detected; otherwise force enable/disable.

**Methods:** All `Storage` methods plus:
- `create_index(schema, indexname="index", ...)`: Creates and returns a new
  `Index` object.
- `open_index(indexname="index", ...)`: Opens an existing `Index`.
- `lock(name)`: Returns a lock object for the given lock name.

### `RamStorage`

```python
class whoosh.filedb.filestore.RamStorage(cachesize_limit=10)
```

A `Storage` subclass that keeps all files in memory as bytes. Useful for
testing and small indexes.

**Constructor:**
- `cachesize_limit`: Maximum number of files to cache as decoded objects.

**Methods:** All `Storage` methods plus:
- `create_index(schema, ...)`: Creates an in-memory `Index`.
- `save_to_file(filename, ...)`: Saves the entire storage to a file.
- `load_from_file(filename, ...)`: Loads storage contents from a file.

### `OverlayStorage`

```python
class whoosh.filedb.filestore.OverlayStorage(base, overlay)
```

A `Storage` wrapper that presents two storage layers: a base and an overlay.
Files in the overlay take precedence over the base.

**Constructor:**
- `base`: The base `Storage` (e.g., read-only original).
- `overlay`: The overlay `Storage` (e.g., writable copy).

## Storage Exceptions

### `StorageError`

```python
class whoosh.filedb.filestore.StorageError
```

Base exception for storage-related errors.

### `ReadOnlyError`

```python
class whoosh.filedb.filestore.ReadOnlyError(StorageError)
```

Raised when attempting to write to a read-only storage.

## File Tables

### `HashWriter`

```python
class whoosh.filedb.filetables.HashWriter(dbfile, keycoder=None, keydecoder=None, data_encoder=None, data_decoder=None, **kwargs)
```

Writes key-value pairs to a file, with optional indexing by key.

**Constructor:**
- `dbfile`: The `StructFile` to write to.
- `keycoder`: Function to encode keys for storage.
- `keydecoder`: Function to decode keys from storage.
- `data_encoder`: Function to encode values.
- `data_decoder`: Function to decode values.

### `HashReader`

```python
class whoosh.filedb.filetables.HashReader(dbfile, length, keycoder=None, keydecoder=None, data_decoder=None, **kwargs)
```

Reads key-value pairs from a file written by `HashWriter`.

**Constructor:**
- `dbfile`: The `StructFile` to read from.
- `length`: Length of the data section.
- `keycoder`/`keydecoder`/`data_decoder`: Same as `HashWriter`.

**Methods:**
- `__getitem__(key)`: Returns the value for `key`.
- `keys()`: Yields all keys.
- `values()`: Yields all values.
- `items()`: Yields `(key, value)` pairs.
- `keys_from(prefixbytes)`: Yields keys starting at `prefixbytes`.
- `items_from(prefixbytes)`: Yields `(key, value)` pairs starting at prefix.
- `closest_key_pos(key)`: Returns the position of the closest matching key.
- `range_for_key(key)`: Returns `(startpos, endpos)` for a key range.

### `OrderedHashWriter`

```python
class whoosh.filedb.filetables.OrderedHashWriter(HashWriter)
```

A `HashWriter` that maintains keys in sorted order.

### `OrderedHashReader`

```python
class whoosh.filedb.filetables.OrderedHashReader(HashReader)`

A `HashReader` for reading data written by `OrderedHashWriter`. Preserves
key ordering for efficient prefix iteration.

### `FieldedOrderedHashWriter`

```python
class whoosh.filedb.filetables.FieldedOrderedHashWriter(HashWriter)
```

An `OrderedHashWriter` that stores an extra "fieldmap" in the extras dict,
mapping field names to numeric IDs.

### `FieldedOrderedHashReader`

```python
class whoosh.filedb.filetables.FieldedOrderedHashReader(HashReader)
```

Reader for data written by `FieldedOrderedHashWriter`.

## Struct File

### `StructFile`

```python
class whoosh.filedb.structfile.StructFile(name, source, cachesize_limit=40)
```

Wraps a file object and adds methods for reading/writing packed binary
values, arrays, varints, and pickle objects.

**Methods include:**
- `read_int()`, `write_int(n)`: Read/write a 4-byte signed integer.
- `read_long()`, `write_long(n)`: Read/write a 8-byte signed integer.
- `read_uint()`, `write_uint(n)`: Read/write unsigned int.
- `read_ulong()`, `write_ulong(n)`: Read/write unsigned long.
- `read_float()`, `write_float(n)`: Read/write a float.
- `read_ushort()`, `write_ushort(n)`: Read/write unsigned short.
- `read_byte()`, `write_byte(b)`: Read/write a single byte.
- `write_array(arr)`: Write an array of values.
- `get_array(offset, typecode, length)`: Read an array from offset.
- `write_pickle(obj)`: Pickle and write an object.
- `read_pickle()`: Read and unpickle an object.
- `get(offset, length)`: Read `length` bytes from `offset`.
- `get_int()`, `get_uint()`, `get_long()`, `get_float()`, `get_byte()`:
  Read a single value from the given offset.

### `BufferFile`

```python
class whoosh.filedb.structfile.BufferFile
```

A `StructFile` that wraps an in-memory byte buffer.

### `ChecksumFile`

```python
class whoosh.filedb.structfile.ChecksumFile(dbfile)
```

A `StructFile` wrapper that computes a checksum as data is written, for
integrity verification.

## Compound Storage

### `CompoundStorage`

```python
class whoosh.filedb.compound.CompoundStorage(dbfile, use_mmap=True)
```

Treats a single file as a container for multiple sub-files. Used for compound
segment files.

**Methods:**
- `create_file(name)`: Create a sub-file within the compound file.
- `open_file(name)`: Open a sub-file for reading.
- `list()`: List all sub-file names.
- `close()`: Close the compound storage.

### `SubFile`

```python
class whoosh.filedb.compound.SubFile
```

A file-like object representing a sub-file within a `CompoundStorage`.

### `CompoundWriter`

```python
class whoosh.filedb.compound.CompoundWriter(storage)
```

Writes a compound file by assembling multiple files from a storage.

**Methods:**
- `create_file(name)`: Reserve a filename in the compound file.
- `save_as_files(dest_storage, fn_generator)`: Assemble the compound file
  from source files into the destination storage.

## Storage Utility Functions

### `copy_storage`

```python
whoosh.filedb.filestore.copy_storage(sourcestore, deststore)
```

Copies all files from one storage to another.

### `copy_to_ram`

```python
whoosh.filedb.filestore.copy_to_ram(storage)
```

Reads all files from a storage into a `RamStorage` and returns it.


## DOCUMENT: Formats

# Formats API

Classes that control how posting information (frequencies, positions,
character offsets, and weights) is encoded and stored for each field in the
index. The `Format` object is a factory and encoder/decoder for the
value strings stored alongside each posting.

## Module Functions

### `tokens`

```python
whoosh.formats.tokens(value, analyzer, kwargs)
```

Takes a text `value` and an `analyzer`, runs the analyzer on the value, and
returns the resulting token generator (wrapped with `unstopped()` to ignore
`STOP` tokens). Used internally by `Format.word_values()`.

## Format Classes

All format classes accept a `field_boost` parameter (default `1.0`) that
scales the score of all queries matching terms in that field.

### `Format`

```python
class whoosh.formats.Format(field_boost=1.0, **options)
```

Abstract base class for all posting formats. Format objects are
field-level objects: one is created per `Field` and shared across all
postings for that field.

**Attributes:**
- `posting_size (int)`: Fixed byte size of encoded postings, or `None`/`-1`
  if variable-size.
- `textual (bool)`: Whether this format expects string tokens (vs. bytes).
  Default `True`.

**Methods:**

#### `word_values(value, analyzer, **kwargs)`

Abstract. Takes a text value, runs it through the analyzer, and yields
`(tokentext, frequency, weight, valuestring)` tuples.

#### `encode(value)`

Abstract. Encodes raw posting data into the value string bytes.

#### `decode_frequency(valuestring)`

Abstract. Decodes the frequency (term count in document) from the value
string.

#### `decode_weight(valuestring)`

Abstract. Decodes the weight (total boost contribution) from the value string.

#### `combine(valuestrings)`

Abstract. Combines multiple value strings (from overlapping segments) into
a single value string.

#### `supports(name)`

Returns `True` if this format supports interpreting its postings as `name`
(e.g., `"frequency"`, `"positions"`, `"characters"`, `"position_boosts"`,
`"character_boosts"`). Equivalent to `hasattr(self, "decode_" + name)`.

#### `decoder(name)`

Returns the `decode_<name>` method for the given attribute name.

#### `decode_as(astype, valuestring)`

Calls the appropriate `decode_<astype>` method on `valuestring` and returns
the result.

#### `fixed_value_size()`

Returns `self.posting_size` if positive, otherwise `None`.

#### `__eq__(other)`

Returns `True` if `other` is the same class with equal `__dict__`.

### `Existence`

```python
class whoosh.formats.Existence(field_boost=1.0, **options)
```

Indexes only whether a term occurred in a document—not its frequency or
positions. Useful for non-scorable fields like paths.

- `posting_size = 0`
- Supports: `frequency` (always 1), `weight` (always `field_boost`)
- `encode()` returns empty bytes

### `Frequency`

```python
class whoosh.formats.Frequency(field_boost=1.0, boost_as_freq=False, **options)
```

Stores term frequency information (term count per document) for each posting.

- `posting_size = _INT_SIZE` (4 bytes)
- Supports: `frequency`, `weight`
- `encode()` encodes the count as a packed unsigned int
- `boost_as_freq`: If `True`, boosts are interpreted as frequency boosts

```python
from whoosh.formats import Frequency
fmt = Frequency(field_boost=1.0)
```

### `Positions`

```python
class whoosh.formats.Positions(field_boost=1.0, **options)
```

Stores position information (term offsets within the document) in each
posting, enabling phrase queries and "near" queries.

- Supports: `frequency`, `weight`, `positions`, `position_boosts`
- `encode(poslist)` encodes positions using variable-length delta encoding
- Positions are stored as delta-encoded variable-length integers

```python
from whoosh.formats import Positions
fmt = Positions()
```

### `Characters`

```python
class whoosh.formats.Characters(field_boost=1.0, **options)
```

Extends `Positions` to also store character start and end offsets for each
term occurrence, enabling character-precise highlighting.

- Supports: `frequency`, `weight`, `positions`, `position_boosts`,
  `characters`
- `encode()` encodes (position, startchar, endchar) triples with delta
  encoding

### `PositionBoosts`

```python
class whoosh.formats.PositionBoosts(field_boost=1.0, **options)
```

Extends `Positions` to store per-position boost values in addition to
positions.

- Supports: `frequency`, `weight`, `positions`, `position_boosts`
- `encode()` encodes `(position, boost)` pairs

### `CharacterBoosts`

```python
class whoosh.formats.CharacterBoosts(field_boost=1.0, **options)
```

Extends `Characters` to store per-position boost values along with
character offsets.

- Supports: `frequency`, `weight`, `positions`, `position_boosts`,
  `characters`, `character_boosts`
- `encode()` encodes `(position, startchar, endchar, boost)` tuples


## DOCUMENT: Highlight

# Highlight API

Classes and functions for highlighting matches in search result fragments.
The highlight module is a refactored package exposing the same public API as
the former monolithic module.

## Overview

The highlighting system has four components:

- **Fragmenters** split text into fragments.
- **Fragment Scorers** score fragments to determine which to display.
- **Formatters** render fragments as output (HTML, plain text, etc.).
- **Highlighter** ties these together and is used by `Searcher.highlights()`.

## Module-level Functions

### `highlight`

```python
whoosh.highlight.highlight(
    text: str,
    terms: list[str],
    analyzer,
    fragmenter,
    formatter,
    top: int = 3,
    scorer=None,
    minscore: int = 1,
    order=SCORE,
    mode: str = "query"
) -> str
```

Highlights the matched terms in `text` and returns a formatted string.

- `text`: The text to highlight.
- `terms`: A list of matched terms (strings).
- `analyzer`: The analyzer for the field.
- `fragmenter`: A `Fragmenter` instance or class.
- `formatter`: A `Formatter` instance or class.
- `top`: Maximum number of fragments to return.
- `scorer`: Optional fragment scorer (defaults to `BasicFragmentScorer`).
- `minscore`: Minimum score for a fragment to be included.
- `order`: Sort order for fragments (`FIRST`, `SCORE`, `LONGER`, `SHORTER`).
- `mode`: Analysis mode, typically `"query"` or `"index"`.

### `mkfrag`

```python
whoosh.highlight.mkfrag(
    text: str,
    tokens,
    startchar=None,
    endchar=None,
    charsbefore: int = 0,
    charsafter: int = 0
) -> Fragment
```

Returns a `Fragment` object based on `Token` objects in `tokens`.

### `get_text`

```python
whoosh.highlight.get_text(
    original: str,
    token,
    replace: bool
) -> str
```

Returns the text to use for a match when formatting. If `replace` is `False`,
returns the original text between `token.startchar` and `token.endchar`. If
`True`, returns `token.text`.

### `set_matched_filter`

```python
whoosh.highlight.set_matched_filter(
    tokens,
    termset: frozenset
) -> Iterator[Token]
```

Marks tokens as matched if their `text` attribute is in `termset`. Used for
phrase-agnostic highlighting.

### `set_matched_filter_phrases`

```python
whoosh.highlight.set_matched_filter_phrases(
    tokens,
    text: str,
    terms,
    phrases
) -> Iterator[Token]
```

Marks tokens as matched using phrase-aware logic. Highlights only tokens that
are part of matched phrases.

### `top_fragments`

```python
whoosh.highlight.top_fragments(
    fragments,
    count: int,
    scorer,
    order,
    minscore: int = 1
) -> list[Fragment]
```

Returns the best `count` fragments sorted by `order`, filtered by `minscore`.

## Constants

### `DEFAULT_CHARLIMIT`

```python
whoosh.highlight.DEFAULT_CHARLIMIT = 2**15
```

Default character limit for fragments.

### Sort Order Constants

```python
whoosh.highlight.FIRST   # Sort passages from earlier in the document first
whoosh.highlight.SCORE   # Sort higher scored passages first
whoosh.highlight.LONGER  # Sort longer passages first
whoosh.highlight.SHORTER # Sort shorter passages first
```

## Formatters

### `Formatter`

```python
class whoosh.highlight.Formatter
```

Base class for formatters. Subclasses implement `format_token()` to define
how matched tokens are rendered.

**Methods:**

- `format_token(text, token, replace=False)`: Returns formatted text for a
  matched token.
- `format_fragment(fragment, replace=False)`: Returns formatted text for a
  `Fragment`.
- `format(fragments, replace=False)`: Returns formatted text for a list of
  fragments, joined by `between`.

**Attributes:**
- `between`: String inserted between formatted fragments (default `"..."`).

### `NullFormatter`

```python
class whoosh.highlight.NullFormatter(Formatter)
```

A formatter that does not modify the string. Returns fragments unformatted.

### `UppercaseFormatter`

```python
class whoosh.highlight.UppercaseFormatter(between="...")
```

Formats matched terms in uppercase.

### `HtmlFormatter`

```python
class whoosh.highlight.HtmlFormatter(
    tagname="strong",
    between="...",
    classname="match",
    termclass="term",
    maxclasses=5,
    attrquote='"'
)
```

Wraps matched terms in HTML tags with CSS class names. Two classes are
applied to each match: `classname` (same for all matches) and `termclass`
(different for each term, e.g. `term0`, `term1`).

- `tagname`: The HTML tag to wrap matches (default `"strong"`).
- `between`: Text inserted between fragments.
- `classname`: CSS class applied to all matched term tags.
- `termclass`: CSS class prefix for per-term classes.
- `maxclasses`: Maximum number of distinct per-term class numbers.
- `attrquote`: Quote character for attribute values.

**Methods:**
- `clean()`: Clears the internal term-to-classname mapping dictionary.

### `GenshiFormatter`

```python
class whoosh.highlight.GenshiFormatter(qname="strong", between="...")
```

Formats matched terms as Genshi event streams (requires the Genshi library).

## Fragmenters

### `Fragmenter`

```python
class whoosh.highlight.Fragmenter
```

Base class for fragmenters. Subclasses implement `fragment_tokens()` and/or
`fragment_matches()`.

**Methods:**
- `must_retokenize()`: Returns `True` if this fragmenter needs to re-tokenize
  the text (calls `fragment_tokens` with all tokens). Returns `False` if it can
  work from matched token positions alone (calls `fragment_matches`).

### `WholeFragmenter`

```python
class whoosh.highlight.WholeFragmenter(charlimit=DEFAULT_CHARLIMIT)
```

Does not fragment text. Returns the entire text as one fragment. Useful for
highlighting short fields.

```python
results.fragmenter = WholeFragmenter()
```

### `SentenceFragmenter`

```python
class whoosh.highlight.SentenceFragmenter(
    maxchars: int = 200,
    sentencechars=".!?",
    charlimit=DEFAULT_CHARLIMIT
)
```

Breaks text at sentence-ending punctuation (`.`, `!`, `?`).

- `maxchars`: Maximum characters per fragment.
- `sentencechars`: Characters that indicate sentence boundaries.
- `charlimit`: Maximum character position to process.

**Note:** Should be used with an analyzer that does not remove stop words.

### `ContextFragmenter`

```python
class whoosh.highlight.ContextFragmenter(
    maxchars: int = 200,
    surround: int = 20,
    charlimit=DEFAULT_CHARLIMIT
)
```

The default fragmenter. Finds matched terms and includes `surround` characters
of context before and after each match.

- `maxchars`: Maximum characters per fragment.
- `surround`: Number of context characters to include around matches.
- `charlimit`: Maximum character position to process.

### `PinpointFragmenter`

```python
class whoosh.highlight.PinpointFragmenter(
    maxchars: int = 200,
    surround: int = 20,
    autotrim: bool = False,
    charlimit=DEFAULT_CHARLIMIT
)
```

A non-retokenizing fragmenter that builds fragments from character positions of
matched terms. Faster than `ContextFragmenter` because it doesn't need to
re-tokenize text.

- `maxchars`: Maximum characters per fragment.
- `surround`: Number of context characters around matches.
- `autotrim`: If `True`, trims fragments to the nearest spaces.
- `charlimit`: Maximum character position to process.

### `NullFragmeter`

Alias for `WholeFragmenter`.

### `Fragment`

```python
class whoosh.highlight.Fragment(
    text: str,
    matches,
    startchar: int = 0,
    endchar: int = -1
)
```

Represents a fragment (excerpt) from a hit document. Stores the start and end
character offsets and the list of matched term objects.

**Attributes:**
- `text`: The original source text.
- `matches`: List of objects with `startchar` and `endchar` attributes.
- `startchar`: Start index of the fragment.
- `endchar`: End index of the fragment.
- `matched_terms`: Set of text values of matched terms.

**Methods:**
- `overlaps(fragment)`: Returns `True` if this fragment overlaps the given one.
- `overlapped_length(fragment)`: Returns the combined length of overlapping
  fragments.

### `FragmentScorer`

```python
class whoosh.highlight.FragmentScorer
```

Base class for fragment scoring objects. Subclasses implement `__call__()`
to score a `Fragment`.

### `BasicFragmentScorer`

```python
class whoosh.highlight.BasicFragmentScorer
```

Scores fragments by summing the boosts of matched terms, then multiplying by
the number of distinct matched terms (favors diversity).

## Highlighter

### `Highlighter`

```python
class whoosh.highlight.Highlighter(
    fragmenter=None,
    scorer=None,
    formatter=None,
    always_retokenize: bool = False,
    order=SCORE
)
```

Main highlighter object used by `Searcher.highlights()`.

- `fragmenter`: Fragmenter instance (defaults to `ContextFragmenter`).
- `scorer`: Fragment scorer (defaults to `BasicFragmentScorer`).
- `formatter`: Formatter instance (defaults to `HtmlFormatter(tagname="b")`).
- `always_retokenize`: If `True`, always re-tokenize text instead of using
  character offsets from postings.
- `order`: Sort order for fragments.

**Methods:**
- `highlight_hit(hitobj, fieldname, top=3, minscore=1, strict_phrase=False)`:
  Returns the highlighted string for a single hit in a given field.
- `can_load_chars(results, fieldname)`: Returns `True` if the field supports
  "pinpoint" highlighting using stored character offsets.


## DOCUMENT: Idsets

# Idsets API

Specialized set implementations for storing sorted lists of positive
integers (document IDs). These are more memory-efficient than the built-in
`set` for certain use cases, though they are slower for most operations since
they are pure Python.

## Overview

The `DocIdSet` class is the abstract base class. Concrete implementations
include `BitSet`, `OnDiskBitSet`, `SortedIntSet`, `RoaringIdSet`, and
`MultiIdSet`. The `AutoIdSet` function selects the best implementation
based on the contents.

## Module Functions

### `autoset`

```python
whoosh.idsets.autoset
```

A factory that creates an appropriate `DocIdSet` subclass based on the
contents of a given iterable. If all integers in the set are below 10,000,
returns a `BitSet`; otherwise returns a `SortedIntSet`.

## `DocIdSet`

```python
class whoosh.idsets.DocIdSet
```

Abstract base class for set implementations specialized toward storing sorted
lists of positive integers.

**Inheritance:** Inherits from `set`-like interface.

**Methods:**
- `__eq__(other)`: Compares two `DocIdSet` instances by iterating.
- `__len__()`: Returns the number of elements. Override in subclasses.
- `__iter__()`: Yields elements in sorted order. Override in subclasses.
- `__contains__(i)`: Returns `True` if `i` is in the set.
- `__or__(other)`: Returns `self.union(other)`.
- `__and__(other)`: Returns `self.intersection(other)`.
- `__sub__(other)`: Returns `self.difference(other)`.
- `copy()`: Returns a copy of this set.
- `add(n)`: Adds `n` to the set.
- `discard(n)`: Removes `n` from the set (no error if absent).
- `update(other)`: Adds all elements from `other`.
- `intersection_update(other)`: Removes elements not in `other`.
- `difference_update(other)`: Removes all elements in `other`.
- `invert_update(size)`: In-place inversion over the range `[0, size)`.
- `intersection(other)`: Returns a new set with elements in both.
- `union(other)`: Returns a new set with elements from both.
- `difference(other)`: Returns a new set with elements in self but not other.
- `invert(size)`: Returns a new set that is the inversion over `[0, size)`.
- `isdisjoint(other)`: Returns `True` if no elements are shared.
- `before(i)`: Returns the previous integer in the set before `i`, or `None`.
- `after(i)`: Returns the next integer in the set after `i`, or `None`.
- `first()`: Returns the first (lowest) integer.
- `last()`: Returns the last (highest) integer.

## `BaseBitSet`

```python
class whoosh.idsets.BaseBitSet(DocIdSet)
```

Base class for bitmap-backed `DocIdSet` implementations. Uses a bytes-based
bitmap where each bit represents membership of an integer.

**Abstract Methods to Override:**
- `byte_count()`: Returns the number of bytes in the bitmap.
- `_get_byte(i)`: Returns the byte at index `i`.
- `_iter_bytes()`: Yields all bytes in the bitmap.

**Inherited Methods:** All `DocIdSet` methods with efficient bitmap
implementations of `__len__`, `__iter__`, `__contains__`, `first`, and
`last`.

## `OnDiskBitSet`

```python
class whoosh.idsets.OnDiskBitSet(file, doc_count)
```

A `BaseBitSet` that reads the bitmap from a file on disk, using `mmap` for
memory efficiency.

**Constructor:**
- `file`: A file-like object (opened in binary mode) containing the bitmap.
- `doc_count`: Total number of documents (bits) represented.

```python
from whoosh.idsets import OnDiskBitSet

with open("deletions.dat", "rb") as f:
    bs = OnDiskBitSet(f, doc_count=10000)
    if 42 in bs:
        print("Document 42 is deleted")
```

## `BitSet`

```python
class whoosh.idsets.BitSet
```

A `BaseBitSet` that stores the bitmap in memory as a `bytearray`. Fast for
membership tests and set operations on small ranges of integers.

**Constructor:**
- Optional initial iterable of integers.

```python
from whoosh.idsets import BitSet

bs = BitSet([0, 5, 10, 15])
print(5 in bs)  # True
print(bs.first())  # 0
print(len(bs))   # 4
```

**Methods:**
- `from_blob(data)`: Create a `BitSet` from raw bytes.
- `tostring()`: Returns the bitmap as a `bytes` string.
- `set_reverse()`: Prepares the set for reverse iteration.

## `SortedIntSet`

```python
class whoosh.idsets.SortedIntSet
```

A `DocIdSet` that stores integers as a sorted list of Python `int` objects.
More memory-efficient than `BitSet` for sparse sets but slower for membership
tests.

**Constructor:**
- Optional initial iterable of integers.

```python
from whoosh.idsets import SortedIntSet

sis = SortedIntSet([100, 500, 999])
print(500 in sis)  # True
print(sis.after(200))  # 500
```

## `ReverseIdSet`

```python
class whoosh.idsets.ReverseIdSet(child)
```

Wraps another `DocIdSet` to reverse the interpretation of integers. Instead
of representing membership directly, the set represents the *complement* of
the inner set. Useful for representing deleted documents.

**Constructor:**
- `child`: The `DocIdSet` to reverse.

**Example:** If `child` represents documents `{3, 7, 9}`, then
`ReverseIdSet(child)` represents all documents *except* `{3, 7, 9}`.

## `RoaringIdSet`

```python
class whoosh.idsets.RoaringIdSet
```

A `DocIdSet` that partitions integers into 16-bit buckets and uses `BitSet`
within each bucket. More memory-efficient than a single flat `BitSet` for
large, sparse sets of integers.

**Constructor:**
- Optional initial iterable of integers.

**Methods:**
- `from_bytes(data)`: Deserialize from bytes.
- `to_bytes()`: Serialize to bytes.
- `to_bytes_list()`: Returns a list of `(bucket, bytes)` pairs.

## `MultiIdSet`

```python
class whoosh.idsets.MultiIdSet(readers, offsets=None)
```

Combines multiple `DocIdSet` instances into one, handling document ID offsets
automatically. Used for combining deletions across multiple segments.

**Constructor:**
- `readers`: List of `DocIdSet` instances (one per segment).
- `offsets`: Optional list of base docnum offsets for each reader. If
  omitted, offsets are computed automatically.

**Methods:**
- `__contains__(i)`: Checks the appropriate sub-set based on offsets.
- `__iter__()`: Iterates over all integers in all sub-sets.
- `__len__()`: Returns the total count across all sub-sets.


## DOCUMENT: Lang

# Language Support API

Language detection helpers, stemmer selection, stop-word lists, and
language-specific modules (Snowball stemmers, ISRI stemmer, Soundex,
Double Metaphone, etc.).

## Module Overview

The `whoosh.lang` package provides functions for detecting and selecting
language-specific resources (stemmers, stop words) and submodules containing
stemmers for various languages.

## Supported Languages

```python
whoosh.lang.languages = ("ar", "da", "nl", "en", "fi", "fr", "de", "hu",
                         "it", "no", "pt", "ro", "ru", "es", "sv", "tr")
```

Two-letter ISO 639-1 language codes for which stemmers or stop-word lists
are available.

## Language Aliases

```python
whoosh.lang.aliases = { ... }
```

A dictionary mapping alternate language identifiers to their canonical
two-letter codes. Includes ISO 639-3 three-letter codes, English names,
and native-language names.

## Exceptions

### `NoStemmer`

```python
class whoosh.lang.NoStemmer
```

Raised by `stemmer_for_language()` when no stemmer is available for the
given language.

### `NoStopWords`

```python
class whoosh.lang.NoStopWords
```

Raised by `stopwords_for_language()` when no stop-word list is available for
the given language.

## Language Functions

### `two_letter_code`

```python
whoosh.lang.two_letter_code(name) -> str or None
```

Converts a language identifier to its canonical two-letter code. Accepts
two-letter codes, ISO 639-3 codes, English names, and native-language names.

```python
from whoosh.lang import two_letter_code

code = two_letter_code("french")   # 'fr'
code = two_letter_code("deutsch")  # 'de'
code = two_letter_code("español")  # 'es'
```

### `has_stemmer`

```python
whoosh.lang.has_stemmer(lang) -> bool
```

Returns `True` if a stemmer is available for the given language.

### `has_stopwords`

```python
whoosh.lang.has_stopwords(lang) -> bool
```

Returns `True` if a stop-word list is available for the given language.

### `stemmer_for_language`

```python
whoosh.lang.stemmer_for_language(lang) -> callable
```

Returns a stemmer function for the given language. Raises `NoStemmer` if
no stemmer is available.

**Supported languages and stemmers:**
- `"en"` / `"en_porter"`: Original Porter stemmer (`whoosh.lang.porter`)
- `"ar"`: ISRI Arabic stemmer (`whoosh.lang.isri`)
- `"da"`: Danish Snowball stemmer
- `"nl"`: Dutch Snowball stemmer
- `"en"`: English Snowball stemmer
- `"fi"`: Finnish Snowball stemmer
- `"fr"`: French Snowball stemmer
- `"de"`: German Snowball stemmer
- `"hu"`: Hungarian Snowball stemmer
- `"it"`: Italian Snowball stemmer
- `"no"`: Norwegian Snowball stemmer
- `"pt"`: Portuguese Snowball stemmer
- `"ro"`: (no stemmer currently)
- `"ru"`: Russian Snowball stemmer
- `"es"`: Spanish Snowball stemmer
- `"sv"`: Swedish Snowball stemmer
- `"tr"`: (no stemmer currently)

```python
from whoosh.lang import stemmer_for_language

stem = stemmer_for_language("en")
print(stem("running"))  # 'run'
```

### `stopwords_for_language`

```python
whoosh.lang.stopwords_for_language(lang) -> list
```

Returns the stop-word list for the given language. Raises `NoStopWords` if
no stop-word list is available.

```python
from whoosh.lang import stopwords_for_language

stops = stopwords_for_language("en")
```

## Snowball Stemmers

The `whoosh.lang.snowball` subpackage contains stemmers implementing the
Snowball stemming algorithms for various languages.

### Available Stemmers

| Module | Class | Language |
|--------|-------|----------|
| `snowball.english` | `EnglishStemmer` | English |
| `snowball.dutch` | `DutchStemmer` | Dutch |
| `snowball.finnish` | `FinnishStemmer` | Finnish |
| `snowball.french` | `FrenchStemmer` | French |
| `snowball.german` | `GermanStemmer` | German |
| `snowball.hungarian` | `HungarianStemmer` | Hungarian |
| `snowball.italian` | `ItalianStemmer` | Italian |
| `snowball.norwegian` | `NorwegianStemmer` | Norwegian |
| `snowball.portugese` | `PortugueseStemmer` | Portuguese |
| `snowball.russian` | `RussianStemmer` | Russian |
| `snowball.romanian` | `RomanianStemmer` | Romanian |
| `snowball.spanish` | `SpanishStemmer` | Spanish |
| `snowball.swedish` | `SwedishStemmer` | Swedish |
| `snowball.danish` | `DanishStemmer` | Danish |

### Base Classes

```python
class whoosh.lang.snowball.bases._ScandinavianStemmer
class whoosh.lang.snowball.bases._StandardStemmer
```

Internal base classes for Snowball stemmers. User code should use the
language-specific stemmer classes directly.

### `classes`

```python
whoosh.lang.snowball.classes = {"da": DanishStemmer, "nl": DutchStemmer, ...}
```

Dictionary mapping two-letter language codes to Snowball stemmer classes.

## Porter Stemmer

### `whoosh.lang.porter`

The original Porter stemming algorithm, faster but less accurate than
Snowball English stemmer.

#### `stem`

```python
whoosh.lang.porter.stem(w) -> str
```

Stems a single English word using the Porter algorithm.

## ISRI Stemmer

### `whoosh.lang.isri.ISRIStemmer`

```python
class whoosh.lang.isri.ISRIStemmer
```

Arabic stemmer based on the Information Science Research Institute (ISRI)
algorithm. Does not use a root dictionary.

#### `stem`

```python
def ISRIStemmer.stem(word) -> str
```

Stems an Arabic word.

## Double Metaphone

### `whoosh.lang.dmetaphone.double_metaphone`

```python
whoosh.lang.dmetaphone.double_metaphone(text) -> tuple
```

Returns a tuple of `(primary, secondary)` metaphone codes for the given
text, using the Double Metaphone algorithm.

## Soundex

### `whoosh.lang.phonetic`

Soundex implementations for phonetic matching.

#### `soundex_en`

```python
whoosh.lang.phonetic.soundex_en(word) -> str
```

English Soundex encoding.

#### `soundex_esp`

```python
whoosh.lang.phonetic.soundex_esp(word) -> str
```

Spanish Soundex encoding.

#### `soundex_ar`

```python
whoosh.lang.phonetic.soundex_ar(word) -> str
```

Arabic Soundex encoding.

## WordNet Thesaurus

### `whoosh.lang.wordnet.Thesaurus`

```python
class whoosh.lang.wordnet.Thesaurus
```

Provides synonym expansion based on WordNet-style data.

**Methods:**
- `synonyms(word)`: Returns the set of synonyms for `word`.
- `__contains__(word)`: Returns `True` if `word` is in the thesaurus.

### Functions

```python
whoosh.lang.wordnet.parse_file(f) -> dict
whoosh.lang.wordnet.make_index(storage, indexname, word2nums, num2words)
whoosh.lang.wordnet.synonyms(word2nums, num2words, word) -> set
```

## Lovins Stemmer

### `whoosh.lang.lovins`

A suffix-stripping stemmer by Lovins. Functions include:
- `stem(word)`: Main stemming function.
- `remove_ending(word)`: Removes suffixes.
- `fix_ending(word)`: Fixes the word ending after stemming.

## Paice-Husk Stemmer

### `whoosh.lang.paicehusk.PaiceHuskStemmer`

```python
class whoosh.lang.paicehusk.PaiceHuskStemmer(rules)
```

A rule-based stemmer using Paice-Husk rules.

#### `stem`

```python
def PaiceHuskStemmer.stem(word) -> str
```

Stems a word using the Paice-Husk algorithm.

**Usage note:** The module also exposes a pre-configured stemmer:
```python
whoosh.lang.paicehusk.stem = PaiceHuskStemmer(defaultrules).stem
```


## DOCUMENT: Matching

# Matching API

Classes and functions for iterating over and combining result sets during
searching. The matching module is a refactored package exposing the same
public API as the former monolithic module.

## Overview

When you search an index, Whoosh creates `Matcher` objects representing the
postings (document IDs and scores) produced by query objects. Matchers can
be combined (e.g., union, intersection) to build compound queries. The
matching module provides the core `Matcher` class hierarchy, utility
functions, and concrete implementations for various query types.

## Core Matcher Classes

### `Matcher`

```python
class whoosh.matching.Matcher
```

Abstract base class for all matchers. Concrete subclasses implement
`__init__()` and the `_set()` and `_maybe_values()` methods.

**Methods:**

#### `init = property(is_active)`

Property that returns whether the matcher is "active" (at top of segment
postings, not exhausted).

#### `init(view, docnum, score)`

Called when the matcher is initialized.

#### `set(matcher)`

Replaces this matcher with another one.

#### `copy()`

Returns a copy of this matcher.

#### `all_ids()`

Returns a list of docnums matched by this matcher.

#### `matches(matcher)`

Returns `True` if any of the current matches in `self` also match in
`matcher`.

#### `skip_to(docid)`

Advances the matcher to the first match at or after `docid`.

#### `skip_to_intersect(matcher)`

Moves this matcher to the earliest matching docnum that is also matched in
`matcher`.

#### `next()`

Advances the matcher to the next match.

#### `next_in_segment()`

Advances to the next match in the current segment.

#### `next_segment(matcher)`

Advances to the next segment in the context of `matcher`.

#### `is_active(in_segment=False)`

Returns `True` if this matcher has more matches to process.

#### `all_matching_segments()`

Generates `(segment_num, matcher)` pairs for all matching segments.

#### `doc()`

Returns the current document number of this matcher. May advance to next
document if not already on one.

#### `docnum()`

Returns the current docnum (segment-relative) of the matcher.

#### `score()`

Returns the current match's score.

#### `value()`

Returns the current match's value (e.g., the decoded stored value of the
term).

#### `supports()`

Returns `True` if `value()` is supported.

#### `value_matches()`

Returns the value at the current match.

#### `all_values()`

Returns a list of all values in this matcher.

#### `supports_lee()`

Returns `True` if the matcher uses lazy evaluation.

#### `lee`

Returns the current "lazy evaluation extension" value (for term vectors).

#### `spans()`

If the postings include positions, returns a list of `Position` objects for
the current match.

#### `spans()`

Returns the spans (positions) of the match in the current document.

#### `next_type()`

Returns the type of the next match.

#### `copy()`

Returns a shallow copy of this matcher.

### `Child`

```python
class whoosh.matching.Child
```

Mixin class for matchers that wrap other matchers.

### `FilterMixin`

```python
class whoosh.matching.FilterMixin
```

Mixin for matchers used as filters (boolean scoring, no relevance).

### `Custom`

```python
class whoosh.matching.Custom
```

Mixin for matchers that return a custom score from `score()` rather than 1.

### `Constant`

```python
class whoosh.matching.Constant
```

Mixin for matchers whose score is always the same value.

### `Coord`

```python
class whoosh.matching.Coord
```

Mixin for matchers that compute coordination factor (for phrase and other
queries that benefit from it).

## Concrete Matcher Classes

### `ListUnion`

```python
class whoosh.matching.ListUnion(matcher, items, maptype=None)
```

Base class for matchers that combine multiple matchers with a list of keys.

#### `filter`

```python
class whoosh.matching.filter
```

Decorator for creating filter matchers (boolean matchers with no relevance).

### `Union`

```python
class whoosh.matching.Union(matcher, items)
```

Base class for the `OR` operator.

### `Intersection`

```python
class whoosh.matching.Intersection(matcher, items)
```

The `AND` operator. A document matches only if it appears in all the child
matchers.

#### `IntersectionFilter`

```python
class whoosh.matching.IntersectionFilter(matcher, items)
```

A filter (no scoring) version of intersection.

### `And`

```python
class whoosh.matching.And(matcher, items)
```

Alias for `Intersection`.

### `Or`

```python
class whoosh.matching.Or(matcher, items)
```

Alias for `Union`.

### `Not`

```python
class whoosh.matching.Not(matcher, a, b)
```

The `NOT` operator. Matches all documents in `a` that are not in `b`.

### `Require`

```python
class whoosh.matching.Require(matcher, a, b)
```

Matches documents in `a` only if they also appear in `b`, but does not add
`b`'s score.

### `AndNot`

```python
class whoosh.matching.AndNot(matcher, a, b)
```

Matches documents in `a` that are not in `b`.

#### `AndMaybe`

```python
class whoosh.matching.AndMaybe(matcher, a, b)
```

Matches documents in `a`, adding `b`'s score if present.

### `BinaryUnion`

```python
class whoosh.matching.BinaryUnion(items)
```

Efficient intersection of exactly two matchers.

#### `BinaryUnion2`

```python
class whoosh.matching.BinaryUnion2
```

Optimized binary union for two items.

### `TreeMatcher`

```python
class whoosh.matching.TreeMatcher
```

A matcher that wraps a `Tree` object for combining results.

### `NestedParent`

```python
class whoosh.matching.NestedParent(parent, child, bools=False)
```

Matches parent documents that have at least one child document matched by
the child matcher. Used for nested document queries.

### `NestedChildren`

```python
class who which.matching.NestedChildren(parentmatch, child)
```

Matches child documents for a given parent document.

### `LengthMatcher`

```python
class whoosh.matching.LengthMatcher(child, q, polarity=False)
```

Matches documents based on field length (used by `Every` query).

### `Filter`

```python
class whoosh.matching.Filter(matcher)
```

Converts any matcher into a filter (no scoring).

### `AlwaysFilter`

```python
class whoosh.matching.AlwaysFilter
```

A filter that matches all documents.

### `NeverFilter`

```python
class whoosh.matching.NeverFilter
```

A filter that matches no documents.

### `PseudoMatcher`

```python
class whoosh.matching.PseudoMatcher
```

Base class for pseudo-matchers used in span queries.

## Matching Utilities

### `current_spans`

```python
whoosh.matching.current_spans(matcher) -> list
```

Returns a list of `Span` objects for the current match in `matcher`, or an
empty list if the matcher doesn't support positions.

### `disjunction_score`

```python
whoosh.matching.disjunction_score(matcher) -> float
```

Returns the sum of `matcher.score()` and the scores of all child matchers of
type `Union`.

### `intersection_score`

```python
whoosh.matching.intersection_score(matcher) -> float
```

Returns the sum of `matcher.score()` and all child matchers of type
`Intersection`.

### `child_count`

```python
whoosh.matching.child_count(matcher) -> int
```

Returns the number of child matchers in `matcher`.

### `has_quality`

```python
whoosh.matching.has_quality(matcher) -> bool
```

Returns `True` if `matcher` has a `query` attribute (i.e., is a
`QueryMatcher`-derived object, or a combination of such matchers).

### `has_untranslated`

```python
whoosh.matching.has_untranslated(matcher) -> bool
```

Returns `True` if the matcher has an `untranslated` attribute (set by
certain wrapper matchers like `TimeLimited`).

### `wrap`

```python
whoosh.matching.wrap(matcher)
```

Returns `matcher` if it has a `.copy()` method, otherwise wraps it in an
`AutoMatcher`.

### `wrap2`

```python
whoosh.matching.wrap2(a, b, m)
```

Returns either a `BinaryUnion2` or an `AutoMatcher` depending on whether `a`
and `b` are list-compatible.

### `unified`

```python
whoosh.matching.unified(matcher)
```

Returns `matcher` if it has an `untranslated` attribute, otherwise returns
`None`.

### `deletion`

```python
whoosh.matching.deletion(matcher)
```

If `matcher` has a `parent` attribute, returns the parent, otherwise returns
`None`.

### `AutoMatcher`

```python
class whoosh.matching.AutoMatcher(m, **kwargs)
```

A general-purpose matcher that wraps arbitrary objects and adds default
behavior for scoring, docnums, and other features. Created by `wrap()`.

### `MatchingTimeLimit`

```python
class whoosh.matching.MatchingTimeLimit
```

A lightweight exception raised when a query matcher exceeds a time limit.

### `TimeLimited`

```python
class whoosh.matching.TimeLimited(child, maxsteps=100, timeout=None, currenttime=None)
```

Wrapper that wraps a `Matcher` to enforce a time limit. Raises
`MatchingTimeLimit` if the time limit is exceeded.

**Parameters:**
- `child`: The matcher to wrap.
- `maxsteps`: Check time every N documents (default `100`).
- `timeout`: Maximum time in seconds (default `None`, no limit).
- `currenttime`: Optional function to use for getting the current time.

### `TermMatcher`

```python
class whoosh.matching.TermMatcher(postings, text, qname, scorer=None, boost=1.0)
```

Matches documents containing a specific term.

**Constructor:**
- `postings`: A `Postings` object from the index reader.
- `text`: The term text.
- `qname`: The query name for this term.
- `scorer`: Optional `Scorer` object.
- `boost`: Boost factor for this term's score.

### `MultiScorer`

```python
class whoosh.matching.MultiScorer(numgroups, start_i=0)
```

A `Scorer` that combines the scores from multiple scorers into one, weighted
across groups of segments.

### `RangeMatcher`

```python
class whoosh.matching.RangeMatcher(start_matcher, end_matcher, query)
```

Matches documents within a range of term values.

### `RegexMatcher`

```python
class whoosh.matching.RegexMatcher(regex, qname, boost=1.0)
```

Matches documents whose terms match a compiled regex.

### `SpanMatcher`

```python
class whoosh.matching.SpanMatcher(matcher, order=0, end=0)
```

Matches spans (positions) within documents.

### `SpanOverlap`

```python
class whoosh.matching.SpanOverlap(l, r)
```

Matches overlapping spans from two matchers.

### `SpanNear`

```python
class whoosh.matching.SpanNear(l, r, slop=1, ordered=True)
```

Matches spans that are near each other within a document.

### `SpanCondition`

```python
class whoosh.matching.SpanCondition(l, r)
```

Matches a condition on spans.

### `SpanBefore`

```python
class whoosh.matching.SpanBefore(l, r, end=0)
```

Matches spans before a given position.

### `SpanAfter`

```python
class whoosh.matching.SpanAfter(l, r, end=0)
```

Matches spans after a given position.

### `SpanOutside`

```python
class whoosh.matching.SpanOutside(l, r, end=0)
```

Matches spans outside a given range.

### `SpanFirst`

```python
class whoosh.matching.SpanFirst(l, start=0, end=1)
```

Matches spans at the beginning of a document.

### `SpanNot`

```python
class whoosh.matching.SpanNot(l, r)
```

Matches spans in `l` that are not in `r`.

### `SpanOr`

```python
class whoosh.matching.SpanOr(items)
```

Logical OR for span matchers.

### `SpanAnd`

```python
class whoosh.matching.SpanAnd(l, r)
```

Logical AND for span matchers.


## DOCUMENT: Middleware

# Middleware API

Reference for the middleware pipeline.

## MiddlewareContext

```python
class whoosh.middleware.context.MiddlewareContext
```

Context object passed through all middleware hooks.

### Attributes

| Attribute | Type | Description |
|-----------|------|-------------|
| `operation` | `str` | Operation type: `index`, `search`, `delete`, `commit` |
| `index` | `Any` | The index object |
| `backend` | `Any` | The backend storage object |
| `writer` | `Any` | The index writer |
| `searcher` | `Any` | The searcher |
| `document` | `dict \| None` | Document being indexed/deleted |
| `query` | `str` | Search query string |
| `collector` | `Any` | Collector instance |
| `results` | `Any` | Search results |
| `labels` | `dict` | Middleware identification labels |
| `metadata` | `dict` | Arbitrary middleware communication data |

### Methods

#### `copy()`

```python
ctx_copy = context.copy()
```

Create a shallow copy.

---

## Middleware

```python
class whoosh.middleware.base.Middleware
```

Base class for all middleware.

### Methods

#### `startup()`

```python
def startup(self, context: MiddlewareContext) -> None:
    """Called once on initialization."""
```

#### `shutdown()`

```python
def shutdown(self, context: MiddlewareContext) -> None:
    """Called once on teardown."""
```

#### `before_index()`

```python
def before_index(self, context: MiddlewareContext) -> MiddlewareContext:
    """Called before indexing a document."""
```

#### `after_index()`

```python
def after_index(self, context: MiddlewareContext) -> MiddlewareContext:
    """Called after indexing a document."""
```

#### `before_delete()`

```python
def before_delete(self, context: MiddlewareContext) -> MiddlewareContext:
    """Called before deleting a document."""
```

#### `after_delete()`

```python
def after_delete(self, context: MiddlewareContext) -> MiddlewareContext:
    """Called after deleting a document."""
```

#### `before_search()`

```python
def before_search(self, context: MiddlewareContext) -> MiddlewareContext:
    """Called before executing a search."""
```

#### `after_search()`

```python
def after_search(self, context: MiddlewareContext) -> MiddlewareContext:
    """Called after search results are returned."""
```

#### `on_error()`

```python
def on_error(self, context: MiddlewareContext, exc: Exception) -> None:
    """Called on exception. Re-raise by default."""
```

#### `on_commit()`

```python
def on_commit(self, context: MiddlewareContext) -> None:
    """Called after commit."""
```

---

## MiddlewareChain

```python
class whoosh.middleware.chain.MiddlewareChain
```

Orchestrates ordered middleware execution.

### Methods

#### `add()`

```python
chain.add(middleware: Middleware)
```

Add a middleware.

---

#### `extend()`

```python
chain.extend(middlewares: list[Middleware])
```

Add multiple middlewares.

---

#### `run_before()`

```python
context = chain.run_before(
    hook_name: str,
    context: MiddlewareContext,
    fail_open: bool = False
)
```

Run before hooks in order.

---

#### `run_after()`

```python
context = chain.run_after(
    hook_name: str,
    context: MiddlewareContext,
    fail_open: bool = False
)
```

Run after hooks in reverse.

---

#### `run_on_error()`

```python
chain.run_on_error(context, exc, fail_open=False)
```

Call on_error hooks.

---

## Built-in Middleware Classes

### MetricsMiddleware

```python
class whoosh.middleware.base.MetricsMiddleware
```

Tracks indexing/search counts.

#### Methods

##### `after_index()`

Increment documents indexed count.

##### `after_search()`

Increment searches executed count.

##### `get_metrics()`

```python
metrics = metrics_mw.get_metrics() -> dict
```

Return collected metrics.

---

### CacheMiddleware

```python
class whoosh.middleware.base.CacheMiddleware
```

In-memory search cache.

#### Methods

##### `before_search()`

Check cache for query.

##### `after_search()`

Store results in cache.

##### `get_cached()`

```python
cached = cache_mw.get_cached(query: str) -> Any
```

##### `set_cached()`

```python
cache_mw.set_cached(query: str, results: Any)
```

---

### CompressionMiddleware

```python
class whoosh.middleware.base.CompressionMiddleware
```

Mark documents for compression at backend level.

---

### EncryptionMiddleware

```python
class whoosh.middleware.base.EncryptionMiddleware
```

Mark documents for encryption at backend level.

---

## Exceptions

### StopOperation

```python
class whoosh.middleware.exceptions.StopOperation(Exception)
```

Raise to abort an operation.

---

## Integration Helpers

### `apply_middleware_to_writer()`

```python
def apply_middleware_to_writer(
    writer: IndexWriter,
    middleware: list[Middleware] = None
) -> MiddlewareWriter
```

---

### `apply_middleware_to_searcher()`

```python
def apply_middleware_to_searcher(
    searcher: Searcher,
    middleware: list[Middleware] = None
) -> MiddlewareSearcher
```


## DOCUMENT: Modern

# Modern API

The `whoosh_modern` package provides the modern, fully-typed surface of
Whoosh-NG. It includes data sources, schema discovery, validation,
middleware, profiling, autocomplete, vector search integration, and an
optimized batch writer.

## Data Sources

```python
from whoosh_modern.data_sources import (
    DataSource,
    SQLSource,
    RESTSource,
    FastCSVSource,
    JSONSource,
    GraphQLSource,
    PydanticSource,
    PandasSource,
    PolarsSource,
    ParquetSource,
    PeeweeSource,
    TortoiseSource,
    SQLAlchemySource,
    ObservableDataSource,
    DataSourceConfig,
)
```

### DataSource Protocol

The `DataSource` protocol defines the interface for all data source
implementations:

```python
class DataSource(Protocol):
    @property
    def name(self) -> str
    def discover_schema(self) -> Schema
    def iter_documents(self) -> Iterator[Document]
    def stream_batches(self, batch_size=1000) -> Iterator[list[dict]]
    def health_check(self) -> bool
```

Additional capability protocols are available:
- `IncrementalDataSource` — supports `iter_changes(since)` for incremental sync
- `AsyncDataSource` — supports `aiter_documents()` for async iteration
- `RefreshableDataSource` — supports `refresh()`
- `CountableDataSource` — supports `document_count()`
- `MetadataDataSource` — supports `metadata()`
- `ObservableDataSource` — supports `add_observer()`/`remove_observer()`

### SQLSource

```python
from whoosh_modern.data_sources import SQLSource

source = SQLSource(
    connection=conn,
    query="SELECT * FROM articles WHERE status='published'",
    incremental_field="updated_at",
    id_field="id",
    pool_size=5,
)
schema = source.discover_schema()
docs = list(source.iter_documents())
batches = list(source.stream_batches(batch_size=1000))
count = source.document_count()
meta = source.metadata()
```

### RESTSource

```python
from whoosh_modern.data_sources import RESTSource

source = RESTSource(
    url="https://api.example.com/v2/products",
    method="GET",
    pagination="page",  # or "offset", "cursor"
    page_size=50,
    headers={"Authorization": "Bearer token"},
    auth={"type": "bearer", "token": "..."},
    document_path="results",  # Extract from nested response
)
schema = source.discover_schema()
docs = list(source.iter_documents())
```

### DataSourceConfig

Declarative configuration with a factory:

```python
from whoosh_modern.data_sources import DataSourceConfig

config = DataSourceConfig(
    type="sql",
    connection=conn,
    query="SELECT * FROM articles",
    sequential_field="updated_at",
)
source = config.create()  # Returns a configured SQLSource
```

Supported types: `sql`, `sqlalchemy`, `rest`, `csv`, `json`, `graphql`,
`pydantic`, `pandas`, `polars`, `parquet`, `peewee`, `tortoise`.

## Schema Discovery

```python
from whoosh_modern.schema_discovery import SchemaDiscovery

# From column metadata (list of (name, sql_type) tuples)
columns = [("id", "INTEGER"), ("title", "TEXT"), ("published", "TIMESTAMP")]
schema = SchemaDiscovery.from_result_set(columns)

# From sample documents (auto-detects types)
schema = SchemaDiscovery.from_sample(docs)

# Optimized variant (drops non-searchable TEXT, infers IDs/booleans)
schema = SchemaDiscovery.from_sample_optimized(docs, searchable_text=["title", "content"])

# Detect ID field from schema
id_field = SchemaDiscovery.detect_id_field(dict(schema))
```

### SQL Type Mapping

`SchemaDiscovery` includes a built-in SQL type map: `VARCHAR`→`TEXT`,
`INTEGER`→`NUMERIC`, `BOOLEAN`→`BOOLEAN`, `TIMESTAMP`→`DATETIME`, `JSON`→
`KEYWORD`, `UUID`→`ID`, etc.

## FacetManager

```python
from whoosh_modern.facets import FacetManager, TermsFacet, RangeFacet, DateRangeFacet

manager = FacetManager(schema)
# Or with manual config:
manager = FacetManager(schema, config={"price": {"type": "range", "buckets": [...]}})

facets = manager.get_facets()          # Auto-discovered + manual facets
config = manager.get_facet_config("category")
stats = manager.get_facet_stats()
manager.set_manual_override("price", {"type": "range", "buckets": ["0-100", "100-500"]})
```

Auto-discovery rules:
- `TEXT`, `KEYWORD`, `BOOLEAN`, `ID` → `TermsFacet`
- `NUMERIC` → `RangeFacet`
- `DATETIME` → `DateRangeFacet`

## Validation Framework

```python
from whoosh_modern.validation import ValidationFramework, ValidationResult

validator = ValidationFramework()
results = validator.validate(source)

for result in results:
    print(f"Level {result.level}: passed={result.passed}")
    for warning in result.warnings:
        print(f"  Warning: {warning}")
    for error in result.errors:
        print(f"  Error: {error}")
```

Four validation levels:

| Level | Method | Purpose |
|-------|--------|---------|
| 1 | `validate_structural()` | DataSource availability, schema detection |
| 2 | `validate_search()` | Indexable fields, term vectors, searchable analyzers |
| 3 | `validate_performance()` | Performance warnings (e.g., TEXT fields on large datasets) |
| 4 | `validate_runtime()` | Sample iteration, type conformance |

## Middleware Pipeline

```python
from whoosh_modern.middleware import (
    Middleware,
    MiddlewarePipeline,
    RetryMiddleware,
    LoggingMiddleware,
    CacheMiddleware,
)

pipeline = MiddlewarePipeline(
    RetryMiddleware(attempts=3, backoff="exponential", jitter=True),
    LoggingMiddleware(level=logging.INFO),
    CacheMiddleware(maxsize=128),
)

def my_operation():
    return searcher.search(query)

result = pipeline.execute(my_operation)
```

### Middleware Types

- **`Middleware`**: The core base class (`whoosh.middleware.base.Middleware`, re-exported from `whoosh_modern.middleware`). Subclass it and implement the lifecycle hooks (`before_index`, `after_index`, `before_search`, `after_search`, `on_error`, `on_commit`). `RetryMiddleware`, `LoggingMiddleware`, and `CacheMiddleware` additionally keep a `wrap(operation)` helper for decorating callables.
- **`RetryMiddleware`**: Retries failed operations with exponential or linear
  backoff, with optional jitter.
- **`LoggingMiddleware`**: Logs execution time and errors.
- **`CacheMiddleware`**: Caches results keyed by operation name and arguments.
  Exposes `stats` property and `clear()` method.

## SearchView

```python
from whoosh_modern.views import SearchView

view = SearchView(
    name="articles",
    source=source,
    fields={"title": fields.TEXT(stored=True)},  # Field type overrides
    facets={"category": {"type": "terms", "limit": 50}},
    incremental_field="updated_at",
    strict=False,  # Raise on validation failures
    middleware=[LoggingMiddleware()],
    schema_version="1.0",
)

ix = view.build("indexdir")       # Create/populate index
count = view.reindex()            # Full reindex
count = view.refresh()            # Incremental refresh
results = view.validate()         # Run validation
view.evolve_schema({"new_field": fields.TEXT})  # Add fields without reindexing
```

## Optimized Writer

```python
from whoosh_modern.writer import ModernIndex

# Create or open an optimized index
index = ModernIndex.create("indexdir", schema=my_schema)
# Or open existing:
# index = ModernIndex.open("indexdir")

# Optimized writer for batch processing millions of docs
with index.writer(batch_size=5000, limitmb=512, multisegment=True) as writer:
    for batch in source.stream_batches(batch_size=5000):
        writer.add_batch(batch)
        # Or: writer.add_batches(source.stream_batches(batch_size=5000))

print(f"Documents indexed: {writer.doc_count}")

# Access searcher
with index.searcher() as searcher:
    results = searcher.search(query)
```

### Key Optimizations

- **Multisegment mode**: No merging during indexing (set with
  `multisegment=True`)
- **Reduced Python overhead**: Batch-oriented add API
- **Configurable memory limits**: `limitmb` parameter controls buffering

## Analysis Extensions

```python
from whoosh_modern.analysis import StemmingAnalyzer, get_stemmer, register_stemmer
```

### StemmingAnalyzer

Enhanced analyzer with pluggable stemmer backends:

```python
# Auto-detect best available stemmer
analyzer = StemmingAnalyzer(stemmer="auto")

# Explicit internal stemmer
analyzer = StemmingAnalyzer(stemmer="internal")

# PyStemmer (requires pip install whoosh-ng[fast-stemming])
analyzer = StemmingAnalyzer(stemmer="pystemmer")

# Custom stemmer provider
analyzer = StemmingAnalyzer(stemmer=my_custom_stemmer)

# Full parameters
analyzer = StemmingAnalyzer(
    expression=r"\S+",
    stoplist=None,
    minsize=2,
    maxsize=None,
    gaps=False,
    stemmer="auto",
    ignore=None,
    cachesize=50000,
)
```

### Stemmer Providers

```python
from whoosh_modern.analysis import get_stemmer, list_available_backends

# Get a stemmer provider
stemmer = get_stemmer("auto", "english")
stemmed = stemmer.stem("running")  # "run"

# List available backends
backends = list_available_backends()
# {"internal": "available", "pystemmer": "not installed"}

# Register a custom stemmer
@register_stemmer("my_stemmer")
class MyStemmer:
    def stem(self, word: str) -> str:
        return word.lower()

# Priority: PyStemmer > Internal (whoosh.lang)
```

### StemmerProvider Protocol

```python
class StemmerProvider(Protocol):
    def stem(self, word: str) -> str
    @property
    def name(self) -> str
    @property
    def language(self) -> str
```

Available providers:
- `InternalStemmerProvider` — wraps Whoosh's built-in `whoosh.lang.porter.stem`
- `PyStemmerProvider` — wraps the PyStemmer library (fastest)
- `IdentityStemmerProvider` — no-op stemmer for testing

## Autocomplete

```python
from whoosh_modern.autocomplete import create_autocomplete

# Create an autocomplete provider
provider = create_autocomplete("inverted")

# Add phrases
provider.add(["hello world", "hello there", "goodbye world"])

# Search
hits = provider.search("hello", limit=10)
for hit in hits:
    print(hit.text, hit.score)
```

### Classes

- **`AutocompleteHit`**: Simple data class with `text` and `score` attributes.
- **`AutocompleteProvider` (Plugin)**: Abstract base for autocomplete
  implementations. Implements `add()` and `search()`.
- **`InvertedIndexAutocomplete`**: Default provider using prefix matching with
  a scoring function favoring exact prefix matches.

## Exceptions

```python
from whoosh_modern.exceptions import (
    DataSourceError,
    DataSourceNotFoundError,
    DocumentIterationError,
    SchemaDiscoveryError,
    ValidationError,
)
```

All exceptions inherit from `DataSourceError`, which carries optional
`source` and `field` context attributes.

## Storage Providers

```python
from whoosh_modern.storage import (
    FileStorage,
    AsyncFileStorage,
    S3Storage,
    HybridStorage,
    AsyncHybridStorage,
)
```

### FileStorage

Local filesystem storage. `FileStorage` is an alias of `FileStorageProvider`. Keys are
relative paths under ``root``.

```python
from whoosh_modern.storage import FileStorage

storage = FileStorage("indexdir")
storage.write("segment_1.dat", b"data")
assert storage.read("segment_1.dat") == b"data"
assert storage.exists("segment_1.dat") is True
storage.delete("segment_1.dat")
keys = storage.list_keys()
```

### AsyncFileStorage

Async variant of ``FileStorage``. All operations run on a worker thread
via ``asyncio.to_thread``.

```python
import asyncio
from whoosh_modern.storage import AsyncFileStorage

storage = AsyncFileStorage("indexdir")

async def main() -> None:
    await storage.awrite("segment_1.dat", b"data")
    data = await storage.aread("segment_1.dat")
    await storage.adelete("segment_1.dat")

asyncio.run(main())
```

## See Also

- [Storage Providers Guide](../modern/storage-providers.md) — Storage backend integration and benchmarks
- [Provider Integration Guide](../modern/provider-integration.md) — Complete pipeline guide for all providers
- [Middleware Guide](../modern/middleware-pipeline.md) — Pipeline hooks and provider adapters
- [Stemming Guide](../modern/stemming-providers.md) — Stemmer provider integration
- [Vector Search Guide](../modern/vector.md) — Vector provider integration
- [Autocomplete Guide](../modern/autocomplete-providers.md) — Autocomplete provider integration
### S3Storage

S3-compatible blob storage. ``boto3`` is required only when this provider
is used; it is imported lazily so the rest of Whoosh-NG does not depend on
it. A ``client`` may be injected for testing.

```python
from whoosh_modern.storage import S3Storage

storage = S3Storage(bucket="my-index-bucket", prefix="segments")
storage.write("segment_1.dat", b"data")
data = storage.read("segment_1.dat")
keys = storage.list_keys()
```

### HybridStorage

Compose a local cache with a remote backend for cloud-native indexes.
The remote is the source of truth; the local cache is a write-through
performance layer.

```python
from whoosh_modern.storage import HybridStorage, S3Storage

remote = S3Storage(bucket="my-index-bucket", prefix="segments")
storage = HybridStorage(local_cache="./cache", remote=remote)

storage.write("segment_1.dat", b"data")
data = storage.read("segment_1.dat")  # served from cache after first read
storage.invalidate("segment_1.dat")   # force refresh from remote
storage.prefetch(["segment_2.dat"])   # warm cache proactively
```

Read path:

1. local cache hit → return immediately
2. cache miss → read from remote, write-through into cache, return

Write path:

- ``remote.write(key, data)`` (source of truth)
- on success → ``local_cache.write(key, data)``
- on failure → raise before polluting cache

### AsyncHybridStorage

Async variant of ``HybridStorage``. Remote operations are executed on a
worker thread via ``asyncio.to_thread`` so the event loop is never blocked.

```python
import asyncio
from whoosh_modern.storage import AsyncHybridStorage, S3Storage

remote = S3Storage(bucket="my-index-bucket", prefix="segments")
storage = AsyncHybridStorage(local_cache="./cache", remote=remote)

async def main() -> None:
    await storage.awrite("segment_1.dat", b"data")
    data = await storage.aread("segment_1.dat")
    await storage.adelete("segment_1.dat")

asyncio.run(main())
```


## DOCUMENT: Overview

# API Overview

This section provides a comprehensive reference of the Whoosh-NG public API.

## Modules

| Module | Description |
|--------|-------------|
| `whoosh.index` | High-level index creation, opening, and management |
| `whoosh.fields` | Schema and field type definitions |
| `whoosh.writing` | Writer classes and merge policies |
| `whoosh.searching` | Searcher, Results, and collectors |
| `whoosh.query` | Query classes and parsers |
| `whoosh.qparser` | Query parser implementation |
| `whoosh.analysis` | Tokenizers, filters, and analyzers |
| `whoosh.highlight` | Search result highlighting |
| `whoosh.spelling` | Spelling correction |
| `whoosh.sorting` | Facets and sorting |
| `whoosh.event_bus` | Event system |
| `whoosh.hooks` | Hook system |
| `whoosh.middleware` | Middleware pipeline |
| `whoosh.plugins` | Plugin system and registry |
| `whoosh.backends` | Storage backends |
| `whoosh.vector` | Vector search providers |
| `whoosh_modern.autocomplete` | Autocomplete providers |
| `whoosh_fastapi` | FastAPI integration |

## Quick Reference

### Index Lifecycle

```python
from whoosh.index import create_in, open_dir, exists_in

# Create
ix = create_in("indexdir", schema)

# Open
ix = open_dir("indexdir")

# Check
if exists_in("indexdir"):
    ix = open_dir("indexdir")
```

### Writing

```python
with ix.writer() as writer:
    writer.add_document(field1=value1, field2=value2)
    writer.commit()
```

### Reading

```python
from whoosh.qparser import QueryParser

with ix.searcher() as searcher:
    qp = QueryParser("content", ix.schema)
    q = qp.parse("query")
    results = searcher.search(q)
```

### Schema

```python
from whoosh.fields import Schema, TEXT, ID, NUMERIC

schema = Schema(
    title=TEXT(stored=True),
    path=ID(stored=True, unique=True),
    count=NUMERIC(int, stored=True)
)
```


## DOCUMENT: Plugins

# Plugins & Registry

Extend Whoosh-NG through the plugin system and registries.

## Plugin System

### BasePlugin

```python
class whoosh.plugins.base.BasePlugin
```

All plugins inherit from this class.

#### Attributes

- `name (str)`: Plugin name.
- `version (str)`: Plugin version.
- `dependencies (list[str])`: Required plugins.

#### Methods

##### `setup()`

```python
def setup(self, registry) -> None:
    """Called when the plugin is enabled."""
```

##### `teardown()`

```python
def teardown(self, registry) -> None:
    """Called when the plugin is disabled."""
```

##### `middleware()`

```python
def middleware(self) -> list[Middleware]:
    """Return middleware instances."""
```

---

### PluginManager

```python
class whoosh.plugins.manager.PluginManager
```

Manages plugin lifecycle.

#### Methods

##### `load_plugins()`

```python
PluginManager.load_plugins()
```

Auto-discover plugins from entry points.

---

##### `register()`

```python
PluginManager.register(name: str, plugin: BasePlugin)
```

Register a plugin manually.

---

##### `enable()`

```python
PluginManager.enable(name: str)
```

Enable a registered plugin.

---

##### `disable()`

```python
PluginManager.disable(name: str)
```

Disable a plugin.

---

##### `get()`

```python
plugin = PluginManager.get(name: str)
```

Get a plugin instance.

---

##### `list_plugins()`

```python
plugins = PluginManager.list_plugins()
```

List all registered plugins.

---

##### `get_middleware_chain()`

```python
chain = PluginManager.get_middleware_chain()
```

Get the combined middleware chain from all plugins.

---

## Registry System

Registries provide centralized object management.

### Registry Base

```python
class whoosh.registry.base.Registry
```

Generic registry.

#### Methods

##### `register()`

```python
Registry.register(
    key: str,
    value: Any,
    owner: str = None
)
```

Register a value.

---

##### `un


## DOCUMENT: Query

# Query API

Build and execute queries programmatically.

## QueryParser

```python
class whoosh.qparser.QueryParser(
    fieldname: str,
    schema: Schema,
    group=AndGroup,
    **kwargs
)
```

Convert a query string into a Query object.

### Methods

#### `parse()`

```python
query = qp.parse(querystring)
```

Parse a query string.

---

#### `tokenize()`

```python
tokens = qp.tokenize(querystring)
```

Tokenize a query string without parsing.

---

### MultifieldParser

```python
class whoosh.qparser.MultifieldParser(
    fieldnames: list,
    schema: Schema,
    fieldboosts: dict = None,
    group=OrGroup,
    **kwargs
)
```

Search multiple fields with different boosts.

**Example:**
```python
from whoosh.qparser import MultifieldParser

qp = MultifieldParser(
    ["title", "content"],
    schema,
    fieldboosts={"title": 2.0}
)
```

## Query Classes

All queries inherit from `Query`:

```python
class whoosh.query.Query
```

### Methods

#### `matcher()`

```python
matcher = query.matcher(searcher, context=None)
```

Return a matcher for executing the query.

#### `__and__()`, `__or__()`, `__invert__()`

Combine queries with `&`, `|`, `-`.

### Leaf Queries

#### Term

```python
Term(fieldname: str, text: str, boost: float = 1.0)
```

Match a specific term.

---

#### Phrase

```python
Phrase(fieldname: str, words: list, boost: float = 1.0, slop: int = 1)
```

Match a phrase.

---

#### Prefix

```python
Prefix(fieldname: str, text: str, boost: float = 1.0)
```

Match terms starting with `text`.

---

#### Wildcard

```python
Wildcard(fieldname: str, text: str, boost: float = 1.0)
```

Match terms with `?` and `*` wildcards.

---

#### FuzzyTerm

```python
FuzzyTerm(
    fieldname: str,
    text: str,
    maxdist: int = 2,
    prefix: int = 0,
    boost: float = 1.0
)
```

Fuzzy match with edit distance.

---

#### Range

```python
NumericRange(
    fieldname: str,
    start: Any,
    end: Any,
    startexact: bool = False,
    endexact: bool = False,
    boost: float = 1.0
)
```

Numeric range query.

```python
DateRange(
    fieldname: str,
    start: datetime,
    end: datetime,
    startexact: bool = False,
    endexact: bool = False,
    boost: float = 1.0
)
```

Date range query.

---

#### Every

```python
Every(fieldname: str, boost: float = 1.0)
```

Match every document with any term in this field.

### Boolean Queries

#### And

```python
And(children: list, boost: float = 1.0)
```

All children must match.

---

#### Or

```python
Or(children: list, boost: float = 1.0)
```

Any child must match.

---

#### Not

```python
Not(query, exclude)
```

Match docs matching query but not exclude.

---

#### DisjunctionMax

```python
DisjunctionMax(
    children: list,
    tiebreak: float = 0.0,
    boost: float = 1.0
)
```

OR-like with scoring tiebreaker.

### Special Queries

#### Require

```python
Require(match, requires)
```

Match must have `match`, and at least one of `requires`.

---

#### AndMaybe

```python
AndMaybe(must, should)
```

Must match `must`, optionally boosting with `should`.

---

#### Boost

```python
Boost(q, factor)
```

Multiply score by factor.

---

#### ConstantScore

```python
ConstantScore(q, score=1.0)
```

Assign constant score.

## Query Operators

```python
q1 & q2      # And
q1 | q2      # Or
~q1          # Not
q1 ^ q2      # DisjunctionMax
```

## Plugins

```python
from whoosh.qparser import QueryParserPlugin

class RangePlugin(QueryParserPlugin):
    def __init__(self):
        pass

    def evaluate(self, env, signode):
        # Return a query node
        return query.Range(signode.fieldname, ...)
```

## Exceptions

```python
class whoosh.qparser.QueryParserError(Exception)
```

Raised on parse errors.


## DOCUMENT: Reading

# Reading API

Classes and functions for reading from an index. The reading module is a
refactored package exposing the same public API as the former monolithic
module.

## Overview

The reading module provides classes for accessing documents, terms, and
postings in an index. The main entry points are `IndexReader` objects obtained
from a searcher. These readers allow you to enumerate terms, access stored
fields, iterate postings, and get term frequencies.

## Core Classes

### `IndexReader`

```python
class whoosh.reading.IndexReader
```

Abstract base class for reading index data. Concrete subclasses include
`SegmentReader` and `MultiReader` (which wraps multiple segment readers).

### `MultiReader`

```python
class whoosh.reading.MultiReader(readers, base=None)
```

Combines multiple `IndexReader` instances into one. All docnums are treated
as relative to the combined index.

**Constructor:**
- `readers`: A list of `IndexReader` instances.
- `base`: Optional list of cumulative document count offsets for each reader.

**Methods:**

#### `doc_frequency(fieldname, text)`

Returns the total number of documents that have the given term in the given
field across all sub-readers.

#### `documents()`

Yields dictionaries of stored fields for each document in the index.

#### `stored_fields(docnum)`

Returns a dictionary of stored field values for the given document number
(index-wide docnum).

```python
r = my_index.reader()
print(r.stored_fields(20))
```

#### `all_stored_fields()`

Yields a `(docnum, stored_fields)` tuple for each document in the index.

#### `terms(fieldname)`

Yields `(fieldname, text)` tuples for every term in the given field.

#### `terms_from(segmentreader,fieldnameprefix)`

Low-level method for multi-reader.

#### `has_termvector(docnum, fieldname)`

Returns `True` if the document has a term vector for the given field.

#### `term_vector(docnum, fieldname)`

Returns a `TermVector` for the given document and field.

#### `is_deleted(docnum)`

Returns `True` if the given document (index-wide docnum) is deleted.

#### `all_doc_ids()`

Returns a sorted array of non-deleted document IDs.

#### `min_spam(fieldname)`

Returns the minimum spam value for the given field.

#### `set_spam(fieldname)`

Returns the set spam value for the given field.

#### `has_exact_length(docnum)`

Returns `True` if the exact length is known for `docnum`.

#### `doc_field_length(docnum, fieldname=None, default=1)`

Returns the length of the given field in the given document.

```python
r = my_index.reader()
length = r.doc_field_length(20, "content")
```

#### `max_field_length(fieldname)`

Returns the maximum length of the given field across all documents.

#### `iter_fieldname`

Low-level method for multi-reader.

#### `lexicon(fieldname)`

Returns an array of all unique terms in the given field, sorted.

#### `expanded_lexicon(fieldname)`

Low-level method that yields terms without the overhead of building an array.

#### `term_info(fieldname, text)`

Returns a `TermInfo` object for the given term, or `None` if the term does
not appear in the index.

#### `terminfos(fieldname)`

Yields `(text, TermInfo)` pairs for the given field.

#### `postings(fieldname, text, stype=None)`

Returns a `Matcher` for the postings list of the given term.

```python
r = my_index.reader()
m = r.postings("content", "whoosh")
for docnum, score in m:
    print("doc %d has term" % docnum)
```

#### `_all_postings(fieldname)`

Low-level. Yields `(text, matcher)` pairs for all terms in a field.

#### `_posting_fragments()`

Low-level.

#### `has_vector(docnum, fieldname)`

Returns `True` if the given field has a term vector in the given document.

#### `vectors(docnum)`

Yields `(fieldname, TermVector)` pairs for all term vectors in the document.

#### `all_items(fieldname)`

Yields `(term, weight, docfreq)` tuples for every term in the given field.

#### `frequency(fieldname, text)`

Returns the total frequency of the term across all documents.

#### `idf(term)`

Returns an iterator of `(docnum, idf)` pairs for the given term.

#### `spelling`

Returns a `SpellingAnalyzer` for the given field.

#### `doc_term(slicenum, fieldname, word)`

Returns `(df, weight)` for `word` in `fieldname` in segment `slicenum`.

#### `doc_diff(slicenum, fieldname, text, num)`

Returns `(df, weight)` for `word` in `fieldname` in segment `slicenum`.

### `SegmentReader`

```python
class whoosh.reading.SegmentReader(segment, schema, storage, base=True)
```

Reader for a single segment of the index.

**Constructor:**
- `segment`: The `Segment` object.
- `schema`: The `Schema` object.
- `storage`: The `Storage` instance.
- `base`: Base document number offset (usually `True`, meaning compute it).

### `MultiID3Reader`

```python
class whoosh.reading.MultiID3Reader(readers, base)
```

Combines multiple readers that have ID3 codec.

### `TermInfo`

```python
class whoosh.reading.TermInfo(
    df=0,
    weight=0,
    minlength=0,
    maxlen=0,
    maxnum=0,
    numdocs=0,
    scorable=True
)
```

Information about a term in the index.

**Attributes:**
- `df`: Document frequency (number of documents containing the term).
- `weight`: Total term frequency across all documents.
- `minlength`: Minimum document length where the term appears.
- `maxlength`: Maximum document length where the term appears. This is `0`
  if lengths are not stored.
- `maxnum`: Maximum number of occurrences per document.
- `numdocs`: Number of documents where the term has a non-zero contribution
  to the score.
- `scorable`: Whether this term is scorable.

## Term Vector

### `TermVector`

```python
class whoosh.reading.TermVector(docnum, fieldname, format_, terms, store_term_vector)
```

Represents the term vector for a single document/field pair.

**Methods:**

#### `tokens(text=None)`

Yields `(t, w, v, p)` tuples for terms in this field.

- `t`: The term string.
- `w`: The term weight (frequency in this document).
- `v`: The list of positions where the term occurs. (`None` if positions
  are not stored.)
- `p`: The list of characters where the term occurs. (`None` if character
  vectors are not stored.)

#### `items(text=None)`

Like `tokens()` but includes term strings in the result.

```python
tv = my_index.reader().term_vector(0, "content")
for token, frequency, positions, chars in tv.tokens():
    print(token, frequency, positions)
```

**Parameters:**
- `text`: Optional `Bytes` object. If given, only yield terms starting with
  this text (used for multi-byte tokenization).

## Reader Utilities

### `get_storage`

```python
whoosh.reading.get_storage(searcher) -> Storage
```

Returns the storage object associated with the searcher.

### `get_index_schema`

```python
whoosh.reading.get_index_schema(searcher) -> Schema
```

Returns the schema object associated with the searcher.

### `load_termdocs`

```python
whoosh.reading.load_termdocs(reader, fieldname, text) -> list
```

Returns a list of document numbers that have the given term.

### `read_pattern`

```python
whoosh.reading.read_pattern(reader, fieldname, expression) -> list
```

Returns sorted term list from `reader.lexicon(fieldname)` filtered to those
matching `expression`.

### `read_terminfo`

```python
whoosh.reading.read_terminfo(reader, fieldname, text) -> TermInfo or None
```

Returns a `TermInfo` for the given term, or `None` if not found.


## DOCUMENT: Reference

# API Reference

The Whoosh-NG API reference is auto-generated from source code using
[pydoctor](https://pydoctor.readthedocs.io/), which parses Python modules
and generates HTML documentation from docstrings.

:::note
If the embedded documentation does not display, the API docs may not have
been generated yet in this deployment. [View on GitHub](https://github.com/dorel14/whoosh-ng/tree/master/website/static/api_docs)
for the full API documentation, or check the
[API modules list](#api-modules) below.
:::

## API Modules

### Core API

| Module | Description |
|--------|-------------|
| `whoosh.index` | High-level index creation, opening, and management |
| `whoosh.fields` | Schema and field type definitions |
| `whoosh.writing` | Writer classes and merge policies |
| `whoosh.searching` | Searcher, Results, and collectors |
| `whoosh.query` | Query classes and parsers |
| `whoosh.qparser` | Query parser implementation |
| `whoosh.analysis` | Tokenizers, filters, and analyzers |
| `whoosh.highlight` | Search result highlighting |
| `whoosh.spelling` | Spelling correction |
| `whoosh.sorting` | Facets and sorting |
| `whoosh.event_bus` | Event system |
| `whoosh.hooks` | Hook system |
| `whoosh.middleware` | Middleware pipeline |
| `whoosh.plugins` | Plugin system and registry |
| `whoosh.backends` | Storage backends |

### Modern API

| Module | Description |
|--------|-------------|
| `whoosh_modern.data_sources` | Data source protocol and implementations |
| `whoosh_modern.views` | SearchView unified interface |
| `whoosh_modern.middleware` | Retry, cache, logging middleware |
| `whoosh_modern.facets` | FacetManager for auto-discovery |
| `whoosh_modern.validation` | 4-level validation framework |
| `whoosh_modern.indexing` | BatchIndexWriter, AnalyzerCache |
| `whoosh_modern.linguistics` | Linguistic engine (stemmers, synonyms) |
| `whoosh_modern.storage` | Storage providers (HybridStorage, etc.) |
| `whoosh_modern.vector` | NumpyProvider for vector similarity |
| `whoosh_modern.autocomplete` | Autocomplete provider plugins |
| `whoosh_fastapi` | FastAPI REST API endpoints |
| `whoosh_admin` | Admin UI dashboard |

:::info
For the full interactive API documentation, run:
```bash
pip install pydoctor
python scripts/generate_api_docs.py
```
Then open `website/static/api_docs/index.html` in your browser.
:::


## DOCUMENT: Searching

# Searching API

Execute queries and retrieve results.

## Searcher

```python
class whoosh.searching.Searcher
```

The Searcher is the primary interface for reading from the index.

### Methods

#### `search()`

```python
results = searcher.search(query, limit=10, **kwargs)
```

Execute a query and return Results.

**Args:**
- `query`: The query to run.
- `limit (int)`: Maximum number of results. Use `None` for all results.

**Returns:**
- `Results`: A Results object.

---

#### `search_page()`

```python
results = searcher.search_page(query, pagenum, pagelen=10)
```

Get a page of results.

---

#### `search_with_collector()`

```python
searcher.search_with_collector(query, collector)
```

Advanced search with custom collector.

---

#### `find()`

```python
results = searcher.find("field", "text")
```

Convenience method to search a single field.

---

#### `documents()`

```python
docs = list(searcher.documents(fieldname=value))
```

Get stored documents matching a term.

---

#### `document()`

```python
doc = searcher.document(fieldname=value)
```

Get a single stored document.

---

#### `lexicon()`

```python
terms = list(searcher.lexicon("fieldname"))
```

List all terms in a field.

---

#### `all_stored_fields()`

```python
for fields in searcher.all_stored_fields():
    print(fields)
```

Iterate over all stored fields.

---

#### `all_features()`

```python
with searcher.all_features() as features:
    facets = features.facet(facet)
```

Get facet counts across all documents.

## Results

```python
class whoosh.searching.Results
```

List-like container for matched documents.

### Methods

#### `__len__()`

```python
total = len(results)
```

Total matching documents (may recount).

#### `scored_length()`

```python
scored = results.scored_length()
```

Number of scored/sorted documents in this results object.

#### `__getitem__()`

```python
hit = results[0]
hits = results[0:10]
```

Get a hit by index or slice.

#### `has_matched_terms()`

```python
if results.has_matched_terms():
    print(results.matched_terms())
```

Check if matched terms were collected.

#### `iter_matched_terms()`

Iterate over (docnum, term) pairs.

#### `upgrade()`

Move docs from another Results to top.

#### `extend()`

Append docs from another Results.

#### `upgrade_and_extend()`

Upgrade docs and append rest.

#### `filtered_count`

Number of documents filtered out.

#### `collapsed_counts`

Dict of collapse keys to filtered counts.

## Hit

```python
class whoosh.searching.Hit
```

A single matched document.

### Attributes

- `hit["fieldname"]`: Stored field value
- `hit.score`: Relevance score
- `hit.docnum`: Internal document number

### Methods

#### `highlights()`

```python
snippets = hit.highlights("content", top=3)
```

Get highlighted snippets.

#### `matched_terms()`

```python
terms = hit.matched_terms()
```

Get terms that matched (if `terms=True`).

## Highlight

```python
from whoosh.highlight import highlight, Fragment

snippets = hit.highlights(
    "content",
    top=3,
    fragmenter=None,
    formatter=None
)
```

## Collectors

```python
from whoosh.collectors import Collector, FacetCollector, TimeLimitCollector
```

## Sorting and Facets

```python
from whoosh import sorting

facet = sorting.FieldFacet("category")
results = searcher.search(query, sortedby="date")
```


## DOCUMENT: Sorting

# Sorting API

Classes and functions for faceting and sorting search results. The sorting
module is a refactored package exposing the same public API as the former
monolithic module.

## Overview

Sorting and faceting use `FacetType` objects to compute sort keys for documents.
A `FacetType` creates a `Categorizer` that computes a key for each document.
The key is used for sorting and grouping. `FacetMap` objects hold the
results of grouping documents by a facet.

## Facet Types

### `FacetType`

```python
class whoosh.sorting.FacetType
```

Base class for "facets" — aspects that can be sorted and/or faceted.

**Attributes:**
- `maptype`: Default `FacetMap` class to use for this facet.

**Methods:**

#### `categorizer(global_searcher)`

Returns a `Categorizer` corresponding to this facet.

- `global_searcher`: A parent searcher for global document ID references.

#### `map(default=None)`

Returns a `FacetMap` instance for holding facet results.

#### `default_name()`

Returns the default name for this facet (default `"facet"`).

### `Categorizer`

```python
class whoosh.sorting.Categorizer
```

Base class for objects that compute a key value for a document for sorting and
faceting. Created by `FacetType` objects via `categorizer()`.

**Attributes:**
- `allow_overlap (bool)`: If `True`, use `keys_for()` to allow overlapping
  groups. Default `False`.
- `needs_current (bool)`: If `True`, the categorizer needs the matcher to be
  in a valid state when `key_for()` is called. Default `False`.

**Methods:**

#### `set_searcher(segment_searcher, docoffset)`

Called when the collector moves to a new segment. Sets up segment-specific
data.

- `segment_searcher`: The atomic sub-searcher for the current segment.
- `docoffset`: Offset of the segment's docnums relative to the full index.

#### `key_for(matcher, segment_docnum)`

Returns a sort key for the current match.

- `matcher`: A `Matcher` object. If `needs_current` is `False`, do not use
  this object as it may be inconsistent.
- `segment_docnum`: Segment-relative document number.

#### `keys_for(matcher, segment_docnum)`

Yields multiple keys for the current match. Called instead of `key_for()`
when `allow_overlap` is `True`.

#### `key_to_name(key)`

Translates the sort key into a human-readable representation for facet
group names (e.g., converts an integer date sort key to a `datetime`).

### `FieldFacet`

```python
class whoosh.sorting.FieldFacet(
    fieldname,
    reverse=False,
    allow_overlap=False,
    maptype=None
)
```

Sorts/facets by the contents of a field.

**Constructor:**
- `fieldname`: Name of the field to sort/facet on.
- `reverse`: If `True`, reverse the sort order.
- `allow_overlap`: If `True`, allow documents to appear in multiple groups
  when they have multiple terms in the field.
- `maptype`: `FacetMap` class for holding results.

```python
paths = FieldFacet("path", reverse=True)
tags = FieldFacet("tag")
results = searcher.search(myquery, sortedby=paths, groupedby=tags)
```

### `ColumnCategorizer`

Categorizer that reads values from a column for sorting. Used when a field
has a column type.

### `ReversedColumnCategorizer`

Categorizer that reverses column values for fields that are not naturally
reversible.

### `OverlappingCategorizer`

```python
class whoosh.sorting.OverlappingCategorizer
```

Categorizer used when `allow_overlap=True`. A single document can belong to
multiple facet groups.

### `PostingCategorizer`

```python
class whoosh.sorting.PostingCategorizer
```

Categorizer for fields without column values. Builds an array caching the
order of all documents. Used as a fallback; prefer setting
`sortable=True` on fields.

### `QueryFacet`

```python
class whoosh.sorting.QueryFacet(
    querydict: dict,
    other=None,
    allow_overlap=False,
    maptype=None
)
```

Sorts/facets based on the results of a series of queries.

**Constructor:**
- `querydict`: Dictionary mapping keys to `Query` objects.
- `other`: Key to use for documents matching no queries.

### `RangeFacet`

```python
class whoosh.sorting.RangeFacet(
    fieldname,
    start,
    end,
    gap,
    hardend=False,
    maptype=None
)
```

Sorts/facets based on numeric ranges. Ranges are inclusive at the start and
exclusive at the end.

```python
prices = RangeFacet("price", 0, 1000, 100)
results = searcher.search(myquery, groupedby=prices)
```

- `fieldname`: The numeric field to facet on.
- `start`: Start of the entire range.
- `end`: End of the entire range.
- `gap`: Size of each bucket (can be a sequence for progressive gaps).
- `hardend`: If `True`, clamp the last bucket to `end`.

### `DateRangeFacet`

```python
class whoosh.sorting.DateRangeFacet(
    fieldname,
    startdate,
    enddate,
    gap,
    hardend=False,
    maptype=None
)
```

Sorts/facets based on date ranges. Extends `RangeFacet` but uses
`datetime` objects for start/end and `timedelta`/`relativedelta` for gaps.
Generates `DateRange` queries instead of `TermRange` queries.

```python
from datetime import datetime
from whoosh.support.relativedelta import relativedelta

startdate = datetime(1920, 1, 1)
enddate = datetime.now()
gap = relativedelta(years=5)
bdays = DateRangeFacet("birthday", startdate, enddate, gap)
```

### `ScoreFacet`

```python
class whoosh.sorting.ScoreFacet
```

Uses a document's relevance score as a sorting criterion.

```python
tag_score = MultiFacet(["tag", ScoreFacet()])
results = searcher.search(myquery, sortedby=tag_score)
```

### `FunctionFacet`

```python
class whoosh.sorting.FunctionFacet(fn)
```

Lets you pass an arbitrary function that computes the sort key. The function
is called with `(searcher, docid)` where `docid` is an absolute index
document number.

```python
fn = lambda s, docid: s.doc_field_length(docid, "content")
lengths = FunctionFacet(fn)
```

### `TranslateFacet`

```python
class whoosh.sorting.TranslateFacet(fn, *facets)
```

Applies a custom function to the key generated by one or more wrapped facets.
Useful for custom collation, such as Unicode Collation Algorithm (UCA) sorting.

```python
from pyuca import Collator

c = Collator("allkeys.txt")
facet = FieldFacet("name")
facet = TranslateFacet(c.sort_key, facet)
results = searcher.search(myquery, sortedby=facet)
```

**Constructor:**
- `fn`: Function applied to the computed key values.
- `*facets`: One or more `FacetType` objects whose keys are passed to `fn`.

### `StoredFieldFacet`

```python
class whoosh.sorting.StoredFieldFacet(
    fieldname,
    allow_overlap=False,
    split_fn=None,
    maptype=None
)
```

Sorts/groups using the value in an unindexed, stored field (e.g., `STORED`).
Usually slower than using an indexed field.

**Constructor:**
- `fieldname`: Name of the stored field.
- `allow_overlap`: If `True`, when grouping, allow documents to appear in
  multiple groups when they have multiple values (split by `split_fn` or
  `string.split()`).
- `split_fn`: Custom function to split a stored field value into multiple
  facet values (only used when `allow_overlap=True`).

### `MultiFacet`

```python
class whoosh.sorting.MultiFacet(items=None, maptype=None)
```

Sorts/facets by the combination of multiple sub-facets.

```python
facet = MultiFacet([FieldFacet("tag"), FieldFacet("path")])
results = searcher.search(myquery, sortedby=facet)
```

Strings in the items list are treated as field names:

```python
facet = MultiFacet(["tag", "path"])
```

**Methods:**
- `from_sortedby(sortedby)`: Class method that creates a `MultiFacet` from
  a field name, facet, or list thereof.
- `add_field(fieldname, reverse=False)`: Add a `FieldFacet`.
- `add_query(querydict, other=None, allow_overlap=False)`: Add a `QueryFacet`.
- `add_score()`: Add a `ScoreFacet`.
- `add_facet(facet)`: Add an arbitrary `FacetType`.

### `Facets`

```python
class whoosh.sorting.Facets(x=None)
```

Maps facet names to `FacetType` objects for creating multiple independent
groupings of documents.

```python
facets = Facets()
facets.add_field("tag")
facets.add_facet("price", RangeFacet("price", 0, 1000, 100))
results = searcher.search(myquery, groupedby=facets)

tag_groups = results.groups("tag")
price_groups = results.groups("price")
```

**Class Methods:**
- `from_groupedby(groupedby)`: Creates a `Facets` object from a field name,
  `FacetType`, dict, list, or another `Facets` object.

**Methods:**
- `names()`: Returns an iterator of facet names.
- `items()`: Returns a list of `(name, facet)` tuples.
- `add_field(fieldname, **kwargs)`: Adds a `FieldFacet`.
- `add_query(name, querydict, **kwargs)`: Adds a `QueryFacet`.
- `add_facet(name, facet)`: Adds a `FacetType` under the given name.
- `add_facets(facets, replace=True)`: Adds the contents of a `Facets` or
  `dict` to this object.

## Facet Maps

### `FacetMap`

```python
class whoosh.sorting.FacetMap
```

Base class for objects holding the results of grouping search results by a
facet. Use `as_dict()` to access results.

```python
myfacet = FieldFacet("size", maptype=OrderedList)
myfacet = FieldFacet("size", maptype=Count)
```

**Methods:**
- `add(groupname, docid, sortkey)`: Adds a document to the facet results.
- `as_dict()`: Returns a dictionary mapping group names to values.

### `OrderedList`

```python
class whoosh.sorting.OrderedList
```

Stores a list of document numbers for each group, in sorted order.

### `UnorderedList`

```python
class whoosh.sorting.UnorderedList
```

Stores a list of document numbers for each group in arbitrary order. Slightly
faster and more memory-efficient than `OrderedList` when ordering doesn't
matter.

### `Count`

```python
class whoosh.sorting.Count
```

Stores the count of documents in each group.

### `Best`

```python
class whoosh.sorting.Best
```

Stores the "best" (highest sort key) document in each group.

## Sorting Utilities

### `add_sortable`

```python
whoosh.sorting.add_sortable(
    writer,
    fieldname,
    facet,
    column=None
)
```

Adds a per-document value column to an existing field, making it sortable.
Useful for retrofitting fields that were created without `sortable=True`.

**Example:**
```python
from whoosh import index, sorting

ix = index.open_dir("indexdir")
with ix.writer() as w:
    facet = sorting.FieldFacet("price")
    sorting.add_sortable(w, "price", facet)
```

**Parameters:**
- `writer`: An `IndexWriter` object.
- `fieldname`: Name of the field to add sortable values to.
- `facet`: A `FacetType` object to generate per-document values.
- `column`: Optional `ColumnType` to store the values. If omitted, uses the
  field's default column type.


## DOCUMENT: Spelling

# Spelling API

Functions and classes for correcting typos in user queries using edit-distance
(Damerau-Levenshtein) matching against the terms in the index.

## Corrector Objects

### `Corrector`

```python
class whoosh.spelling.Corrector
```

Base class for spelling correction objects. Concrete subclasses implement the
`_suggestions()` method.

**Methods:**

#### `suggest(text, limit=5, maxdist=2, prefix=0)`

Returns a list of suggested corrections for `text`, ranked by edit distance
then by frequency.

- `text`: The text to check. Will **not** be added to suggestions even if it
  appears in the index.
- `limit`: Maximum number of suggestions to return.
- `maxdist`: Maximum edit distance to look at (values > 2 are inefficient).
- `prefix`: Require suggestions to share this length of prefix with `text`.
  Increasing to even `1` dramatically speeds up suggestions.

#### `_suggestions(text, maxdist, prefix)`

Low-level method yielding `(score, suggestion)` tuples. Subclasses must
implement this.

### `ReaderCorrector`

```python
class whoosh.spelling.ReaderCorrector(reader, fieldname, fieldobj)
```

Suggests corrections based on terms in a specific field of an `IndexReader`.

**Ranks suggestions by edit distance, then by highest to lowest frequency.**

**Constructor:**
- `reader`: An `IndexReader` object.
- `fieldname`: The name of the field to get suggestions from.
- `fieldobj`: The `FieldType` for the field.

### `ListCorrector`

```python
class whoosh.spelling.ListCorrector(wordlist)
```

Suggests corrections based on a sorted list of strings.

**Constructor:**
- `wordlist`: A sorted list of words to match against.

### `MultiCorrector`

```python
class whoosh.spelling.MultiCorrector(correctors, op)
```

Merges suggestions from a list of sub-correctors.

**Constructor:**
- `correctors`: List of `Corrector` objects.
- `op`: A function (e.g., `max` or `operator.add`) to combine scores from
  multiple correctors for the same suggestion.

## Query Correction

### `Correction`

```python
class whoosh.spelling.Correction(q, qstring, corr_q, tokens)
```

Represents the corrected version of a user query string.

**Attributes:**
- `query`: The corrected `Query` object.
- `string`: The corrected user query string.
- `original_query`: The original `Query` object.
- `original_string`: The original user query string.
- `tokens`: List of token objects representing corrected words.

**Methods:**

#### `format_string(formatter)`

Highlights corrected words in the original query string using the given
`Formatter`.

```python
from whoosh import highlight

correction = searcher.correct_query(q, qstring)
hf = highlight.HtmlFormatter(classname="change")
html = correction.format_string(hf)
```

- `formatter`: A `Formatter` instance (or class, which will be instantiated).
- Returns: Formatted string, typically with corrections emphasized.

### `QueryCorrector`

```python
class whoosh.spelling.QueryCorrector(fieldname)
```

Base class for objects that correct words in a user query.

**Constructor:**
- `fieldname`: The default field name for corrections.

**Methods:**

#### `correct_query(q, qstring)`

Returns a `Correction` object representing the corrected form of the given
query.

- `q`: The original `Query` tree to be corrected.
- `qstring`: The original user query string (may be `None`).
- Returns: A `Correction` object.

#### `field()`

Returns the field name this corrector operates on.

### `SimpleQueryCorrector`

```python
class whoosh.spelling.SimpleQueryCorrector(
    correctors: dict,
    terms: list,
    aliases=None,
    prefix: int = 0,
    maxdist: int = 2
)
```

A simple query corrector based on a mapping of field names to `Corrector`
objects, and a list of `(fieldname, text)` tuples to correct.

**Constructor:**
- `correctors`: Dictionary mapping field names to `Corrector` objects.
- `terms`: Sequence of `(fieldname, text)` tuples representing terms to be
  corrected.
- `aliases`: Dictionary mapping field names in the query to field names for
  spelling suggestions.
- `prefix`: Suggested replacement words must share this number of initial
  characters. Default `0`.
- `maxdist`: Maximum edit distance for suggestions. Values > 2 may be slow.


## DOCUMENT: Writing

# Writing API

Write, update, and delete documents using the `IndexWriter` interface.

## IndexWriter

```python
class whoosh.writing.IndexWriter
```

Base class for writing documents.

### Context Manager

```python
with ix.writer() as writer:
    writer.add_document(title="Hello", content="World")
    # commit() called automatically
```

### Methods

#### `add_document()`

```python
writer.add_document(**fields)
```

Add a document to the index.

**Special kwargs:**
- `_stored_<fieldname>`: Alternate stored value
- `_<fieldname>_boost`: Field-specific boost
- `_boost`: Document-wide boost

---

#### `update_document()`

```python
writer.update_document(**fields)
```

Update/replace a document. Uses `unique` fields to find existing documents.

---

#### `delete_document()`

```python
writer.delete_document(docnum: int, delete: bool = True)
```

Delete by document number.

---

#### `delete_by_term()`

```python
writer.delete_by_term(fieldname: str, text: str) -> int
```

Delete all documents with term in field.

**Returns:**
- `int`: Number of documents deleted.

---

#### `delete_by_query()`

```python
writer.delete_by_query(q: Query, searcher=None) -> int
```

Delete documents matching query.

---

#### `commit()`

```python
writer.commit(
    mergetype=None,
    optimize=False,
    merge=True
)
```

Commit changes to disk.

**Args:**
- `mergetype`: Custom merge function
- `optimize`: Merge all segments into one
- `merge`: If False, don't merge existing segments

---

#### `cancel()`

```python
writer.cancel()
```

Cancel pending changes and release lock.

---

#### `add_field()`

```python
writer.add_field(fieldname: str, fieldtype, **kwargs)
```

Add a field to schema (before adding documents).

---

#### `remove_field()`

```python
writer.remove_field(fieldname: str)
```

Remove a field from schema.

---

#### `searcher()`

```python
searcher = writer.searcher(**kwargs)
```

Return a searcher (for reading during write session).

---

#### `reader()`

```python
reader = writer.reader(**kwargs)
```

Return a reader for the current state.

---

#### `group()`

```python
with writer.group():
    writer.add_document(kind="class", name="MyClass")
    writer.add_document(kind="method", name="my_method")
```

Context manager for grouping documents into one segment.

## SegmentWriter

Concrete implementation of `IndexWriter`.

### Constructor

```python
SegmentWriter(
    ix,
    poolclass=None,
    timeout=0.0,
    delay=0.1,
    _lk=True,
    limitmb=128,
    docbase=0,
    codec=None,
    compound=True,
    **kwargs
)
```

## AsyncWriter

Threaded writer that automatically retries on lock contention.

```python
from whoosh.writing import AsyncWriter

writer = AsyncWriter(
    index,
    delay=0.25,
    writerargs={}
)
```

## BufferedWriter

Buffers documents in memory and commits periodically.

```python
from whoosh.writing import BufferedWriter

writer = BufferedWriter(
    index,
    period=60,       # Max seconds between commits
    limit=100,       # Max documents per commit
    writerargs={}    # Extra args for writer
)
```

The `BufferedWriter` also provides `reader()` and `searcher()` methods for quasi-real-time search.

## Merge Policies

```python
from whoosh.writing import NO_MERGE, MERGE_SMALL, OPTIMIZE, CLEAR

writer.commit(mergetype=NO_MERGE)     # No merging
writer.commit(mergetype=MERGE_SMALL) # Merge small segments
writer.commit(mergetype=OPTIMIZE)    # Merge all into one
writer.commit(mergetype=CLEAR)       # Delete all existing segments
```

## PostingPool

Internal pool for sorting postings. Typically not used directly.

```python
class whoosh.writing.PostingPool
```

## Exceptions

### IndexingError

```python
class whoosh.writing.IndexingError(Exception)
```

Raised when an indexing operation fails.


## DOCUMENT: Analysis

:::info
Following the rename of `whoosh-reloaded` to `whoosh-ng`, new Whoosh-NG specific modules are typically found under `whoosh_modern`.
Core Whoosh components (like `whoosh.analysis`, `whoosh.index`) remain accessible directly under the `whoosh` namespace for backward compatibility.
:::

# About analyzers

## Overview

An analyzer is a function or callable class (a class with a `__call__` method)
that takes a unicode string and returns a generator of tokens. Usually a
"token" is a word, for example the string "Mary had a little lamb" might yield
the tokens "Mary", "had", "a", "little", and "lamb". However, tokens do not
necessarily correspond to words. For example, you might tokenize Chinese text
into individual characters or bi-grams. Tokens are the units of indexing, that
is, they are what you are able to look up in the index.

An analyzer is basically just a wrapper for a tokenizer and zero or more
filters. The analyzer's `__call__` method will pass its parameters to a
tokenizer, and the tokenizer will usually be wrapped in a few filters.

A tokenizer is a callable that takes a unicode string and yields a series of
`analysis.Token` objects.

For example, the provided `whoosh.analysis.RegexTokenizer` class implements a
customizable, regular-expression-based tokenizer that extracts words and
ignores whitespace and punctuation:

```python
from whoosh.analysis import RegexTokenizer

tokenizer = RegexTokenizer()
for token in tokenizer("Hello there my friend!"):
    print(repr(token.text))
# u'Hello'
# u'there'
# u'my'
# u'friend'
```

A filter is a callable that takes a generator of Tokens (either a tokenizer or
another filter) and in turn yields a series of Tokens.

For example, the provided `whoosh.analysis.LowercaseFilter()` filters tokens by
converting their text to lowercase. The implementation is very simple:

```python
def LowercaseFilter(tokens):
    """Uses lower() to lowercase token text."""
    for t in tokens:
        t.text = t.text.lower()
        yield t
```

You can wrap the filter around a tokenizer to see it in operation:

```python
from whoosh.analysis import LowercaseFilter, RegexTokenizer

tokenizer = RegexTokenizer()
for token in LowercaseFilter(tokenizer("These ARE the things I want!")):
    print(repr(token.text))
# u'these'
# u'are'
# u'the'
# u'things'
# u'i'
# u'want'
```

An analyzer is just a means of combining a tokenizer and some filters into a
single package.

You can implement an analyzer as a custom class or function, or compose
tokenizers and filters together using the `|` character:

```python
my_analyzer = RegexTokenizer() | LowercaseFilter() | StopFilter()
```

The first item must be a tokenizer and the rest must be filters (you can't put
a filter first or a tokenizer after the first item). Note that this only works
if at least the tokenizer is a subclass of `whoosh.analysis.Composable`, as all
the tokenizers and filters that ship with Whoosh are.

## Using analyzers

When you create a field in a schema, you can specify your analyzer as a keyword
argument to the field object:

```python
schema = Schema(content=TEXT(analyzer=StemmingAnalyzer()))
```

## Advanced analysis

### Token objects

The `Token` class has no methods. It is merely a place to record certain
attributes. A `Token` object actually has two kinds of attributes: *settings*
that record what kind of information the `Token` object does or should contain,
and *information* about the current token.

#### Token setting attributes

A `Token` object should always have the following attributes. A tokenizer or
filter can check these attributes to see what kind of information is available
and/or what kind of information they should be setting on the `Token` object.
Filters **should not** change the values of these attributes.

| Type | Attribute name | Description | Default |
|------|----------------|-------------|---------|
| str | mode | The mode in which the analyzer is being called, e.g. `'index'` during indexing or `'query'` during query parsing | `''` |
| bool | positions | Whether term positions are recorded in the token | `False` |
| bool | chars | Whether term start and end character indices are recorded in the token | `False` |
| bool | boosts | Whether per-term boosts are recorded in the token | `False` |
| bool | removestops | Whether stop-words should be removed from the token stream | `True` |

#### Token information attributes

A `Token` object may have any of the following attributes. The `text` attribute
should always be present. The `original` attribute may be set by a tokenizer.
All other attributes should only be accessed or set based on the values of the
"settings" attributes above.

| Type | Name | Description |
|------|------|-------------|
| unicode | text | The text of the token (this should always be present) |
| unicode | original | The original (pre-filtered) text of the token |
| int | pos | The position of the token in the stream, starting at 0 (only set if positions is True) |
| int | startchar | The character index of the start of the token in the original string (only set if chars is True) |
| int | endchar | The character index of the end of the token in the original string (only set if chars is True) |
| float | boost | The boost for this token (only set if boosts is True) |
| bool | stopped | Whether this token is a "stop" word (only set if removestops is False) |

### Performing different analysis for indexing and query parsing

Whoosh sets the `mode` setting attribute to indicate whether the analyzer is
being called by the indexer (`mode='index'`) or the query parser
(`mode='query'`). This is useful if there's a transformation that you only want
to apply at indexing or query parsing:

```python
class MyFilter(Filter):
    def __call__(self, tokens):
        for t in tokens:
            if t.mode == 'query':
                ...
            else:
                ...
```

The `whoosh.analysis.MultiFilter` filter class lets you specify different
filters to use based on the mode setting:

```python
intraword = MultiFilter(
    index=IntraWordFilter(mergewords=True, mergenums=True),
    query=IntraWordFilter(mergewords=False, mergenums=False),
)
```

### Stop words

"Stop" words are words that are so common it's often counter-productive to
index them, such as "and", "or", "if", etc. The provided `analysis.StopFilter`
lets you filter out stop words, and includes a default list of common stop
words.

```python
from whoosh.analysis import StopFilter

stopper = StopFilter()
for token in stopper(LowercaseFilter(tokenizer("These ARE the things I want!"))):
    print(repr(token.text))
# u'these'
# u'things'
# u'want'
```

#### Renumbering term positions

Remember that analyzers are sometimes asked to record the position of each
token in the token stream. So what happens to the `pos` attribute of the
tokens if `StopFilter` removes the words `had` and `a` from the stream? Should
it renumber the positions to pretend the "stopped" words never existed? Or
should it preserve the original positions of the words?

It turns out that different situations call for different solutions, so the
provided `StopFilter` class supports both of the above behaviors. Renumbering
is the default, since that is usually the most useful and is necessary to
support phrase searching. However, you can set a parameter in StopFilter's
constructor to tell it not to renumber positions:

```python
stopper = StopFilter(renumber=False)
```

#### Removing or leaving stop words

The point of using `StopFilter` is to remove stop words, right? Well, there are
actually some situations where you might want to mark tokens as "stopped" but
not remove them from the token stream.

The `removestops` parameter passed to the analyzer's `__call__` method (and
copied to the `Token` object as an attribute) specifies whether stop words
should be removed from the stream or left in.

```python
from whoosh.analysis import StandardAnalyzer

analyzer = StandardAnalyzer()
print([(t.text, t.stopped) for t in analyzer("This is a test")])
# [(u'test', False)]

print([(t.text, t.stopped) for t in analyzer("This is a test", removestops=False)])
# [(u'this', True), (u'is', True), (u'a', True), (u'test', False)]
```

The `analysis.unstopped()` filter function takes a token generator and yields
only the tokens whose `stopped` attribute is `False`.

> Even if you leave stopped words in the stream in an analyzer you use for
> indexing, the indexer will ignore any tokens where the `stopped` attribute is
> `True`.

### Implementation notes

Because object creation is slow in Python, the stock tokenizers do not create a
new `analysis.Token` object for each token. Instead, they create one `Token`
object and yield it over and over. This is a nice performance shortcut but can
lead to strange behavior if your code tries to remember tokens between loops of
the generator.

```python
# WRONG: the generator reuses the same Token object
print(list(tokenizer("Hello there my friend")))
# [Token(u"friend"), Token(u"friend"), Token(u"friend"), Token(u"friend")]

# RIGHT: save the attributes, not the token object
print([t.text for t in tokenizer("Hello there my friend")])
# [u'Hello', u'there', u'my', u'friend']
```

If you implement your own tokenizer, filter, or analyzer as a class, you should
implement an `__eq__` method. This is important to allow comparison of `Schema`
objects.

## See also

- [Stemming & Stop Words](/core/stemming) — Practical stemming and stop-word guides
- [N-grams](/core/ngrams) — Substring and prefix matching with N-gram analyzers
- [API: analysis](../api/analysis) — Full reference for analyzers, tokenizers, and filters


## DOCUMENT: Backends

# Backends

Whoosh-NG supports pluggable storage backends through the Provider Architecture. The default backend stores data as files on disk, but you can use SQLite, PostgreSQL, S3, and more.

## Built-in Backends

| Backend | Class | Description |
|---------|-------|-------------|
| File (default) | `FileBackend` | Stores index as files on disk |
| SQLite | `SQLiteBackend` | Stores index in SQLite database |
| Memory | `MemoryBackend` | In-memory backend (testing only) |

## File Backend (Default)

```python
from whoosh.index import create_in

# Uses FileBackend by default
ix = create_in("indexdir", schema)
```

### Configuration

```python
from whoosh.backends.file import FileBackend

backend = FileBackend(
    storage=FileStorage("indexdir"),
    compound=True  # Use compound files
)
```

## SQLite Backend

```python
from whoosh.backends.sqlite import SQLiteBackend
from whoosh.store.sqlite import SQLiteStorage

storage = SQLiteStorage("index.db")
backend = SQLiteBackend(storage=storage)
```

### Advantages

- Single file index
- Better for transactional workloads
- Easier backups
- Supports concurrent reads

### Disadvantages

- Slower for large indexes
- Limited by SQLite performance

## Memory Backend

```python
from whoosh.backends.memory import MemoryBackend

backend = MemoryBackend()
# Useful for testing
```

## Custom Backend

Create a custom backend by subclassing `Backend`:

```python
from whoosh.backends.abc import Backend

class MyBackend(Backend):
    def create(self):
        """Create a new segment."""
        pass

    def open(self):
        """Open existing segment."""
        pass

    def close(self):
        """Close the backend."""
        pass

    def commit(self):
        """Commit changes."""
        pass
```

## Registering a Backend

```python
from whoosh.registry import BackendRegistry

BackendRegistry.register("my_backend", MyBackend, "my_package")
```

## Backend Selection

Choose a backend based on your use case:

| Use Case | Recommended Backend |
|----------|---------------------|
| Small to medium indexes | File (default) |
| Single-file deployment | SQLite |
| Testing | Memory |
| Distributed systems | Object storage (S3, MinIO) |
| High concurrency | SQLite or custom |

## Best Practices

1. **File backend for production**: Most battle-tested
2. **SQLite for single-file**: Easier deployment
3. **Memory for tests**: Fast, no cleanup needed
4. **Compound files**: Enable for reduced file count
5. **Backup strategy**: File backend = copy directory; SQLite = copy file


## DOCUMENT: Batch

# Tips for speeding up batch indexing

## Overview

Indexing documents tends to fall into two general patterns: adding documents
one at a time as they are created (as in a web application), and adding a bunch
of documents at once (batch indexing).

The following settings and alternate workflows can make batch indexing faster.

## StemmingAnalyzer cache

The stemming analyzer by default uses a least-recently-used (LRU) cache to
limit the amount of memory it uses, to prevent the cache from growing very
large if the analyzer is reused for a long period of time. However, the LRU
cache can slow down indexing by almost 200% compared to a stemming analyzer
with an "unbounded" cache.

When you're indexing in large batches with a one-shot instance of the analyzer,
consider using an unbounded cache:

> **Note**: For new implementations or complex multilingual scenarios, consider
> using the `CachedStemmingAnalyzer` (from `whoosh_modern.analysis.cached_stemming_analyzer`)
> which offers integrated LRU caching and flexible configuration.

```python
w = myindex.writer()
# Get the analyzer object from a text field
stem_ana = w.schema["content"].analyzer
# Set the cachesize to -1 to indicate unbounded caching
stem_ana.cachesize = -1
# Reset the analyzer to pick up the changed attribute
stem_ana.clear()

# Use the writer to index documents...
```

## The `limitmb` parameter

The `limitmb` parameter to `whoosh.index.Index.writer()` controls the
*maximum* memory (in megabytes) the writer will use for the indexing pool. The
higher the number, the faster indexing will be.

The default value of `128` is actually somewhat low, considering many people
have multiple gigabytes of RAM these days. Setting it higher can speed up
indexing considerably:

```python
from whoosh import index

ix = index.open_dir("indexdir")
writer = ix.writer(limitmb=256)
```

> The actual memory used will be higher than this value because of interpreter
> overhead (up to twice as much!). It is very useful as a tuning parameter, but
> not for trying to exactly control the memory usage of Whoosh.

## The `procs` parameter

The `procs` parameter to `whoosh.index.Index.writer()` controls the number of
processors the writer will use for indexing (via the `multiprocessing` module):

```python
from whoosh import index

ix = index.open_dir("indexdir")
writer = ix.writer(procs=4)
```

When you use multiprocessing, the `limitmb` parameter controls the amount of
memory used by *each process*, so the actual memory used will be
`limitmb * procs`:

```python
# Each process will use a limit of 128, for a total of 512
writer = ix.writer(procs=4, limitmb=128)
```

## The `multisegment` parameter

The `procs` parameter causes the default writer to use multiple processors to
do much of the indexing, but then still uses a single process to merge the pool
of each sub-writer into a single segment.

You can get much better indexing speed by also using the `multisegment=True`
keyword argument, which instead of merging the results of each sub-writer,
simply has them each just write out a new segment:

```python
from whoosh import index

ix = index.open_dir("indexdir")
writer = ix.writer(procs=4, multisegment=True)
```

The drawback is that instead of creating a single new segment, this option
creates a number of new segments **at least** equal to the number of processors
you use. For example, if you use `procs=4`, the writer will create four new
segments.

So, while `multisegment=True` is much faster than a normal writer, you should
only use it for large batch indexing jobs (or perhaps only for indexing from
scratch). It should not be the only method you use for indexing, because
otherwise the number of segments will tend to increase forever!

## See also

- [Indexing](/core/indexing) — Writer options and merge policies
- [API: writing](../api/writing) — `Index.writer()` parameters


## DOCUMENT: Changelog

# Changelog

Release notes for Whoosh-NG, auto-generated from GitHub releases and commit messages.

## v5.1.0 (2026-08-11)
**Tag**: `v5.1.0`

## v5.1.0 (2026-08-11)

_This release is published under the BSD-2-Clause License._

### Bug Fixes

- **config,linguistics**: Resolve mypy/pyright issues in config loader and yaml provider ([`3f59f96`](https://github.com/dorel14/whoosh-ng/commit/3f59f9607398547225b090a2eddc0fc7e1f62efd))

- **fastapi**: Correct WebSocket autocomplete test assertion ([`b9b1d57`](https://github.com/dorel14/whoosh-ng/commit/b9b1d57524b6f121bff60833c6b5563264bc4152))

- **storage**: Validate SnapshotStorage key before remote read ([`3ec0b43`](https://github.com/dorel14/whoosh-ng/commit/3ec0b43120e05578a4191d25e0d66b10ab06b914))

- **tests**: Import CoreStorageAdapter in test_storage_providers.py ([`18484b0`](https://github.com/dorel14/whoosh-ng/commit/18484b00c2e3f66e1baab76349d28694a3fde45a))

### Documentation

- Add Configuration Engine docs, update CHANGELOG, FastAPI WebSocket, CoreStorageAdapter, and SnapshotStorage fix ([`5e8ec4a`](https://github.com/dorel14/whoosh-ng/commit/5e8ec4ae49aaa64d66c554751f7cd67d9ca37f31))

- Auto-update llms context files ([`916bb34`](https://github.com/dorel14/whoosh-ng/commit/916bb34e16c4b5adf7cf6cc7a1449864db60873a))

- Auto-update llms context files ([`5b6b9a6`](https://github.com/dorel14/whoosh-ng/commit/5b6b9a63346559febe51c16a23bbe1602188a2c2))

- Auto-update llms context files ([`25f4477`](https://github.com/dorel14/whoosh-ng/commit/25f4477091e5506c58bfe6787e2fa7a6fcc22943))

- Auto-update llms context files [skip ci] ([`127bc4a`](https://github.com/dorel14/whoosh-ng/commit/127bc4aa3277c109f73d932e878be90807c4aafc))

- **config**: Clarify list merge behavior, refactor PyYAML import, document pre-push hook rationale ([`3d122d5`](https://github.com/dorel14/whoosh-ng/commit/3d122d58b85d10b85bf079ad9d3d15acdeab09e2))

- **config**: Clarify list merge rationale and make unsupported format error dynamic ([`25076b6`](https://github.com/dorel14/whoosh-ng/commit/25076b61ab91df44cd81327bf827732325fc1aab))

- **fastapi,config**: Clarify optional FastAPI dependency and warn on list merge behavior ([`e917567`](https://github.com/dorel14/whoosh-ng/commit/e917567509d1b1d84bb27b7ceb75b1b8c15941a6))

### Features

- Add PyYAML extra, make WebSocket limit configurable, validate ConfigEngine priority ([`5fb4a38`](https://github.com/dorel14/whoosh-ng/commit/5fb4a382ccda5e6408ec93ce1e6e2efa8fd583d7))

- **config**: Implement Configuration Engine core with Pydantic models, YAML/JSON loader, and hierarchical merging ([`de7bbca`](https://github.com/dorel14/whoosh-ng/commit/de7bbcadd384391adef090f1944865490b93d8fe))

- **fastapi**: Make WebSocket limit configurable and run autocomplete off the event loop ([`c538ea6`](https://github.com/dorel14/whoosh-ng/commit/c538ea6af2195bd292035c5de3e520577f090bc4))

- **storage**: Add CoreStorageAdapter wrapping core FileStorage for SyncStorageProvider ([`52ab7e6`](https://github.com/dorel14/whoosh-ng/commit/52ab7e614de1f92a7ac8d7afbf95bbc52aaba0e2))

- **website**: Ajouter les fichiers de configuration du site statique ([`103fcff`](https://github.com/dorel14/whoosh-ng/commit/103fcff73199a4e8e126f7f887d0f31bcd003999))

---

**Detailed Changes**: [v5.0.0...v5.1.0](https://github.com/dorel14/whoosh-ng/compare/v5.0.0...v5.1.0)

### Commits

### Code Refactoring

- Sprint D cleanup + P1-1/P3 dedup fixes
- improve WebSocket error handling and document list merge behavior

### Features

- add CoreStorageAdapter wrapping core FileStorage for SyncStorageProvider
- implement Configuration Engine core with Pydantic models, YAML/JSON loader, and hierarchical merging
- ajouter les fichiers de configuration du site statique
- add PyYAML extra, make WebSocket limit configurable, validate ConfigEngine priority
- make WebSocket limit configurable and run autocomplete off the event loop

### Bug Fixes

- correct WebSocket autocomplete test assertion
- validate SnapshotStorage key before remote read
- import CoreStorageAdapter in test_storage_providers.py
- resolve mypy/pyright issues in config loader and yaml provider

### Other

- .
- Rename LICENSE.txt to LICENSE_OLD.txt
- Merge branch 'dev' of https://github.com/dorel14/whoosh-ng into dev
- Merge branch 'dev' of https://github.com/dorel14/whoosh-ng into dev
- Merge pull request #418 from dorel14/dev

### Chores

- merge master into dev [skip ci]
- synchronize version from pyproject.toml [skip ci]
- apply pre-commit fixes
- apply pre-commit fixes
- v5.1.0 [skip ci]

### Documentation

- add Configuration Engine docs, update CHANGELOG, FastAPI WebSocket, CoreStorageAdapter, and SnapshotStorage fix
- auto-update llms context files
- auto-update llms context files
- clarify list merge behavior, refactor PyYAML import, document pre-push hook rationale
- auto-update llms context files
- clarify list merge rationale and make unsupported format error dynamic
- clarify optional FastAPI dependency and warn on list merge behavior
- auto-update llms context files [skip ci]

### CI/CD

- regenerate LLM context files only on documentation changes


[View on GitHub](https://github.com/dorel14/whoosh-ng/releases/tag/v5.1.0)

## v5.0.0 (2026-08-11)
**Tag**: `v5.0.0`

## v5.0.0 (2026-08-11)

_This release is published under the BSD-2-Clause License._

### Bug Fixes

- **ci**: Ensure git push runs even when pre-commit commit is a no-op ([`b4fa710`](https://github.com/dorel14/whoosh-ng/commit/b4fa710bfb979a764373b1b9dc67e60f0a84ff0e))

- **ci**: Restore || true suppression for git pull --rebase in test workflow ([`6b8ae46`](https://github.com/dorel14/whoosh-ng/commit/6b8ae46f08908aa132b55261643589993de11ba9))

- **indexing**: Avoid NameError when ix.writer() fails in ParallelIndexBuilder ([`b629fed`](https://github.com/dorel14/whoosh-ng/commit/b629feda7398ece400636ef1ab7bf1df742c4255))

- **linguistics**: Restore constructor-style calls for language analyzers ([`8307731`](https://github.com/dorel14/whoosh-ng/commit/8307731b5958654307f6014743fe1e39700ba547))

- **s3**: Corriger l'ordre de validation des chemins dans SnapshotStorage ([`4fdf384`](https://github.com/dorel14/whoosh-ng/commit/4fdf384158249d66937f16976307db01f232ddf0))

- **storage**: Sanitize S3 keys in SnapshotStorage.read to prevent path traversal ([`b57b00d`](https://github.com/dorel14/whoosh-ng/commit/b57b00d84e2adc8d73e83a93bdc9813eeeff69bf))

### Documentation

- Auto-update llms context files ([`6318ddd`](https://github.com/dorel14/whoosh-ng/commit/6318dddda1db67f1d9ffc1492038340eafd2b62e))

- Auto-update llms context files ([`eb618ab`](https://github.com/dorel14/whoosh-ng/commit/eb618ab65aceb212b417a09d5c2ca74e3296bd0d))

### Features

- **workflows**: Amélioration des workflows CI et ajout de la section LLM Context ([`359e19a`](https://github.com/dorel14/whoosh-ng/commit/359e19a79002a1b67ea8faeef868a0cbe877612b))

---

**Detailed Changes**: [v4.3.0...v5.0.0](https://github.com/dorel14/whoosh-ng/compare/v4.3.0...v5.0.0)

### Commits

### Other

- Merge pull request #15 from dorel14/master
- Merge branch 'master' into dev
- Merge branch 'dev' of https://github.com/dorel14/whoosh-ng into dev
- Merge branch 'master' into feat/last_updates
- Merge pull request #417 from dorel14/feat/last_updates

### Documentation

- auto-update llms context files
- rename sprint-c/d docs, add provider-integration guide, sync website docs
- auto-update llms context files
- auto-update llms context files
- auto-update llms context files
- auto-update llms context files

### Chores

- synchronize version from pyproject.toml [skip ci]
- synchronize version from pyproject.toml [skip ci]
- apply pre-commit fixes
- synchronize version from pyproject.toml [skip ci]
- v5.0.0 [skip ci]

### Code Refactoring

- modernisation architecturale et unification sur les composants core
- améliorations de la sécurité S3 et du typage statique

### CI/CD

- ajout de la détection des doublons de code

### Bug Fixes

- sanitize S3 keys in SnapshotStorage.read to prevent path traversal
- restore constructor-style calls for language analyzers
- corriger l'ordre de validation des chemins dans SnapshotStorage
- ensure git push runs even when pre-commit commit is a no-op
- restore || true suppression for git pull --rebase in test workflow
- avoid NameError when ix.writer() fails in ParallelIndexBuilder

### Features

- amélioration des workflows CI et ajout de la section LLM Context


[View on GitHub](https://github.com/dorel14/whoosh-ng/releases/tag/v5.0.0)

## v4.3.0 (2026-08-09)
**Tag**: `v4.3.0`

## v4.3.0 (2026-08-09)

_This release is published under the BSD-2-Clause License._

### Bug Fixes

- Add actions: read permission to workflow_run triggers ([`d1cb49c`](https://github.com/dorel14/whoosh-ng/commit/d1cb49c7d5d84b4d790090d8b24250bb0bf5791b))

- Normalize line endings to LF in workflow files ([`d68513b`](https://github.com/dorel14/whoosh-ng/commit/d68513b84b97115f1708412e731c6bfcc7a2b6b1))

- Use repository_dispatch instead of workflow_run for Pages trigger ([`c601ccb`](https://github.com/dorel14/whoosh-ng/commit/c601ccbc6469b4048083ab71c334f937422b2b2f))

- **pages**: Ensure Pages deploys on all master pushes + changelog sidebar fix ([`d5d297f`](https://github.com/dorel14/whoosh-ng/commit/d5d297f17ef941e5575f7f0ac7f5c99d0c188f76))

### Features

- **website**: Ajouter des sidebars dédiées et refondre la page de référence API ([`a69d3c9`](https://github.com/dorel14/whoosh-ng/commit/a69d3c9c9093fa78c246010f79d751ad36c8b9b0))

- **website**: Ajouter des sidebars dédiées par section de documentation ([`ffaf1a9`](https://github.com/dorel14/whoosh-ng/commit/ffaf1a969c53c80fa2d968f5f733b40ae9e2cc00))

---

**Detailed Changes**: [v4.2.3...v4.3.0](https://github.com/dorel14/whoosh-ng/compare/v4.2.3...v4.3.0)

### Commits

### Chores

- synchronize version from pyproject.toml [skip ci]
- synchronize version from pyproject.toml [skip ci]
- apply pre-commit fixes
- synchronize version from pyproject.toml [skip ci]
- v4.3.0 [skip ci]

### CI/CD

- restructurer la chaîne de déploiement CI/CD et optimiser les workflows
- ajouter des garde-fous et corriger le script de synchronisation de version
- améliorer la logique de déclenchement du déploiement GitHub Pages
- ajouter un déclenchement push pour les modifications de documentation

### Other

- Merge branch 'master' of https://github.com/dorel14/whoosh-ng

### Features

- ajouter des sidebars dédiées par section de documentation
- ajouter des sidebars dédiées et refondre la page de référence API

### Code Refactoring

- simplifier la configuration des sidebars

### Bug Fixes

- normalize line endings to LF in workflow files
- ensure Pages deploys on all master pushes + changelog sidebar fix
- add actions: read permission to workflow_run triggers
- use repository_dispatch instead of workflow_run for Pages trigger


[View on GitHub](https://github.com/dorel14/whoosh-ng/releases/tag/v4.3.0)

## v4.2.3 (2026-08-08)
**Tag**: `v4.2.3`

## v4.2.3 (2026-08-08)

_This release is published under the BSD-2-Clause License._

### Bug Fixes

- **ci**: Add concurrency groups to prevent workflow race conditions ([`76e38f0`](https://github.com/dorel14/whoosh-ng/commit/76e38f03045d19b8d8032f066432af4e9d17d133))

---

**Detailed Changes**: [v4.2.2...v4.2.3](https://github.com/dorel14/whoosh-ng/compare/v4.2.2...v4.2.3)

### Commits

### Chores

- synchronize version from pyproject.toml [skip ci]
- sync version to 4.2.1 [skip ci]
- v4.2.3 [skip ci]

### Other

- Merge: resolve sync-version conflicts (CI and local generated same changes)

### Bug Fixes

- add concurrency groups to prevent workflow race conditions


[View on GitHub](https://github.com/dorel14/whoosh-ng/releases/tag/v4.2.3)

## v4.2.2 (2026-08-08)
**Tag**: `v4.2.2`

## v4.2.2 (2026-08-08)

_This release is published under the BSD-2-Clause License._

### Bug Fixes

- **ci**: Fix changelog workflow detached HEAD ([`db4b78e`](https://github.com/dorel14/whoosh-ng/commit/db4b78e6dc51a0e10fde48fd93eb2ce5d1dbecd0))

---

**Detailed Changes**: [v4.2.1...v4.2.2](https://github.com/dorel14/whoosh-ng/compare/v4.2.1...v4.2.2)

### Commits

### Chores

- synchronize version from pyproject.toml [skip ci]
- v4.2.2 [skip ci]

### Bug Fixes

- fix changelog workflow detached HEAD


[View on GitHub](https://github.com/dorel14/whoosh-ng/releases/tag/v4.2.2)

## v4.2.1 (2026-08-07)
**Tag**: `v4.2.1`

## v4.2.1 (2026-08-07)

_This release is published under the BSD-2-Clause License._

---

**Detailed Changes**: [v4.2.0...v4.2.1](https://github.com/dorel14/whoosh-ng/compare/v4.2.0...v4.2.1)

### Commits

### Bug Fixes

- fix sync-changelog.yml YAML syntax error

### Other

- Merge branch 'master' of https://github.com/dorel14/whoosh-ng
- i18n(fr): translate nested.md, storage-providers.md, stemming.md, ngrams.md, glossary.md

### Chores

- synchronize version to 4.2.0 [skip ci]
- synchronize version from pyproject.toml [skip ci]
- v4.2.1 [skip ci]


[View on GitHub](https://github.com/dorel14/whoosh-ng/releases/tag/v4.2.1)

## v4.2.0 (2026-08-07)
**Tag**: `v4.2.0`

## v4.2.0 (2026-08-07)

_This release is published under the BSD-2-Clause License._

### Bug Fixes

- **ci**: Fix changelog workflow typo and pages.yml checkout depth ([`d88b82b`](https://github.com/dorel14/whoosh-ng/commit/d88b82b68f7ea2ed28c4530cbda1ecad77308acf))

### Features

- **docs**: Enable dark theme by default and remove edit button ([`2c3d98b`](https://github.com/dorel14/whoosh-ng/commit/2c3d98b27087125073729505cba156d1c2578d66))

---

**Detailed Changes**: [v4.1.0...v4.2.0](https://github.com/dorel14/whoosh-ng/compare/v4.1.0...v4.2.0)

### Commits

### Chores

- synchronize version from pyproject.toml [skip ci]
- v4.2.0 [skip ci]

### Features

- enable dark theme by default and remove edit button

### Bug Fixes

- fix changelog workflow typo and pages.yml checkout depth


[View on GitHub](https://github.com/dorel14/whoosh-ng/releases/tag/v4.2.0)

## v4.1.0 (2026-08-07)
**Tag**: `v4.1.0`

## v4.1.0 (2026-08-07)

_This release is published under the BSD-2-Clause License._

### Bug Fixes

- **ci**: Fix broken workflows and Docusaurus build errors after migration ([`0e86e6c`](https://github.com/dorel14/whoosh-ng/commit/0e86e6c51e3d41fec984f0f79a4457e9d5d0b1ef))

- **ci**: Fix sync_version.py NameError and missing imports ([`f809f56`](https://github.com/dorel14/whoosh-ng/commit/f809f563851b102c7220cec0371c560191bdb217))

### Features

- **docs**: Migrate Jekyll/Just the Docs to Docusaurus v3 ([`7e70791`](https://github.com/dorel14/whoosh-ng/commit/7e70791d23fb0f3097e3603ba0ff3fa5c8d822c2))

---

**Detailed Changes**: [v4.0.1...v4.1.0](https://github.com/dorel14/whoosh-ng/compare/v4.0.1...v4.1.0)

### Commits

### Chores

- synchronize version from pyproject.toml [skip ci]
- v4.1.0 [skip ci]

### Features

- migrate Jekyll/Just the Docs to Docusaurus v3

### Bug Fixes

- fix broken workflows and Docusaurus build errors after migration
- fix sync_version.py NameError and missing imports


[View on GitHub](https://github.com/dorel14/whoosh-ng/releases/tag/v4.1.0)

## v4.0.1 (2026-08-07)
**Tag**: `v4.0.1`

## v4.0.1 (2026-08-07)

_This release is published under the BSD-2-Clause License._

### Bug Fixes

- **pages**: Pointer Bundler sur docs/Gemfile via BUNDLE_GEMFILE ([`0f3a8f1`](https://github.com/dorel14/whoosh-ng/commit/0f3a8f11087c5df910d1762f31ae1cee4017ed31))

---

**Detailed Changes**: [v4.0.0...v4.0.1](https://github.com/dorel14/whoosh-ng/compare/v4.0.0...v4.0.1)

### Commits

### Chores

- synchronize version from pyproject.toml [skip ci]
- v4.0.1 [skip ci]

### Bug Fixes

- pointer Bundler sur docs/Gemfile via BUNDLE_GEMFILE


[View on GitHub](https://github.com/dorel14/whoosh-ng/releases/tag/v4.0.1)

## v4.0.0 (2026-08-07)
**Tag**: `v4.0.0`

## v4.0.0 (2026-08-07)

_This release is published under the BSD-2-Clause License._

### Bug Fixes

- **ci**: Corriger les erreurs de lint, mypy et pyright ([`4283355`](https://github.com/dorel14/whoosh-ng/commit/428335590e641870f6bc399b241b453745e4b9a3))

---

**Detailed Changes**: [v3.0.0...v4.0.0](https://github.com/dorel14/whoosh-ng/compare/v3.0.0...v4.0.0)

### Commits

### Features

- restructurer le système de middleware et étendre PluginManager
- ajouter le module linguistics avec analyseurs multilingues
- add asearch/awriter bridges and AsyncFileStorage
- enhance FastAPI models and Admin Studio modules
- add SearchApplication and FileStorage exports
- add S3Storage, HybridStorage, AsyncHybridStorage
- add SnapshotStorage, CachedObjectStorage alias, and Phase 3 roadmap
- publier la version 3.0.0 et ajouter la documentation LLM

### Documentation

- ajouter les guides Whoosh-NG 2.0 et ajuster la configuration de release
- add Gemfile.lock for reproducible Jekyll build and fix French index permalinks
- add S3 storage benchmarks and documentation
- auto-update llms context files

### Chores

- restructurer les workflows CI/CD et nettoyer le code
- apply pre-commit fixes
- v4.0.0 [skip ci]

### Other

- Merge branch 'master' into dev
- Merge pull request #14 from dorel14/dev

### Bug Fixes

- corriger les erreurs de lint, mypy et pyright


[View on GitHub](https://github.com/dorel14/whoosh-ng/releases/tag/v4.0.0)

## v3.0.0 (2026-08-06)
**Tag**: `v3.0.0`

## v3.0.0 (2026-08-06)

_This release is published under the BSD-2-Clause License._

### Bug Fixes

- Ajouter reportAssignmentType en warning dans pyrightconfig.json ([`3259c76`](https://github.com/dorel14/whoosh-ng/commit/3259c76660a4f99135a8cda1ac26baa3f642a873))

- Corriger docstring PathTokenizer et restaurer Literal pour engine ([`b90222e`](https://github.com/dorel14/whoosh-ng/commit/b90222e5962671dfb20f496d396f546f33c95747))

- Mapper engine pyarrow vers auto pour pandas et exclure tests/ de mypy ([`dadeb91`](https://github.com/dorel14/whoosh-ng/commit/dadeb91cffd9462d0c5c2aff77fd36f4cbac8f4a))

- Ne pas yield de token vide dans RegexTokenizer gaps=True ([`6385a24`](https://github.com/dorel14/whoosh-ng/commit/6385a24cb048777aef9cc5d2bad46f2e5d2e5dfa))

- Remove unreachable dead code after build() return in parallel_builder ([`c2e03c9`](https://github.com/dorel14/whoosh-ng/commit/c2e03c97bda6ac61d78c38c6ac3ad37df4f9345a))

- Rendre test_buffered_threads deterministe en couverture des valeurs ([`77a66f6`](https://github.com/dorel14/whoosh-ng/commit/77a66f6320f9ff7bfe8a1c787c52d1bb0e3cf0d6))

- Resolve mypy errors in CI (parquet, tortoise, hnswlib) ([`514f1f6`](https://github.com/dorel14/whoosh-ng/commit/514f1f62d55edd92633654e0096caf920a9ff3a6))

- Réintégrer assert segment_reader is not None après merge distant ([`c42b065`](https://github.com/dorel14/whoosh-ng/commit/c42b065355bb0004094223748a7c9b6eca810405))

- Résoudre les erreurs pyright dans pre-commit (Token typing, reportAttributeAccessIssue) ([`eff5adf`](https://github.com/dorel14/whoosh-ng/commit/eff5adf55cbbae6169bf2efacc76fddacc1891dd))

- Résoudre les erreurs pyright reportOptionalMemberAccess et reportAssignmentType ([`572529e`](https://github.com/dorel14/whoosh-ng/commit/572529e1fb7f0828f3729ed7920e219a71df139c))

- Supprimer cast redondant et ajouter reportAssignmentType en warning ([`0834923`](https://github.com/dorel14/whoosh-ng/commit/0834923f185a4fb62ded3e5020ea6d36b90bfc42))

- **analysis**: Corriger l'indentation du RegexTokenizer et ajuster les annotations de type ([`69e9ad8`](https://github.com/dorel14/whoosh-ng/commit/69e9ad815192ed9113acea4075e21eaf9e514f8d))

- **deps**: Retirer les modules obsolètes des exclusions mypy ([`f53e157`](https://github.com/dorel14/whoosh-ng/commit/f53e15729647ac7473066f193a33fcbfb2a3dce0))

- **indexing**: Close segment_ix in ParallelIndexBuilder to prevent fd leak ([`bad2928`](https://github.com/dorel14/whoosh-ng/commit/bad2928155d424e19de24e5b00822831695e7449))

- **indexing**: Corriger les fuites de handles et erreurs de nettoyage sous Windows ([`3cb85c7`](https://github.com/dorel14/whoosh-ng/commit/3cb85c7d9f80eccdbb5d2225d270d9f4c15b79bd))

- **indexing**: Merge parallel worker segments into main index ([`6f18248`](https://github.com/dorel14/whoosh-ng/commit/6f182488b18aedaceaf4c886b1b0d87eadc02a99))

- **indexing**: Merge worker segments into main index in ParallelIndexBuilder ([`27b0541`](https://github.com/dorel14/whoosh-ng/commit/27b054140d1a99228a804b75a1d4efd3e4f300ce))

- **mypy**: Restore ignore_missing_imports for pytest, peewee, httpx, re2, psutil ([`699ee4f`](https://github.com/dorel14/whoosh-ng/commit/699ee4f2fb3007012913d5970376f97fc99a0296))

- **profiling**: Add segment_write() and sibling step context managers to CommitProfilerV2 ([`599b650`](https://github.com/dorel14/whoosh-ng/commit/599b6507149332b3e0522b753e547bae718b468e))

- **profiling**: Implement SegmentProfiler to resolve NameError in benchmark.py ([`1b03b3a`](https://github.com/dorel14/whoosh-ng/commit/1b03b3ae7fbc3d03c7223ab4f7759eb500c9c8bf))

### Documentation

- Auto-update llms context files ([`5cca243`](https://github.com/dorel14/whoosh-ng/commit/5cca243cba55d49836e89793af3ac126815b862c))

- Auto-update llms context files ([`55825c1`](https://github.com/dorel14/whoosh-ng/commit/55825c15502f0b15041aec9ac15d7f9c4bc23e6c))

- Auto-update llms context files ([`c0d74a4`](https://github.com/dorel14/whoosh-ng/commit/c0d74a4cda58a44898a0b3f67fdc009fefa565fe))

- Restructurer la documentation et ajouter les pages API et guides ([`4e9b1e2`](https://github.com/dorel14/whoosh-ng/commit/4e9b1e2ce8a494c66f1725cec3b03e5025643e8d))

- **guides**: Ajouter le guide d'indexation moderne et mettre à jour les index de documentation ([`9fbc4ff`](https://github.com/dorel14/whoosh-ng/commit/9fbc4ff41dafc184219f0b4f1da3ee4f3e0ecb71))

### Features

- Ajouter FastCSVSource, indexation par lots, infrastructure de profiling et optimisations du cœur ([`fc63f9c`](https://github.com/dorel14/whoosh-ng/commit/fc63f9c890155fa9ce0b25c03e9db733eb7f0139))

- **analysis**: Ajouter le système de stemmers, FastCSVSource et l'infrastructure de profiling des performances ([`a077319`](https://github.com/dorel14/whoosh-ng/commit/a077319a8fc7435ef283b5c2b5402538c6550819))

- **core**: Ajouter CacheMiddleware et ObservableDataSource ([`508589a`](https://github.com/dorel14/whoosh-ng/commit/508589a8c86c7d1eadeb6ffd044a340fe84363be))

- **data-sources**: Ajouter les sources de données et le pooling de connexions ([`51805d4`](https://github.com/dorel14/whoosh-ng/commit/51805d4c08109b78704f6938deb4c135f57b0674))

- **data-sources**: Améliorer la robustesse et la validation des sources de données ([`35281b5`](https://github.com/dorel14/whoosh-ng/commit/35281b534865111e30d0d1195c492d72b8769e75))

- **profiling**: Ajouter les groupes d'options profiling et fast-stemming, stream_batches et restructurer les chemins d'import des sources de données ([`152e9c4`](https://github.com/dorel14/whoosh-ng/commit/152e9c44b68380a9d238489a29ebfd52e28f97a9))

---

**Detailed Changes**: [v2.0.0...v3.0.0](https://github.com/dorel14/whoosh-ng/compare/v2.0.0...v3.0.0)

### Commits

### Other

- Remove workflows permission from test.yml
- Revise README for version 2.0.0 updates
- Merge pull request #12 from dorel14/master
- Merge branch 'dev' of https://github.com/dorel14/whoosh-ng into dev
- Merge branch 'dev' of https://github.com/dorel14/whoosh-ng into dev
- Potential fix for pull request finding 'CodeQL / Workflow does not contain permissions'
- Merge branch 'dev' of https://github.com/dorel14/whoosh-ng into dev
- Merge branch 'dev' of https://github.com/dorel14/whoosh-ng into dev
- Merge branch 'dev' of https://github.com/dorel14/whoosh-ng into dev
- .
- Update src/whoosh_modern/indexing/parallel_builder.py
- codec/base: restore missing out-of-order term check in add_postings
- Update src/whoosh_modern/profiling/segment_profiler.py
- Merge branch 'dev' of https://github.com/dorel14/whoosh-ng into dev
- Merge branch 'dev' of https://github.com/dorel14/whoosh-ng into dev
- Merge c42b065355bb0004094223748a7c9b6eca810405 into 9988770df03a2d255a21b74843772776dbbf998a
- Merge pull request #13 from dorel14/dev
- Update commit_parser_options in pyproject.toml
- Add files via upload

### Build System

- automatiser la synchronisation de la version entre les fichiers du projet

### Chores

- apply pre-commit fixes
- simplifier la configuration type-checking et retirer les dépendances inutilisées
- apply pre-commit fixes
- apply pre-commit fixes
- trigger pre-commit workflow
- v3.0.0 [skip ci]

### Features

- ajouter CacheMiddleware et ObservableDataSource
- ajouter les sources de données et le pooling de connexions
- ajouter FastCSVSource, indexation par lots, infrastructure de profiling et optimisations du cœur
- ajouter le système de stemmers, FastCSVSource et l'infrastructure de profiling des performances
- ajouter les groupes d'options profiling et fast-stemming, stream_batches et restructurer les chemins d'import des sources de données
- améliorer la robustesse et la validation des sources de données

### Code Refactoring

- simplifier les expressions multi-lignes et optimiser Token avec __slots__
- nettoyer les annotations de type et supprimer les dépendances inutilisées

### Documentation

- auto-update llms context files
- restructurer la documentation et ajouter les pages API et guides
- auto-update llms context files
- ajouter le guide d'indexation moderne et mettre à jour les index de documentation
- auto-update llms context files

### Bug Fixes

- corriger l'indentation du RegexTokenizer et ajuster les annotations de type
- resolve mypy errors in CI (parquet, tortoise, hnswlib)
- ne pas yield de token vide dans RegexTokenizer gaps=True
- résoudre les erreurs pyright dans pre-commit (Token typing, reportAttributeAccessIssue)
- supprimer cast redondant et ajouter reportAssignmentType en warning
- ajouter reportAssignmentType en warning dans pyrightconfig.json
- corriger docstring PathTokenizer et restaurer Literal pour engine
- mapper engine pyarrow vers auto pour pandas et exclure tests/ de mypy
- remove unreachable dead code after build() return in parallel_builder
- add segment_write() and sibling step context managers to CommitProfilerV2
- implement SegmentProfiler to resolve NameError in benchmark.py
- merge parallel worker segments into main index
- merge worker segments into main index in ParallelIndexBuilder
- rendre test_buffered_threads deterministe en couverture des valeurs
- retirer les modules obsolètes des exclusions mypy
- close segment_ix in ParallelIndexBuilder to prevent fd leak
- résoudre les erreurs pyright reportOptionalMemberAccess et reportAssignmentType
- réintégrer assert segment_reader is not None après merge distant
- restore ignore_missing_imports for pytest, peewee, httpx, re2, psutil
- corriger les fuites de handles et erreurs de nettoyage sous Windows


[View on GitHub](https://github.com/dorel14/whoosh-ng/releases/tag/v3.0.0)

## v2.0.0 (2026-07-31)
**Tag**: `v2.0.0`

## v2.0.0 (2026-07-31)

_This release is published under the BSD-2-Clause License._

### Bug Fixes

- Address review findings in CI/CD workflows and documentation ([`b1e0b89`](https://github.com/dorel14/whoosh-ng/commit/b1e0b89f10ad182ad0120d6d19bcac2f8a04470a))

### Documentation

- Auto-update llms context files ([`10e6c90`](https://github.com/dorel14/whoosh-ng/commit/10e6c90fb6e7ffd9ec9f1b3fa7f4e398a933077b))

- Auto-update llms context files ([`992e4d7`](https://github.com/dorel14/whoosh-ng/commit/992e4d780fd7829bd804d68b215e06f255e9cacf))

- Auto-update llms context files ([`5dfe9dc`](https://github.com/dorel14/whoosh-ng/commit/5dfe9dcfdd725d1ca12f3bba0c09a2f7d163aa88))

- Auto-update llms context files ([`aadce99`](https://github.com/dorel14/whoosh-ng/commit/aadce99460c30cd5950e44c927c07eb6fa5ffde8))

### Features

- **deps**: Add sqlalchemy and sqlmodel to models extra and configure mypy overrides ([`4486b5f`](https://github.com/dorel14/whoosh-ng/commit/4486b5feef2700fd8720a0dec1419985f9479951))

- **models**: Ajouter AutoIndexer et améliorer la génération de schémas ([`919131c`](https://github.com/dorel14/whoosh-ng/commit/919131ce7f475b8584e74d98d9cf41e04891b7c7))

- **models**: ✨ Introduce ModelIndex and SearchField for auto-mapping ([`6c202bc`](https://github.com/dorel14/whoosh-ng/commit/6c202bc6c4e20ad9dba7e4cd81deadfdaf3dcf2a))

- **whoosh_modern**: Add modern API with data sources, schema discovery, facets, validation, middleware, and SearchView ([`36bfbae`](https://github.com/dorel14/whoosh-ng/commit/36bfbaebd6acc18cda06aba83bebade7154c9972))

---

**Detailed Changes**: [v1.3.3...v2.0.0](https://github.com/dorel14/whoosh-ng/compare/v1.3.3...v2.0.0)

### Commits

### Documentation

- merge duplicate FastAPI example into fastapi-search.md
- auto-update llms context files
- auto-update llms context files
- auto-update llms context files
- auto-update llms context files

### Other

- Merge branch 'master' of https://github.com/dorel14/whoosh-ng
- ..
- Merge pull request #10 from dorel14/master
- Merge branch 'dev' of https://github.com/dorel14/whoosh-ng into dev
- Merge branch 'dev' of https://github.com/dorel14/whoosh-ng into dev
- Merge branch 'dev' of https://github.com/dorel14/whoosh-ng into dev
- style(benchmark): ajouter un saut de ligne final manquant dans reuters_modern.py
- Merge branch 'dev' of https://github.com/dorel14/whoosh-ng into dev
- Potential fix for pull request finding 'CodeQL / Workflow does not contain permissions'
- Potential fix for pull request finding 'CodeQL / Workflow does not contain permissions'
- Potential fix for pull request finding 'CodeQL / Workflow does not contain permissions'
- .
- .
- .
- Merge pull request #11 from dorel14/dev

### Features

- ✨ Introduce ModelIndex and SearchField for auto-mapping
- ajouter AutoIndexer et améliorer la génération de schémas
- add modern API with data sources, schema discovery, facets, validation, middleware, and SearchView
- add sqlalchemy and sqlmodel to models extra and configure mypy overrides

### Bug Fixes

- address review findings in CI/CD workflows and documentation

### Code Refactoring

- moderniser les annotations de type avec la syntaxe union PEP 604
- moderniser les annotations de type avec Coroutine et ajouter des ignores pyright
- ajouter des annotations de retour aux méthodes replace des matchers
- moderniser la vérification isinstance avec la syntaxe union PEP 604

### CI/CD

- ajouter des extras d'installation et simplifier la couverture

### Chores

- v2.0.0 [skip ci]


[View on GitHub](https://github.com/dorel14/whoosh-ng/releases/tag/v2.0.0)

## v1.3.3 (2026-07-26)
**Tag**: `v1.3.3`

## v1.3.3 (2026-07-26)

_This release is published under the BSD-2-Clause License._

---

**Detailed Changes**: [v1.3.2...v1.3.3](https://github.com/dorel14/whoosh-ng/compare/v1.3.2...v1.3.3)

### Commits

### Bug Fixes

- reorganize nav_order for coherent navigation (Guides 1-90, API 100-190, Examples 200-270)

### Other

- Merge branch 'master' of https://github.com/dorel14/whoosh-ng

### Chores

- v1.3.3 [skip ci]


[View on GitHub](https://github.com/dorel14/whoosh-ng/releases/tag/v1.3.3)

## v1.3.2 (2026-07-26)
**Tag**: `v1.3.2`

## v1.3.2 (2026-07-26)

_This release is published under the BSD-2-Clause License._

---

**Detailed Changes**: [v1.3.1...v1.3.2](https://github.com/dorel14/whoosh-ng/compare/v1.3.1...v1.3.2)

### Commits

### Bug Fixes

- remove color_scheme from individual pages, use global config

### Other

- Merge branch 'master' of https://github.com/dorel14/whoosh-ng

### Chores

- v1.3.2 [skip ci]


[View on GitHub](https://github.com/dorel14/whoosh-ng/releases/tag/v1.3.2)

## v1.3.1 (2026-07-26)
**Tag**: `v1.3.1`

## v1.3.1 (2026-07-26)

_This release is published under the BSD-2-Clause License._

---

**Detailed Changes**: [v1.3.0...v1.3.1](https://github.com/dorel14/whoosh-ng/compare/v1.3.0...v1.3.1)

### Commits

### Bug Fixes

- align _config.yml with taskiq-flow and clean README front matter

### Other

- Merge branch 'master' of https://github.com/dorel14/whoosh-ng

### Chores

- v1.3.1 [skip ci]


[View on GitHub](https://github.com/dorel14/whoosh-ng/releases/tag/v1.3.1)

## v1.3.0 (2026-07-26)
**Tag**: `v1.3.0`

## v1.3.0 (2026-07-26)

_This release is published under the BSD-2-Clause License._

---

**Detailed Changes**: [v1.2.4...v1.3.0](https://github.com/dorel14/whoosh-ng/compare/v1.2.4...v1.3.0)

### Commits

### Features

- éviter les exécutions inutiles du workflow lors des commits de release

### Other

- Merge branch 'master' of https://github.com/dorel14/whoosh-ng

### Chores

- v1.3.0 [skip ci]


[View on GitHub](https://github.com/dorel14/whoosh-ng/releases/tag/v1.3.0)

## v1.2.4 (2026-07-26)
**Tag**: `v1.2.4`

## v1.2.4 (2026-07-26)

_This release is published under the BSD-2-Clause License._

---

**Detailed Changes**: [v1.2.3...v1.2.4](https://github.com/dorel14/whoosh-ng/compare/v1.2.3...v1.2.4)

### Commits

### Bug Fixes

- remove invalid parent fields and align config with taskiq-flow

### Other

- Merge branch 'master' of https://github.com/dorel14/whoosh-ng

### Chores

- v1.2.4


[View on GitHub](https://github.com/dorel14/whoosh-ng/releases/tag/v1.2.4)

## v1.2.3 (2026-07-26)
**Tag**: `v1.2.3`

## v1.2.3 (2026-07-26)

_This release is published under the BSD-2-Clause License._

---

**Detailed Changes**: [v1.2.2...v1.2.3](https://github.com/dorel14/whoosh-ng/compare/v1.2.2...v1.2.3)

### Commits

### Bug Fixes

- remove invalid parent fields from all pages

### Other

- Merge branch 'master' of https://github.com/dorel14/whoosh-ng

### Chores

- v1.2.3


[View on GitHub](https://github.com/dorel14/whoosh-ng/releases/tag/v1.2.3)

## v1.2.2 (2026-07-26)
**Tag**: `v1.2.2`

## v1.2.2 (2026-07-26)

_This release is published under the BSD-2-Clause License._

### Bug Fixes

- **docs**: Align _config.yml with taskiq-flow pattern ([`2124be0`](https://github.com/dorel14/whoosh-ng/commit/2124be0cf1fd4669c9ebdaa4ad485eabae22c279))

---

**Detailed Changes**: [v1.2.1...v1.2.2](https://github.com/dorel14/whoosh-ng/compare/v1.2.1...v1.2.2)

### Commits

### Code Refactoring

- supprimer la navigation statique codée en dur

### Other

- Merge branch 'master' of https://github.com/dorel14/whoosh-ng

### Bug Fixes

- align _config.yml with taskiq-flow pattern

### Chores

- v1.2.2


[View on GitHub](https://github.com/dorel14/whoosh-ng/releases/tag/v1.2.2)

## v1.2.1 (2026-07-26)
**Tag**: `v1.2.1`

## v1.2.1 (2026-07-26)

_This release is published under the BSD-2-Clause License._

### Bug Fixes

- **docs**: Restore front matter and add explicit nav config ([`74dc151`](https://github.com/dorel14/whoosh-ng/commit/74dc151104a7a89ffd1459a60e0e28488bf110ff))

### Documentation

- Fix Jekyll links with relative_url and clean deploy workflow ([`2f9afc2`](https://github.com/dorel14/whoosh-ng/commit/2f9afc20f5e41022117a636b0d223f92c660df3d))

---

**Detailed Changes**: [v1.2.0...v1.2.1](https://github.com/dorel14/whoosh-ng/compare/v1.2.0...v1.2.1)

### Commits

### Documentation

- fix Jekyll links with relative_url and clean deploy workflow

### Bug Fixes

- restore front matter and add explicit nav config

### Chores

- v1.2.1


[View on GitHub](https://github.com/dorel14/whoosh-ng/releases/tag/v1.2.1)

## v1.2.0 (2026-07-26)
**Tag**: `v1.2.0`

## v1.2.0 (2026-07-26)

_This release is published under the BSD-2-Clause License._

### Documentation

- Ajouter la documentation complète bilingue et le déploiement GitHub Pages ([`a091ced`](https://github.com/dorel14/whoosh-ng/commit/a091cede82bb5d722e5db0ff233c1dcbcecd35ea))

- Auto-update llms context files ([`96e4f2c`](https://github.com/dorel14/whoosh-ng/commit/96e4f2c251ccdde5e9fd7e9cb0ce6b2650ab64d9))

- Auto-update llms context files ([`6abefe4`](https://github.com/dorel14/whoosh-ng/commit/6abefe4b84d99e29c1d6ccb471dd7bf27ea4f31d))

### Features

- **docs**: Ajouter le support multilingue dans la configuration ([`967ed12`](https://github.com/dorel14/whoosh-ng/commit/967ed125e5fcd167e237e1c32b7feeb3ab6f9c4f))

---

**Detailed Changes**: [v1.1.0...v1.2.0](https://github.com/dorel14/whoosh-ng/compare/v1.1.0...v1.2.0)

### Commits

### CI/CD

- restructurer le workflow de release sémantique

### Other

- Merge pull request #7 from dorel14/dev
- revert: supprimer la documentation complète bilingue et restaurer l'état précédent
- Merge branch 'dev' of https://github.com/dorel14/whoosh-ng into dev
- Merge pull request #8 from dorel14/dev
- Merge branch 'dev' of https://github.com/dorel14/whoosh-ng into dev
- Merge pull request #9 from dorel14/dev

### Documentation

- ajouter la documentation complète bilingue et le déploiement GitHub Pages
- auto-update llms context files
- auto-update llms context files

### Features

- ajouter le support multilingue dans la configuration

### Chores

- v1.2.0


[View on GitHub](https://github.com/dorel14/whoosh-ng/releases/tag/v1.2.0)

## v1.1.0 (2026-07-26)
**Tag**: `v1.1.0`

## v1.1.0 (2026-07-26)

_This release is published under the BSD-2-Clause License._

### Bug Fixes

- **bench**: 🐛 add type ignores for method overrides in `XappyModule` ([`822e957`](https://github.com/dorel14/whoosh-ng/commit/822e957c986f603e9fd1c2de2799fb60d3c122a9))

- **schema**: 🐛 corriger l'initialisation de l'objet dans `__new__` ([`5a764a7`](https://github.com/dorel14/whoosh-ng/commit/5a764a78751a6a2bb94118340784da074fdcf2c8))

- **stress**: 🐛 ensure correct handling of string encoding in `test_bigtable` ([`822e957`](https://github.com/dorel14/whoosh-ng/commit/822e957c986f603e9fd1c2de2799fb60d3c122a9))

### Build System

- **deps**: Supprimer les dépendances obsolètes de la section models ([`227a59f`](https://github.com/dorel14/whoosh-ng/commit/227a59fc195799aca02289c2cb5f3d60135ea063))

### Features

- **benchmark**: Refonte du système de benchmarks avec nouvelles spécifications ([`417efd1`](https://github.com/dorel14/whoosh-ng/commit/417efd1be034fe8be4377ad2716e7b15028ad929))

- **matching**: ✨ add type hints for `supports_block_quality` methods ([`822e957`](https://github.com/dorel14/whoosh-ng/commit/822e957c986f603e9fd1c2de2799fb60d3c122a9))

- **support**: ✨ add compatibility for Python 3 unicode handling ([`822e957`](https://github.com/dorel14/whoosh-ng/commit/822e957c986f603e9fd1c2de2799fb60d3c122a9))

- **writing**: ✨ Implement segment writing and merging policies ([`ae5fc62`](https://github.com/dorel14/whoosh-ng/commit/ae5fc62d2185ac349b56cbbf006bbb7fd4f92c80))

---

**Detailed Changes**: [v1.0.0...v1.1.0](https://github.com/dorel14/whoosh-ng/compare/v1.0.0...v1.1.0)

### Commits

### Chores

- ✏️ Mise à jour de la version dans le README
- prepare whoosh-ng 1.0.0
- bump version to 1.0.1
- 🔄 Update project dependencies
- apply pre-commit fixes
- apply pre-commit fixes
- v1.1.0

### Other

- Potential fix for code scanning alert no. 1: Workflow does not contain permissions
- Potential fix for code scanning alert no. 1: Workflow does not contain permissions
- Merge branch 'dev' of https://github.com/dorel14/whoosh-ng into dev
- Merge pull request #5 from dorel14/alert-autofix-1
- Merge branch 'master' into dev
- Potential fix for pull request finding 'CodeQL / Workflow does not contain permissions'
- Merge branch 'dev' of https://github.com/dorel14/whoosh-ng into dev
- Merge 3d0187143ec5a2a4e19f62d26a622d2c53efc2c7 into 68e67aaefb0d48f136d53576e00f10d68f77f15a
- Merge branch 'dev' of https://github.com/dorel14/whoosh-ng into dev
- Merge 4473223d510f0cde2d30ca638d284f23dc467b14 into 68e67aaefb0d48f136d53576e00f10d68f77f15a
- Merge branch 'dev' of https://github.com/dorel14/whoosh-ng into dev
- Merge pull request #6 from dorel14/dev

### Features

- ✨ Ajout de la validation de l'intégrité des segments
- ✨ Ajout d'un wrapper Asyncio pour la gestion asynchrone des écritures
- ✨ Isolation des espaces de stockage temporaires pour les écrivains concurrents
- ✨ Ajout de la normalisation des boosts pour les sous-requêtes
- ✨ Ajout d'un module de reporting pour les résultats de benchmark
- ✨ Ajout d'un backend LMDB et d'un support d'autocomplétion
- ✨ Implement segment writing and merging policies
- refonte du système de benchmarks avec nouvelles spécifications
- ✨ add type hints for `supports_block_quality` methods

### Bug Fixes

- 🐛 Ajout d'un type d'ignore pour l'appel de la requête
- 🐛 Amélioration des benchmarks avec un échauffement et ajustement des seuils d'alerte
- 🐛 corriger l'initialisation de l'objet dans `__new__`

### Code Refactoring

- improve _posting_size estimation, fix benchmark CLI, update mypy target to 3.12

### Build System

- supprimer les dépendances obsolètes de la section models


[View on GitHub](https://github.com/dorel14/whoosh-ng/releases/tag/v1.1.0)

## v1.0.0 (2026-07-12)
**Tag**: `v1.0.0`

## v1.0.0 (2026-07-12)

_This release is published under the BSD-2-Clause License._

- Initial Release


[View on GitHub](https://github.com/dorel14/whoosh-ng/releases/tag/v1.0.0)


## DOCUMENT: Core Concepts

# Core Concepts

Whoosh-NG is a pure-Python search engine library. This guide explains the main concepts you need to understand to use it effectively.

## Architecture

Whoosh-NG follows a layered architecture:

![Architecture diagram: Application → Whoosh-NG Core (Search Engine, Schema, Plugin Manager, Registry, Middleware, Event Bus, Hooks) → Plugins (FastAPI, Vector Search, Autocomplete, PostgreSQL, S3, Monitoring, Admin UI)](/assets/architecture.svg)

## Key Components

### Index

An `Index` is the top-level container for your searchable documents. It manages one or more segments on disk.

```python
from whoosh.index import create_in, open_dir

# Create a new index
ix = create_in("indexdir", schema)

# Open an existing index
ix = open_dir("indexdir")
```

### Schema

The `Schema` defines the fields that documents in your index can have. Each field has a type that determines how it is indexed and stored.

```python
from whoosh.fields import Schema, TEXT, ID, NUMERIC

schema = Schema(
    title=TEXT(stored=True),
    path=ID(stored=True, unique=True),
    content=TEXT,
    rating=NUMERIC(float, stored=True)
)
```

### Writer

An `IndexWriter` lets you add, update, and delete documents in the index.

```python
writer = ix.writer()
writer.add_document(title="Hello", content="World")
writer.commit()
```

### Searcher

A `Searcher` lets you query the index and retrieve results.

```python
with ix.searcher() as s:
    results = s.search("hello")
```

### Query Parser

The `QueryParser` converts a query string into a query object that the searcher can execute.

```python
from whoosh.qparser import QueryParser

qp = QueryParser("content", schema)
query = qp.parse("hello world")
```

## Modern Features

### Plugin System

Plugins extend Whoosh-NG without modifying the core. Plugins can:

- Register new vector providers
- Add FastAPI endpoints
- Provide custom analyzers
- Hook into the middleware pipeline

```python
from whoosh.plugins.manager import PluginManager

# Load plugins from entry points
PluginManager.load_plugins()

# Or register manually
PluginManager.register("my_plugin", MyPlugin())
```

### Middleware Pipeline

Middleware intercepts indexing and search operations:

```python
from whoosh.middleware import Middleware, MiddlewareContext

class LoggingMiddleware(Middleware):
    def before_search(self, context: MiddlewareContext):
        print(f"Searching: {context.query}")
        return context

    def after_search(self, context: MiddlewareContext):
        print(f"Found: {len(context.results) if context.results else 0} results")
        return context
```

### Vector Search

Vector fields enable semantic search using embeddings:

```python
from whoosh.fields import Schema, TEXT, VectorField

schema = Schema(
    content=TEXT,
    embedding=VectorField(dimensions=384)
)
```

### Event Bus

The event system allows loose coupling between components:

```python
from whoosh.event_bus import EventBus, DocumentIndexed

bus = EventBus()

@bus.subscribe
def on_document_indexed(event: DocumentIndexed):
    print(f"Document indexed: {event.docnum}")
```

## Data Flow

### Indexing Flow

1. Application calls `writer.add_document()`
2. Schema validates and analyzes fields
3. Middleware `before_index` hooks run
4. Document is written to segment
5. Middleware `after_index` hooks run
6. `DocumentIndexed` event is published
7. `commit()` merges segments and writes TOC

### Search Flow

1. Application calls `searcher.search(query)`
2. Query is parsed into query tree
3. Middleware `before_search` hooks run
4. Searcher executes query against segments
5. Results are scored and sorted
6. Middleware `after_search` hooks run
7. `SearchExecuted` event is published
8. Results are returned to application

## Design Principles

1. **Composability**: Components combine via `|` and `+` operators
2. **Zero-cost abstractions**: No middleware = no overhead
3. **Sync-first**: Core is synchronous; async is opt-in
4. **Plugin isolation**: Plugins cannot break the core
5. **Type safety**: Comprehensive type hints throughout


## DOCUMENT: Dates

# Dates and Numeric Ranges

This guide covers working with `DATETIME` and `NUMERIC` fields, including
range queries, range faceting, and date math.

## DATETIME Fields

`DATETIME` fields store Python `datetime` objects and can be queried with
range queries.

```python
from datetime import datetime
from whoosh import fields, index

schema = fields.Schema(
    title=fields.TEXT(stored=True),
    published_date=fields.DATETIME(stored=True, sortable=True),
)
```

### Indexing Dates

```python
ix = index.create_in("indexdir", schema)
with ix.writer() as w:
    w.add_document(
        title="Article 1",
        published_date=datetime(2024, 6, 15, 14, 30),
    )
```

### Date Range Queries

Use `Range` or `QueryParser` syntax:

```python
from whoosh.qparser import QueryParser
from whoosh.query import Range, Every

# Using QueryParser syntax
qp = QueryParser("published_date", schema=ix.schema)
q = qp.parse("[2024-01-01 TO 2024-12-31]")

# Using Range query directly
from datetime import datetime
q = Range(
    "published_date",
    datetime(2024, 1, 1),
    datetime(2024, 12, 31),
)

with ix.searcher() as searcher:
    results = searcher.search(q)
```

### Sorting by Date

```python
from whoosh.sorting import FieldFacet

# Sort by date, most recent first
results = searcher.search(
    query,
    sortedby=FieldFacet("published_date", reverse=True),
)
```

## NUMERIC Fields

`NUMERIC` fields store integers and floating-point numbers.

```python
schema = fields.Schema(
    title=fields.TEXT(stored=True),
    price=fields.NUMERIC(int, stored=True, sortable=True),
    rating=fields.NUMERIC(float, stored=True),
)
```

### Numeric Range Queries

```python
from whoosh.query import NumericRange

q = NumericRange("price", 100, 500)

# Or with QueryParser
qp = QueryParser("price", schema=ix.schema)
q = qp.parse("[100 TO 500]")
```

### Numeric Faceting

Group results into numeric ranges using `RangeFacet`:

```python
from whoosh.sorting import RangeFacet

price_ranges = RangeFacet("price", 0, 1000, 100)
results = searcher.search(query, groupedby=price_ranges)

for groupname, docnums in results.groups("price").items():
    print(f"Price ${groupname}: {len(docnums)} results")
```

## Date Faceting

Group results by date intervals using `DateRangeFacet`:

```python
from datetime import datetime
from whoosh.sorting import DateRangeFacet

start = datetime(2020, 1, 1)
end = datetime(2026, 1, 1)
date_facet = DateRangeFacet(
    "published_date",
    start,
    end,
    relativedelta(years=1),  # Requires: from dateutil.relativedelta import relativedelta
)
results = searcher.search(query, groupedby=date_facet)

for year_range, docnums in results.groups("published_date").items():
    print(f"Year {year_range}: {len(docnums)} results")
```

## Sorting and Filtering by Numbers

### Sorting

```python
from whoosh.sorting import FieldFacet

# Sort by price ascending
results = searcher.search(query, sortedby=FieldFacet("price"))
```

### Filtering

```python
from whoosh.query import NumericRange

# Only results with price >= 50 and price < 200
filter_q = NumericRange("price", 50, 200)
results = searcher.search(query, filter=filter_q)
```

## Making Date/Numeric Fields Sortable

When defining a schema, set `sortable=True` on `NUMERIC` or `DATETIME` fields
to enable sorting by that field:

```python
schema = fields.Schema(
    title=fields.TEXT(stored=True),
    price=fields.NUMERIC(int, sortable=True),
    date=fields.DATETIME(sortable=True),
)
```

If you forgot to set `sortable=True`, you can add it after indexing:

```python
from whoosh import index, sorting

ix = index.open_dir("indexdir")
with ix.writer() as w:
    sorting.add_sortable(w, "price", sorting.FieldFacet("price"))
```


## DOCUMENT: Fieldcaches

# Field caches

The default (`filedb`) backend uses *field caches* in certain circumstances.
The field cache basically pre-computes the order of documents in the index to
speed up sorting and faceting.

Generating field caches can take time the first time you sort/facet on a large
index. The field cache is kept in memory (and by default written to disk when
it is generated) so subsequent sorted/faceted searches should be faster.

The default caching policy never expires field caches, so reused searchers
and/or sorting a lot of different fields could use up quite a bit of memory
with large indexes.

## Customizing cache behaviour

(By default, Whoosh saves field caches to disk. To prevent a reader or
searcher from writing out field caches, do this before you start using it:)

```python
searcher.set_caching_policy(save=False)
```

By default, if caches are written to disk they are saved in the index
directory. To tell a reader or searcher to save cache files to a different
location, create a storage object and pass it to the `storage` keyword
argument:

```python
from whoosh.filedb.filestore import FileStorage

mystorage = FileStorage("path/to/cachedir")
reader.set_caching_policy(storage=mystorage)
```

## Creating a custom caching policy

Expert users who want to implement a custom caching policy (for example, to add
cache expiration) should subclass `whoosh.filedb.fieldcache.FieldCachingPolicy`.
Then you can pass an instance of your policy object to the `set_caching_policy`
method:

```python
searcher.set_caching_policy(MyPolicy())
```

## See also

- [Sorting](/core/sorting) — Facets and sort keys
- [API: sorting](../api/sorting) — Sorting and faceting reference


## DOCUMENT: Glossary

# Glossary

A glossary of key terms used in Whoosh.

## Analysis

The process of converting text into tokens (individual units like words or
terms) for indexing. Involves tokenization, normalization (lowercasing,
stemming), and filtering (stop word removal, etc.).

## Analyzer

A chain of `Tokenizer` and `Filter` objects that processes text into
tokens. Examples include `RegexTokenizer`, `NgramTokenizer`, `LowercaseFilter`,
`StopFilter`, and `StemmerFilter`.

## Compound File

A file format that combines multiple index segment files into a single
`.seg` file. This can improve performance on some filesystems by reducing
file handle usage. Configured via the codec's `should_assemble` setting.

## Document

A single record in the index, similar to a row in a database. A document
contains fields (analogous to columns).

## Field

A named attribute of a document. Fields have a type (defined by `FieldType`)
that determines how the field's value is indexed and stored.

## Field Type

The class (e.g., `TEXT`, `ID`, `NUMERIC`, `DATETIME`, `BOOLEAN`) that
defines how a field's value is tokenized, stored, indexed, and made
sortable/facetable.

## Filter

An `Analyzer` component that processes, transforms, or filters tokens
after tokenization. Examples: `LowercaseFilter`, `StopFilter`,
`StemmerFilter`.

## Format

A `Format` object controls how posting information (term frequency, positions,
character offsets) is encoded for each field in the inverted index.
Examples: `Existence`, `Frequency`, `Positions`, `Characters`.

## Fragmentation

The process of selecting text spans around matched terms for highlighting.

## Highlighter

The `whoosh.highlight` module, which provides formatters, fragmenters, and
scorers for highlighting search terms in documents.

## Index

The collection of segment files that store the inverted index, document
data, and metadata (the table of contents, or TOC).

## IndexWriter

The `IndexWriter` class is used to create and modify the index. It buffers
document additions and deletions and commits them to disk.

## Inverted Index

The core data structure of a search engine: for each unique term, it stores a
list of documents (and positions) where that term appears.

## Matcher

An object that iterates over matching documents in the postings list for a
query. Matchers can be combined (union, intersection, etc.) for compound
queries.

## Posting

A single entry in the inverted index: a (document ID, term frequency, value)
tuple for a given term.

## Schema

Defines the fields, their types, and indexing options for an index. A schema
is passed to `Storage.create_index()`.

## Scorer

An object that computes a relevance score for a document given a query and
term weights. Different weighting models (BM25, TF-IDF, etc.) use different
scorers.

## Segment

A self-contained portion of the inverted index. An index may consist of
multiple segments. Segments are merged periodically (during optimize or
merge operations) to improve performance.

## Sort Key

A value computed per-document (via a `FacetType` and its `Categorizer`)
used to order results during sorting and faceting.

## Stemming

The process of reducing words to their root form (e.g., "running" → "run",
"cats" → "cat") to improve recall by matching inflected forms.

## Stop Words

High-frequency, low-information words (e.g., "the", "a", "and") that are
typically filtered out during indexing.

## Term

A unique (field name, token text) pair in the inverted index.

## Term Vector

Optional per-document data structure storing the terms (and optionally
positions and character offsets) that appear in a document's field, enabling
features like highlighting and pseudo-relevance feedback.

## Tokenizer

An `Analyzer` component that splits input text into tokens. Examples:
`RegexTokenizer`, `PathTokenizer`, `NgramTokenizer`.

## Whoosh Query

Whoosh's own query syntax, parsed by `QueryParser`. Supports fielded
search, phrase queries, wildcards, ranges, and more.


## DOCUMENT: Highlight

# Highlighting search result excerpts

## Overview

The highlighting system works as a pipeline, with four component types.

- **Fragmenters** chop up the original text into *fragments*, based on the
  locations of matched terms in the text.
- **Scorers** assign a score to each fragment, allowing the system to rank the
  best fragments by whatever criterion.
- **Order functions** control in what order the top-scoring fragments are
  presented to the user. For example, you can show the fragments in the order
  they appear in the document (`FIRST`) or show higher-scoring fragments first
  (`SCORE`).
- **Formatters** turn the fragment objects into human-readable output, such as
  an HTML string.

## Requirements

Highlighting requires that you have the text of the indexed document available.
You can keep the text in a stored field, or if the original text is available in
a file, database column, etc, just reload it on the fly. Note that you might
need to process the text to remove e.g. HTML tags, wiki markup, etc.

## How to

Get search results and use the `highlights()` method on the
`whoosh.searching.Hit` object to get highlighted snippets:

```python
results = mysearcher.search(myquery)
for hit in results:
    print(hit["title"])
    # Assume "content" field is stored
    print(hit.highlights("content"))
```

If the field is not stored, you need to retrieve the text of the field some
other way, then supply it with the `text` argument:

```python
results = mysearcher.search(myquery)
for hit in results:
    print(hit["title"])
    # Assume the "path" stored field contains a path to the original file
    with open(hit["path"]) as fileobj:
        filecontents = fileobj.read()
    print(hit.highlights("content", text=filecontents))
```

## The character limit

By default, Whoosh only pulls fragments from the first 32K characters of the
text. This prevents very long texts from bogging down the highlighting process
too much. You can change the character limit on the results object:

```python
results = mysearcher.search(myquery)
results.fragmenter.charlimit = 100000
```

To turn off the character limit:

```python
results.fragmenter.charlimit = None
```

If you instantiate a custom fragmenter, you can set the character limit directly:

```python
sf = highlight.SentenceFragmenter(charlimit=100000)
results.fragmenter = sf
```

## Customizing the highlights

### Number of fragments

Use the `top` keyword argument to control the number of fragments returned:

```python
# Show a maximum of 5 fragments from the document
print(hit.highlights("content", top=5))
```

### Fragment size

The default fragmenter has a `maxchars` attribute (default 200) controlling the
maximum length of a fragment, and a `surround` attribute (default 20)
controlling the maximum number of characters of context to add at the beginning
and end of a fragment:

```python
# Allow larger fragments
results.fragmenter.maxchars = 300
# Show more context before and after
results.fragmenter.surround = 50
```

### Fragmenter

A fragmenter controls how to extract excerpts from the original text. The
`highlight` module has the following pre-made fragmenters:

- `whoosh.highlight.ContextFragmenter` (the default) — a "smart" fragmenter
  that finds matched terms and pulls in surround text. Only yields fragments
  that contain matched terms.
- `whoosh.highlight.SentenceFragmenter` — tries to break the text into
  fragments based on sentence punctuation.
- `whoosh.highlight.WholeFragmenter` — returns the entire text as one
  "fragment". Useful for short bits of text.

```python
my_cf = highlight.ContextFragmenter(maxchars=100, surround=30)
results.fragmenter = my_cf
```

### Scorer

A scorer is a callable that takes a `whoosh.highlight.Fragment` object and
returns a sortable value (where higher values represent better fragments). The
default scorer adds up the number of matched terms in the fragment, and adds a
"bonus" for the number of *different* matched terms.

```python
def StandardDeviationScorer(fragment):
    """Gives higher scores to fragments where the matched terms are close together."""
    return 0 - stddev([t.pos for t in fragment.matched])

results.scorer = StandardDeviationScorer
```

### Order

The order is a function that takes a fragment and returns a sortable value used
to sort the highest-scoring fragments before presenting them to the user.

- `FIRST` (the default) — show fragments in document order.
- `SCORE` — show highest scoring fragments first.
- `LONGER` / `SHORTER` — longer/shorter fragments first (less generally useful).

```python
results.order = highlight.SCORE
```

### Formatter

A formatter controls how the highest scoring fragments are turned into a
formatted bit of text. The `highlight` module contains:

- `whoosh.highlight.HtmlFormatter` — outputs HTML with a class attribute around
  matched terms.
- `whoosh.highlight.UppercaseFormatter` — converts matched terms to UPPERCASE.

The easiest way to create a custom formatter is to subclass `highlight.Formatter`
and override `format_token`:

```python
class BracketFormatter(highlight.Formatter):
    """Puts square brackets around the matched terms."""

    def format_token(self, text, token, replace=False):
        tokentext = highlight.get_text(text, token, replace)
        return "[%s]" % tokentext

brf = BracketFormatter()
results.formatter = brf
```

## Highlighter object

Rather than setting attributes on the results object, you can create a reusable
`whoosh.highlight.Highlighter` object:

```python
hi = highlight.Highlighter(fragmenter=my_cf, scorer=sds)
for hit in results:
    print(hit["title"])
    print(hi.highlight_hit(hit))
```

## Speeding up highlighting

Recording which terms matched in which documents during the search may make
highlighting faster:

```python
# Record per-document term matches
results = searcher.search(myquery, terms=True)
```

### PinpointFragmenter

Instead of re-tokenizing the document text, Whoosh can look up the character
positions of the matched terms in the index. To use
`whoosh.highlight.PinpointFragmenter` and avoid re-tokenizing:

1. Index the field with character information (requires re-indexing):

   ```python
   schema = fields.Schema(content=fields.TEXT(stored=True, chars=True))
   ```

2. Record per-document term matches:

   ```python
   results = searcher.search(myquery, terms=True)
   ```

3. Set the `PinpointFragmenter` as the fragmenter:

   ```python
   results.fragmenter = highlight.PinpointFragmenter()
   ```

Use the `autotrim` option to strip whitespace before the first space and after
the last space in the fragments:

```python
results.fragmenter = highlight.PinpointFragmenter(autotrim=True)
```

## Using the low-level API

```python
from whoosh.highlight import highlight

excerpts = highlight(
    text, terms, analyzer, fragmenter, formatter, top=3,
    scorer=BasicFragmentScorer, minscore=1, order=FIRST,
)
```

| Argument | Description |
|----------|-------------|
| `text` | The original text of the document. |
| `terms` | A sequence or set containing the query words to match. |
| `analyzer` | The analyzer to use to break the document text into tokens. |
| `fragmenter` | A `Fragmenter` object. |
| `formatter` | A `Formatter` object. |
| `top` | The number of fragments to include in the output. |
| `scorer` | A `FragmentScorer` object. |
| `minscore` | The minimum score a fragment must have to be included. |
| `order` | An ordering function for the "top" fragments. |

## See also

- [Searching](/core/searching) — The `search()` method and `Hit` objects
- [API: highlight](../api/highlight) — Full `whoosh.highlight` reference


## DOCUMENT: Indexing

:::info
Following the rename of `whoosh-reloaded` to `whoosh-ng`, new Whoosh-NG specific modules are typically found under `whoosh_modern`.
Core Whoosh components (like `whoosh.analysis`, `whoosh.index`) remain accessible directly under the `whoosh` namespace for backward compatibility.
:::

# Indexing

This guide covers adding, updating, and deleting documents in your Whoosh-NG index.

## Opening a Writer

```python
from whoosh import index

ix = index.open_dir("indexdir")

# Basic writer
writer = ix.writer()

# Writer with custom options
writer = ix.writer(
    timeout=10.0,      # Lock acquisition timeout (seconds)
    delay=0.1,         # Delay between lock retries (seconds)
    limitmb=128,       # Posting pool run size (MiB)
    compound=True      # Use compound files
)
```

## Adding Documents

```python
with ix.writer() as writer:
    writer.add_document(
        title="First document",
        content="Hello world",
        path="/doc1",
        tags=["python", "search"]
    )
    writer.add_document(
        title="Second document",
        content="Goodbye world",
        path="/doc2",
        tags=["python", "tutorial"]
    )
    # commit() is called automatically on exit
```

### Multi-value Fields

Pass lists to add multiple values for multi-valued fields:

```python
writer.add_document(
    title="Document with multiple tags",
    content="Content here",
    tags=["python", "whoosh", "search", "tutorial"]
)
```

### Stored vs Indexed Values

For fields that are both indexed and stored, you can store a different value:

```python
writer.add_document(
    title="Title to be indexed",
    _stored_title="Display title to show in results"
)
```

> **Note**: The underscore prefix (`_stored_<field>`, `_<field>_boost`) is a
> Whoosh convention for per-document overrides. It lets you store a different
> value for display (`_stored_title`) without changing what is indexed, or
> boost a specific field for a single document (`_title_boost`) without
> affecting the schema-level boost.

### Field Boosts

Boost individual fields at document level:

```python
writer.add_document(
    title="Important title",
    _title_boost=2.0,   # Double weight for title terms
    content="Body content"
)
```

## Updating Documents

Use `update_document` to replace documents with matching unique fields:

```python
schema = Schema(path=ID(unique=True, stored=True), content=TEXT)
ix = index.create_in("indexdir", schema)

with ix.writer() as writer:
    writer.add_document(path="/doc1", content="Original content")
    writer.commit()

with ix.writer() as writer:
    # Replaces any document with path="/doc1"
    writer.update_document(path="/doc1", content="Updated content")
    writer.commit()
```

## Deleting Documents

```python
# Delete by document number
writer.delete_document(docnum=42)

# Delete by term in a field
writer.delete_by_term("path", "/doc1")

# Delete by query
from whoosh.query import Term
q = Term("tags", "deprecated")
writer.delete_by_query(q)

writer.commit()
```

## Commit and Merge Policies

### Basic Commit

```python
writer.commit()
```

### Optimize (Merge All)

```python
writer.commit(optimize=True)
```

### No Merge

```python
writer.commit(merge=False)
```

### Custom Merge Policy

```python
from whoosh.writing import NO_MERGE, MERGE_SMALL, OPTIMIZE

writer.commit(mergetype=NO_MERGE)
writer.commit(mergetype=MERGE_SMALL)
writer.commit(mergetype=OPTIMIZE)

# Custom function
def my_merge(writer, segments):
    # Custom merge logic
    return segments

writer.commit(mergetype=my_merge)
```

## BufferedWriter

For high-throughput scenarios where documents arrive one at a time:

```python
from whoosh.writing import BufferedWriter

# Buffers documents and commits periodically
buffered = BufferedWriter(
    ix,
    period=60,    # Max seconds between commits
    limit=100,    # Max documents per commit
    writerargs={} # Extra args for underlying writer
)

with buffered:
    buffered.add_document(title="Doc 1", content="Content")
    buffered.add_document(title="Doc 2", content="More")
# commit() called automatically on close
```

## AsyncWriter

For web applications where multiple processes may write:

```python
from whoosh.writing import AsyncWriter

# Automatically retries on lock contention
async_writer = AsyncWriter(ix, delay=0.25)

async_writer.add_document(title="Async doc", content="Content")
async_writer.commit()
```

## Middleware Integration

```python
from whoosh.middleware import MiddlewareChain, MetricsMiddleware, CacheMiddleware
from whoosh.middleware.integration import apply_middleware_to_writer

chain = MiddlewareChain([
    MetricsMiddleware(),
    CacheMiddleware()
])

with apply_middleware_to_writer(ix.writer(), chain.middlewares) as writer:
    writer.add_document(title="Tracked", content="Content")
```

## Best Practices

1. **Use context managers**: `with ix.writer() as w:` ensures proper cleanup
2. **Batch commits**: Group many documents per commit for better performance
3. **Choose merge policy wisely**: `MERGE_SMALL` is usually fine; use `NO_MERGE` for bulk loads followed by `OPTIMIZE`
4. **Handle locks**: Use `BufferedWriter` or `AsyncWriter` in multi-process environments
5. **Don't forget to close**: Always call `commit()` or `cancel()` to release the write lock


## DOCUMENT: Installation

# Installation

## Requirements

- Python 3.10+
- No mandatory dependencies (pure Python)
- Optional extras for advanced features

## pip install

```bash
pip install whoosh-ng
```

## Extras

| Extra | Description |
|-------|-------------|
| `vector` | NumPy-based vector providers |
| `autocomplete` | Autocomplete plugin |
| `api` | FastAPI plugin |
| `metrics` | Prometheus metrics integration |
| `all` | Install everything |

```bash
pip install whoosh-ng[all]
```

## Development install

```bash
git clone https://github.com/your-org/whoosh-NG.git
cd whoosh-NG
uv sync --extra dev
```

## Verification

```bash
uv run pytest tests/ -q
uv run ruff check src/ tests/
uv run ruff format --check .
uv run mypy src/whoosh
```

## Next Steps

- [Quick Start](/core/quickstart)
- [Core Concepts](/core/core-concepts)


## DOCUMENT: Intro

# Introduction to Whoosh

## About Whoosh

Whoosh was created by Matt Chaput. It started as a quick and dirty search
server for the online documentation of the Houdini 3D animation software
package. Side Effects Software generously allowed Matt to open source the code
in case it might be useful to anyone else who needs a very flexible or
pure-Python search engine (or both!).

- Whoosh is fast, but uses only pure Python, so it will run anywhere Python
  runs, without requiring a compiler.
- By default, Whoosh uses the [Okapi BM25F](https://en.wikipedia.org/wiki/Okapi_BM25)
  ranking function, but like most things the ranking function can be easily
  customized.
- Whoosh creates fairly small indexes compared to many other search libraries.
- All indexed text in Whoosh must be **unicode**.
- Whoosh lets you store arbitrary Python objects with indexed documents.

## What is Whoosh?

Whoosh is a fast, pure Python search engine library.

The primary design impetus of Whoosh is that it is pure Python. You should be
able to use Whoosh anywhere you can use Python, no compiler or Java required.

Like one of its ancestors, Lucene, Whoosh is not really a search engine, it's a
programmer library for creating a search engine.

Practically no important behavior of Whoosh is hard-coded. Indexing of text, the
level of information stored for each term in each field, parsing of search
queries, the types of queries allowed, scoring algorithms, etc. are all
customizable, replaceable, and extensible.

## What can Whoosh do for you?

Whoosh lets you index free-form or structured text and then quickly find
matching documents based on simple or complex search criteria.

## Whoosh-NG

Whoosh-NG is the maintained evolution of Whoosh. It preserves the pure-Python
core described above while adding optional, opt-in extensions (vector search,
a plugin system, a middleware pipeline, linguistics, and pluggable storage).
Classic features documented in this section remain backwards-compatible with
Whoosh 1.x/2.x.

## Getting help with Whoosh

You can view outstanding issues on the
[Whoosh-NG GitHub page](https://github.com/dorel14/whoosh-ng) and get help by
opening an issue or discussion there.


## DOCUMENT: Keywords

# Query expansion and keyword extraction

## Overview

Whoosh provides methods for computing the "key terms" of a set of documents.
For these methods, "key terms" basically means terms that are frequent in the
given documents, but relatively infrequent in the indexed collection as a whole.

Because this is a purely statistical operation, not a natural language
processing or AI function, the quality of the results will vary based on the
content, the size of the document collection, and the number of documents for
which you extract keywords.

These methods can be useful for providing the following features to users:

- **Search term expansion.** Extract key terms for the top N results from a
  query and suggest them to the user as additional/alternate query terms.
- **Tag suggestion.** Extracting the key terms for a single document may yield
  useful suggestions for tagging the document.
- **"More like this".** Extract key terms for the top ten or so results from a
  query (and removing the original query terms), and use those key words as the
  basis for another query that may find more documents using terms the user
  didn't think of.

## Usage

### More like this

Get more documents like a certain search hit. *This requires that the field you
want to match on is vectored or stored, or that you have access to the original
text.*

```python
results = mysearcher.search(myquery)
first_hit = results[0]
more_results = first_hit.more_like_this("content")
```

### Key terms from top N results

*This requires that the field is either vectored or stored.*

```python
# Extract five key terms from the "content" field of the top ten documents
keywords = [keyword for keyword, score
            in results.key_terms("content", docs=10, numterms=5)]
```

### Key terms from an arbitrary set of documents

*This requires that the field is either vectored or stored.*

```python
with email_index.searcher() as s:
    docnums = s.document_numbers(emailto="matt@whoosh.ca")
    keywords = [keyword for keyword, score
                in s.key_terms(docnums, "body")]
```

### Key terms from arbitrary text not in the index

```python
with email_index.searcher() as s:
    keywords = [keyword for keyword, score
                in s.key_terms_from_text("body", mytext)]
```

## Expansion models

The `ExpansionModel` subclasses in the `whoosh.classify` module implement
different weighting functions for key words. These models are translated into
Python from original Java implementations in Terrier.

```python
from whoosh.classify import Bo1Model

results = mysearcher.search(myquery)
keywords = results.key_terms("content", docs=10, numterms=5, model=Bo1Model)
```

Available models include `Bo1Model`, `Bo2Model`, and `KLModel`.

## See also

- [Searching](/core/searching) — `Results`, `Hit`, and the `search()` method
- [API: searching](../api/searching) — `key_terms`, `more_like_this` reference


## DOCUMENT: Legacy Cleanup

# Legacy Code Cleanup Strategy

This guide explains how Whoosh-NG separates modern typed code from legacy code,
and how the legacy cleanup is progressing.

## Why a legacy boundary?

`whoosh-modern` is the new, fully typed surface of Whoosh-NG.
The original `whoosh` package still works at runtime, but it carries decades of
Python 2/3 compatibility patterns, dynamic metaprogramming, and untyped internals.
Trying to force strict types on all of it at once would block development.

The cleanup strategy is **incremental and opt-in**:

1. `src/whoosh_modern/` is typed and linted with `pyright` and `mypy` strict.
2. `src/whoosh/` is the legacy surface. It is split into:
   - **excluded modules** (documented in `pyrightconfig.json`) — code that is too
     dynamic or vendored for an economical type pass right now;
   - **cleanup candidates** — small, isolated files that are straightforward to
     annotate and verify.
3. Each sprint, a wave of candidates is typed, tested, and promoted out of the
   high-tolerance zone.

## Current pyright/mypy thresholds (Sprint 2)

| Checker | Scope | Threshold |
|---------|-------|-----------|
| `pyright` | `src/whoosh_modern/` | **0 errors** (strict) |
| `pyright` | legacy | **≤ 500 errors** (tolerant) |
| `mypy` | `src/` | **0 errors** (via overrides + `ignore_errors`) |

## Exclusion rationale (pyrightconfig.json)

The `exclude` list in `pyrightconfig.json` groups excluded files by theme:

- **Vendored / no stubs**: `pyparsing.py`, `relativedelta.py`
- **Migration shims**: `codec/whoosh2.py`, `codec/whoosh3.py`
- **Dynamic parsing**: `qparser/`, `query/`, `analysis/`, `automata/`
- **Large datastores**: `filedb/`, `reading/`, `writing/`
- **Heuristic / data-driven**: `lang/dmetaphone.py`, `lang/lovins.py`,
  `lang/phonetic.py`, `lang/wordnet.py`
- **Core dynamic objects**: `classify.py`, `index.py`, `locking.py`,
  `formats.py`, `middleware/`
- **Vendored low-level**: `support/bench.py`, `support/base85.py`,
  `support/bitstream.py`, `support/bitvector.py`, `support/charset.py`,
  `support/levenshtein.py`

## Sprint 2 cleanup plan

For Sprint 2, the focus is on small utility and support modules that have few
external dependencies and no heavy metaprogramming.

Candidate wave:

- `src/whoosh/util/varints.py`
- `src/whoosh/util/text.py`
- `src/whoosh/util/loading.py`
- `src/whoosh/support/bitstream.py`
- `src/whoosh/support/levenshtein.py`

For each file:

1. Remove the blanket `# type: ignore` (if present).
2. Add precise function signatures.
3. Run `pyright` and `mypy` to confirm **0 new errors**.
4. Move the file out of `pyrightconfig.json` excludes.
5. Add a regression test in `tests/test_legacy_cleanup.py`.

## Long-term goal

Eventually every file in `src/whoosh/` should be checkable by `mypy` and
`pyright` without blanket excludes. Until then, the exclude list is the
explicit ledger of debt, and each sprint chips away at it.


## DOCUMENT: Migration

# Migration Guide

This guide helps you migrate from Whoosh legacy or Whoosh-Reloaded 3.x to Whoosh-NG v3.0.0.
> **Next release**: v4.0.0.dev0 (in development) will add SchemaBuilder, enhanced middleware exception hierarchy, and more — see the [CHANGELOG](https://github.com/dorel14/whoosh-ng/blob/master/CHANGELOG.md) for details.

## From Whoosh 1.x/2.x (Legacy)

### Import Paths

| Legacy | Whoosh-NG |
|--------|-----------|
| `import whoosh` | `import whoosh` |
| `from whoosh.index import create_in` | `from whoosh.index import create_in` |
| `from whoosh.fields import Schema, TEXT` | `from whoosh.fields import Schema, TEXT` |
| `from whoosh.qparser import QueryParser` | `from whoosh.qparser import QueryParser` |

The core API is intentionally stable. Most existing code works unchanged.

### Spelling API

```python
# Legacy
from whoosh.spelling import SpellChecker
corrector = SpellChecker(ix.reader(), "content")

# Whoosh-NG
from whoosh.spelling import ReaderCorrector
corrector = ReaderCorrector(ix.searcher().reader(), "content", ix.schema["content"])
suggestions = corrector.suggest("helo", limit=5)
```

### Highlighting

```python
# Legacy API unchanged
results[0].highlights("content")
```

## From Whoosh-Reloaded 3.x

### No Breaking Changes

Whoosh-NG is a continuation of Whoosh-Reloaded. All existing code works as-is.

### Optional: Plugin Migration

If you used `whoosh_modern` directly:

```python
# Old
from whoosh_modern.vector.numpy_provider import NumpyProvider

# New (via registry)
from whoosh.vector import NumpyProvider
from whoosh.registry import VectorRegistry

VectorRegistry.register("numpy", NumpyProvider(), "my_app")
```

### Middleware (New in v4.0.0.dev0)

```python
# Optional migration: add middleware to existing code

from whoosh.middleware import Middleware, MiddlewareContext

class LoggingMiddleware(Middleware):
    def before_search(self, context):
        print(f"Query: {context.query}")
        return context

# Wrap existing writer/searcher
writer = apply_middleware_to_writer(ix.writer(), [LoggingMiddleware()])
```

### SchemaBuilder (New in v4.0.0.dev0)

```python
# Old
schema = Schema(title=TEXT(stored=True), content=TEXT)

# New (fluent API)
from whoosh.fields import SchemaBuilder

schema = (
    SchemaBuilder()
    .field("title", TEXT(stored=True))
    .field("content", TEXT)
    .build()
)
```

## Upgrade Checklist

1. **Update dependencies**:
   ```bash
   pip install --upgrade whoosh-ng
   ```

2. **Run tests**:
   ```bash
   uv run pytest tests/ -q
   ```

3. **Update optional deps** (if using plugins):
   ```bash
   pip install whoosh-ng[all]
   ```

4. **Review middleware**: Consider adding middleware for cross-cutting concerns

5. **Update config**: If using `whoosh.config`, review new options

## Deprecations

| Feature | Status | Replacement |
|---------|--------|-------------|
| `whoosh_modern.vector` | Deprecated | `whoosh.vector` |
| Raw `whoosh.store` | Deprecated | `whoosh.backends` |
| Direct `SegmentWriter` usage | Discouraged | Use `IndexWriter` |

## Breaking Changes

Whoosh-NG maintains backward compatibility. If you find a breaking change, please report it as an issue.

### Exception Hierarchy

New in v4.0.0.dev0: `MiddlewareError` and `StopOperation` in middleware:

```python
from whoosh.middleware.exceptions import MiddlewareError, StopOperation
```

## Getting Help

- [GitHub Issues](https://github.com/your-org/whoosh-NG/issues)
- [Documentation](/)
- [Migration Examples](https://github.com/dorel14/whoosh-ng/tree/master/docs/archive_jekyll/_en/examples)


## DOCUMENT: Nested

# Nested Documents

This guide covers indexing and searching hierarchical/nested document
structures (e.g., a parent document with multiple child documents) using
Whoosh's parent-child relationship features.

## Defining Nested Documents

You can index parent documents that contain child documents by using a
parent field and child fields:

```python
from whoosh import fields, index

schema = fields.Schema(
    type=fields.ID(sortable=True),
    title=fields.TEXT(stored=True),
    content=fields.TEXT,
    section_name=fields.ID,
    section_content=fields.TEXT,
)
```

The `type` field distinguishes parent documents from child documents.

## Indexing Nested Documents

Use `IndexWriter.add_all()` with a generator that yields parent and child
documents grouped together:

```python
writer = ix.writer()
writer.add_all([
    parent_doc,
    child_doc_1,
    child_doc_2,
    parent_doc_2,
    child_doc_3,
])
```

Parent documents have `type="parent"` and child documents have
`type="child"`.

## Searching Nested Documents

### Parent-Query Child-Search

Search within child documents and match their parents:

```python
from whoosh.query import Every, Term
from whoosh.sorting import NestedParent

# Match all parent documents
parents = NestedParent(Term("type", "parent"))
q = Every("section_content", "hello")
results = searcher.search(q, sortedby=parents)
```

### Child-Query Parent-Search

Search for parent documents whose children match:

```python
from whoosh.sorting import NestedChildren

# Match parent documents that have children matching the query
parent_results = searcher.search(child_query, groupedby=NestedChildren(parent_matcher, child_matcher))
```

## Parent-Child Relationships at Index Time

When writing documents, use the `parent` parameter to link children to
parents:

```python
writer.add_document(type="parent", title="Chapter 1", _key="chapter1")
writer.add_document(type="child", section_name="Section 1.1",
                    section_content="...", parent="chapter1")
writer.add_document(type="child", section_name="Section 1.2",
                    section_content="...", parent="chapter1")
```

## Accessing Nested Results

To retrieve child matches alongside parent results, use the `expand` method
on the results:

```python
results = searcher.search(parent_query)
expanded = results.expand_child("section")
```

## Nested Faceting

Combine parent-child relationships with faceting using `NestedParent` and
`NestedChildren` as facets:

```python
parent_facet = NestedParent(FieldFacet("type"))
results = searcher.search(query, groupedby=parent_facet)
```

## Performance Considerations

- Parent-child joins are more expensive than flat document searches
- Use `childperm` searcher option to limit the number of permutations
  examined
- Consider whether hierarchical structure is needed at query time, or
  whether documents can be flattened during indexing


## DOCUMENT: Ngrams

# N-grams

This guide covers N-gram tokenization and analysis for substring matching,
prefix queries, and autocomplete functionality.

## What Are N-grams?

An N-gram is a contiguous sequence of N characters (or tokens) from a string.
For example, the 2-grams of "hello" are: "he", "el", "ll", "lo".

N-gram analysis is useful for:
- Substring search (finding "ell" within "hello")
- Autocomplete / typeahead suggestions
- Fuzzy matching without edit distance computation

## NgramTokenizer

The `NgramTokenizer` splits text into character-level N-grams:

```python
from whoosh.analysis import NgramTokenizer
from whoosh import fields

tokenizer = NgramTokenizer(minsize=2, maxsize=4)

schema = fields.Schema(
    content=fields.TEXT(analyzer=tokenizer),
)
```

### NgramTokenizer Parameters

- `minsize`: Minimum N-gram length (default `2`)
- `maxsize`: Maximum N-gram length (default `4`)

With the example above, the text "hello" produces these 2-4-grams:
`he, hel, hell, el, ell, ello, l, ll, llo, l, lo, o`

## NgramFilter

The `NgramFilter` creates word-level N-grams from tokenized text:

```python
from whoosh.analysis import RegexTokenizer, NgramFilter

analyzer = RegexTokenizer() | NgramFilter(maxsize=2)
```

This produces word-level grams: for "hello world", it produces ("hello",)
and ("hello", "world").

## NgramWordAnalyzer

A convenience analyzer that combines `NgramTokenizer` with `LowercaseFilter`:

```python
from whoosh.analysis import NgramWordAnalyzer

analyzer = NgramWordAnalyzer(minsize=2, maxsize=4)

schema = fields.Schema(
    content=fields.TEXT(analyzer=analyzer),
)
```

## Use Cases

### Substring Search

With N-gram analysis, you can match substrings:

```python
from whoosh.qparser import QueryParser

# Index text with N-grams
# Searching for "ell" matches "hello" because "ell" is a substring
qp = QueryParser("content", schema=ix.schema)
q = qp.parse("ell")
results = searcher.search(q)
```

### Prefix Matching

Set `maxsize` equal to a large value to effectively create prefix N-grams:

```python
from whoosh.analysis import NgramWordAnalyzer

# Create N-grams where each word's prefixes become searchable tokens
# e.g., "hello" -> "h", "he", "hel", "hell", "hello"
analyzer = NgramWordAnalyzer(minsize=1, maxsize=10)
```

### Autocomplete

N-gram indexes are commonly used for autocomplete/typeahead. For more
advanced autocomplete with edge n-grams, consider:

```python
from whoosh.analysis import RegexTokenizer, NgramFilter
from whoosh.query import Prefix

# Index with standard tokenization, then use Prefix queries for autocomplete
analyzer = RegexTokenizer()
schema = fields.Schema(
    title=fields.TEXT(stored=True, analyzer=analyzer),
    content=fields.TEXT(analyzer=analyzer),
)

# For autocomplete, query with Prefix
from whoosh.qparser import QueryParser
qp = QueryParser("title", schema=ix.schema)
q = Prefix("title", "hel")  # Find documents where title starts with "hel"
```

## Comparison with Edge N-grams

Some search engines support "edge n-grams" (only generating N-grams from the
beginning of words). This is more space-efficient for autocomplete:

- Full N-grams: "hello" → "he", "el", "ll", "lo", "hel", "ell", ...
- Edge N-grams: "hello" → "h", "he", "hel", "hell", "hello"

Whoosh's `NgramTokenizer` generates full (bidirectional) N-grams. For
edge-ngram-like behavior, use the `minsize` and `maxsize` parameters
strategically, or use `Prefix` queries against a standard tokenized field.

## Performance Considerations

- N-gram indexes are typically much larger than standard indexes
- Each original token produces multiple N-gram tokens, increasing index size
- Choose `minsize` and `maxsize` carefully to balance search quality against
  index size
- For autocomplete, consider using `Prefix` queries with a
  non-N-gram field for better performance


## DOCUMENT: Query

# Query Language

Whoosh-NG provides a powerful query language similar to Lucene's, as well as a programmatic query API.

## QueryParser

The `QueryParser` converts a query string into a query tree:

```python
from whoosh.qparser import QueryParser

# Parse a query for a specific field
qp = QueryParser("content", schema)
query = qp.parse("hello world")
```

## Query Syntax

### Basic Terms

```
hello                    # Single term
hello world              # Multiple terms (default AND)
hello OR world           # Explicit OR
"hello world"            # Phrase
```

### Field-Specific

```
title:python             # Search only in title field
title:"Python Tutorial"  # Phrase in specific field
```

### Boolean Operators

```
python AND whoosh
python OR whoosh
python AND NOT java
python AND (whoosh OR lucene)
```

### Prefix and Wildcard

```
pyth*                    # Prefix query
pyth?n                   # Single character wildcard
```

### Range Queries

```
date:[2020 TO 2025]
price:[10 TO 50]
rating:[4.0 TO *]        # Open-ended range
```

### Fuzzy Search

```
python~2                 # Edit distance <= 2
lucene~1                 # Approximate match
```

### Proximity Search

```
"hello world"~5          # Within 5 terms
```

### Boosting

```
python^2.0 whoosh        # Boost python by 2x
(title:python)^3 content:python  # Boost title matches
```

## Query Classes

You can build queries programmatically:

```python
from whoosh.query import *

# Simple term
q = Term("content", "python")

# Multiple terms (AND)
q = And([Term("content", "python"), Term("content", "whoosh")])

# Multiple terms (OR)
q = Or([Term("content", "python"), Term("content", "lucene")])

# Phrase
q = Phrase("content", ["hello", "world"])

# Range
q = NumericRange("price", 10, 50)
q = DateRange("date", datetime(2020,1,1), datetime(2025,1,1))

# Prefix
q = Prefix("content", "pyth")

# Wildcard
q = Wildcard("content", "pyth?n")

# Fuzzy
q = FuzzyTerm("content", "python", maxdist=2)

# Boost
q = Boost(Term("title", "python"), 2.0) & Term("content", "python")
```

## QueryParser Plugins

Extend query parsing with plugins:

```python
from whoosh.qparser import QueryParserPlugin

class MyPlugin(QueryParserPlugin):
    def __init__(self):
        pass

    def evaluate(self, env, signode):
        # Custom evaluation logic
        return Term("custom_field", signode.content)
```

## Multifield Search

Search multiple fields with different boosts:

```python
from whoosh.qparser import MultifieldParser

qp = MultifieldParser(
    ["title", "content", "tags"],
    schema,
    fieldboosts={"title": 2.0, "tags": 1.5}
)
q = qp.parse("python search")
```

## Default Operator

```python
from whoosh.qparser import QueryParser, OrGroup

# Default AND
qp = QueryParser("content", schema)

# Default OR
qp = QueryParser("content", schema, group=OrGroup)
```

## Escaping Special Characters

```
title\:python              # Literal colon
path\:\/\/example          # Escape special chars
```

## Regex Queries

```
content:/p[ya]thon/        # Regex match
```

## Advanced: Custom Queries

```python
from whoosh.query import Query

class CustomQuery(Query):
    def __init__(self, fieldname, text):
        self.fieldname = fieldname
        self.text = text

    def __repr__(self):
        return f"CustomQuery({self.fieldname!r}, {self.text!r})"

    def __hash__(self):
        return hash((self.fieldname, self.text))

    def __eq__(self, other):
        return (
            isinstance(other, CustomQuery)
            and self.fieldname == other.fieldname
            and self.text == other.text
        )

    def __ne__(self, other):
        return not self.__eq__(other)

    def matcher(self, searcher, context=None):
        # Return a custom matcher
        return CustomMatcher(searcher, self)
```


## DOCUMENT: Quickstart

# Quick Start

## Installation

```bash
pip install whoosh-ng
uv pip install whoosh-ng
```

## Basic Example

```python
from whoosh import index
from whoosh.fields import Schema, TEXT, ID

schema = Schema(id=ID(stored=True), content=TEXT())
ix = index.create_in("indexdir", schema)

with ix.writer() as w:
    w.add_document(id="1", content="hello world")
    w.add_document(id="2", content="goodbye world")

with ix.searcher() as s:
    results = s.search("world")
    for hit in results:
        print(hit["id"], hit.score)
```

## With Plugins

```bash
pip install whoosh-ng[vector,autocomplete,api]
```

```python
from whoosh.plugins.manager import PluginManager
from whoosh_modern.vector.plugin import VectorPlugin

PluginManager.load_plugins()
```

## Data Sources

```bash
pip install whoosh-ng
```

```python
import sqlite3
from whoosh_modern.data_sources import SQLSource
from whoosh_modern.views import SearchView

# Use existing benchmark data
conn = sqlite3.connect("benchmark/benchmark_data.db")
source = SQLSource(
    connection=conn,
    query="SELECT * FROM reuters_articles",
)

view = SearchView(name="reuters", source=source)
ix = view.build("indexdir")
```


## DOCUMENT: Recipes

# Whoosh recipes

A collection of small, practical code snippets for common tasks.

## General

### Get the stored fields for a document from the document number

```python
stored_fields = searcher.stored_fields(docnum)
```

## Analysis

### Eliminate words shorter/longer than N

Use a `StopFilter` and the `minsize` and `maxsize` keyword arguments. If you
just want to filter based on size and not common words, set the `stoplist` to
`None`:

```python
sf = analysis.StopFilter(stoplist=None, minsize=2, maxsize=40)
```

### Allow optional case-sensitive searches

Index both the original and lowercased versions of each word. If the user
searches for an all-lowercase word, it acts as a case-insensitive search, but
if they search for a word with any uppercase characters, it acts as a
case-sensitive search:

```python
class CaseSensitivizer(analysis.Filter):
    def __call__(self, tokens):
        for t in tokens:
            yield t
            if t.mode == "index":
                low = t.text.lower()
                if low != t.text:
                    t.text = low
                    yield t

ana = analysis.RegexTokenizer() | CaseSensitivizer()
print([t.text for t in ana("The new SuperTurbo 5000", mode="index")])
# ["The", "the", "new", "SuperTurbo", "superturbo", "5000"]
```

## Searching

### Find every document

```python
myquery = query.Every()
```

### iTunes-style search-as-you-type

Use `whoosh.analysis.NgramWordAnalyzer` as the analyzer for the field you want
to search as the user types. You can save space in the index by turning off
positions in the field using `phrase=False`:

```python
# For example, to search the "title" field as the user types
analyzer = analysis.NgramWordAnalyzer()
title_field = fields.TEXT(analyzer=analyzer, phrase=False)
schema = fields.Schema(title=title_field)
```

See the documentation for the `NgramWordAnalyzer` class for information on the
available options. Also see [N-grams](/core/ngrams).

## Shortcuts

### Look up documents by a field value

```python
# Single document (unique field value)
stored_fields = searcher.document(id="bacon")

# Multiple documents
for stored_fields in searcher.documents(tag="cake"):
    ...
```

## Sorting and scoring

See [Sorting](/core/sorting).

### Score results based on the position of the matched term

The following scoring function uses the position of the first occurrence of a
term in each document to calculate the score, so documents with the given term
earlier in the document will score higher:

```python
from whoosh import scoring

def pos_score_fn(searcher, fieldname, text, matcher):
    poses = matcher.value_as("positions")
    return 1.0 / (poses[0] + 1)

pos_weighting = scoring.FunctionWeighting(pos_score_fn)
with myindex.searcher(weighting=pos_weighting) as s:
    ...
```

## Results

### How many hits were there?

```python
# The number of scored hits
found = results.scored_length()

if results.has_exact_length():
    print("Scored", found, "of exactly", len(results), "documents")
else:
    low = results.estimated_min_length()
    high = results.estimated_length()
    print("Scored", found, "of between", low, "and", high, "documents")
```

### Which terms matched in each hit?

```python
# Use terms=True to record term matches for each hit
results = searcher.search(myquery, terms=True)

for hit in results:
    # Which terms matched in this hit?
    print("Matched:", hit.matched_terms())
    # Which terms from the query didn't match in this hit?
    print("Didn't match:", myquery.all_terms() - hit.matched_terms())
```

## Global information

### How many documents are in the index?

```python
# Including documents that are deleted but not yet optimized away
numdocs = searcher.doc_count_all()
# Not including deleted documents
numdocs = searcher.doc_count()
```

### What fields are in the index?

```python
return myindex.schema.names()
```

### Is term X in the index?

```python
return ("content", "wobble") in searcher
```

### How many times does term X occur in the index?

```python
# Number of times content:wobble appears in all documents
freq = searcher.frequency("content", "wobble")
# Number of documents containing content:wobble
docfreq = searcher.doc_frequency("content", "wobble")
```

### Is term X in document Y?

```python
# Without term vectors
postings = searcher.postings("content", "wobble")
postings.skip_to(500)
return postings.id() == 500

# If field has term vectors
vector = searcher.vector(500, "content")
vector.skip_to("wobble")
return vector.id() == "wobble"
```

## See also

- [Analysis](/core/analysis) — Analyzers, tokenizers, and filters
- [N-grams](/core/ngrams) — Search-as-you-type with N-gram analyzers
- [Searching](/core/searching) — The `search()` method and `Hit` objects


## DOCUMENT: Schema

# Schema Design

How to model documents with Whoosh-NG fields.

## Field types

| Type | Searchable | Stored |
|------|------------|--------|
| TEXT | Yes | Optional |
| ID | Yes | Optional |
| KEYWORD | Yes | Optional |
| STORED | No | Yes |
| NUMERIC | Yes | Optional |
| DATETIME | Yes | Optional |
| BOOLEAN | Yes | Optional |
| VectorField | Provider | Optional |

## Building a schema

```python
from whoosh.fields import Schema, TEXT, ID, KEYWORD, STORED, NUMERIC, BOOLEAN, VectorField

schema = Schema(
    title=TEXT(stored=True),
    slug=ID(stored=True, unique=True),
    content=TEXT,
    tags=KEYWORD(lowercase=True, commas=True),
    published=NUMERIC(int, stored=True),
    featured=BOOLEAN(stored=True),
    embedding=VectorField(dimensions=384, metric="cosine")
)
```

## Multi-value fields

Pass lists for multiple values.

```python
writer.add_document(
    title="Multi-tag post",
    tags=["whoosh", "python", "search"],
    content="..."
)
```

## Per-field boost

Boost fields at write time.

```python
writer.add_document(
    title="Breaking News",
    title_boost=3.0,
    content="..."
)
```

## SchemaBuilder

```python
from whoosh.fields import SchemaBuilder, TEXT, ID, NUMERIC

schema = (
    SchemaBuilder()
    .field("title", TEXT(stored=True))
    .field("path", ID(stored=True, unique=True))
    .field("rating", NUMERIC(float, stored=True))
    .build()
)
```

## Modifying fields

```python
writer.add_document(
    title="Multi-tag post",
    tags=["whoosh", "python", "search"],
    content="..."
)
```

## Search Models

Whoosh-NG can auto-map Python models (dataclasses, Pydantic, SQLAlchemy, SQLModel, msgspec) to a Whoosh `Schema` using `ModelIndex`.

### Level 1: Auto-mapping

```python
from dataclasses import dataclass
from whoosh_modern.models import ModelIndex

@dataclass
class Book:
    title: str
    count: int
    tag: str | None = None

idx = ModelIndex(Book)
schema = idx.schema
```

`ModelIndex` inspects type annotations and maps them to Whoosh fields:

| Python type | Whoosh field |
|-------------|--------------|
| `str` | `TEXT` |
| `int` / `float` | `NUMERIC` |
| `bool` | `BOOLEAN` |
| `datetime` / `date` | `DATETIME` |
| `Decimal` | `NUMERIC(int, decimal_places=2)` |
| `Enum` | `KEYWORD` |
| `bytes` | `KEYWORD` (hex-encoded) |
| `list[str]` | `KEYWORD` |
| `Optional[T]` | mapped type or `STORED` |

ID fields are auto-detected: explicit `SearchOptions(id=True)` > field named `id`/`ID`/`_id` > first `str` field.

### Level 2: Explicit options

Use `SearchField` to override defaults:

```python
from whoosh_modern.models import SearchField, SearchOptions

class Book:
    title: str = SearchField(fulltext=True, stored=True, analyzer="Simple")
    count: int = SearchField(sortable=True)
    tags: list[str] = SearchField(multi=True)
```

### Level 3: Annotated types

Use `Annotated` to attach metadata directly to annotations:

```python
from typing import Annotated
from whoosh_modern.models import SearchField

class Book:
    title: Annotated[str, SearchField(fulltext=True, stored=True)]
```

### Integrations

#### Dataclass

```python
from dataclasses import dataclass
from whoosh_modern.models import ModelIndex

@dataclass
class Article:
    title: str
    body: str
    published: datetime.datetime

idx = ModelIndex(Article)
```

#### Pydantic v2

```python
from pydantic import BaseModel
from whoosh_modern.models import register_model

class Article(BaseModel):
    title: str
    body: str
    published: datetime.datetime

    # Per-field search metadata via json_schema_extra
    model_config = {"json_schema_extra": {"search": {"fulltext": True}}}

idx = register_model(Article)
```

#### SQLAlchemy

```python
from sqlalchemy import Column, Integer, String, DateTime
from sqlalchemy.orm import DeclarativeBase
from whoosh_modern.models import register_model

class Base(DeclarativeBase):
    pass

class Article(Base):
    __tablename__ = "articles"
    id = Column(Integer, primary_key=True)
    title = Column(String, info={"search": {"fulltext": True, "stored": True}})
    published = Column(DateTime, info={"search": {"sortable": True}})

idx = register_model(Article)
```

#### SQLModel

```python
from sqlmodel import SQLModel, Field
from whoosh_modern.models import register_model

class Article(SQLModel, table=True):
    id: int = Field(primary_key=True)
    title: str = Field(sa_column_kwargs={"info": {"search": {"fulltext": True}}})
    published: datetime.datetime

idx = register_model(Article)
```

#### msgspec

```python
import msgspec
from whoosh_modern.models import register_model

class Article(msgspec.Struct):
    title: str = msgspec.field(metadata={"search": {"fulltext": True}})
    published: datetime.datetime

idx = register_model(Article)
```

### Converting instances

```python
doc = idx.to_whoosh_document(book_instance)
writer.add_document(**doc)
```

`to_whoosh_document` handles:
- dataclass: `dataclasses.fields()` iteration
- Pydantic/SQLModel: `model_fields` iteration
- SQLAlchemy: `__mapper__.columns` iteration
- Enum values converted to `.value`
- `bytes` converted to hex string

## Best practices

1. **Minimal**: Only index what you search
2. **STORED sparingly**: Increases index size
3. **Unique fields**: Use `unique=True` for identifiers
4. **Field boost**: Boost important fields at schema level
5. **TEXT options**: Disable `phrase` if you don't need phrase search
6. **ID field**: Let `ModelIndex` auto-detect or explicitly mark with `SearchOptions(id=True)`


## DOCUMENT: Searching

# Searching

This guide covers executing searches, working with results, scoring, sorting, and filtering.

## Basic Search

```python
from whoosh.qparser import QueryParser

qp = QueryParser("content", ix.schema)
query = qp.parse("hello world")

with ix.searcher() as searcher:
    results = searcher.search(query)
    for hit in results:
        print(hit["title"], hit.score)
```

## The Searcher

The `Searcher` is the main interface for reading the index. It is lightweight and supports context management:

```python
# Always use context manager when possible
with ix.searcher() as searcher:
    results = searcher.search(query)

# Or manage manually
searcher = ix.searcher()
try:
    results = searcher.search(query)
finally:
    searcher.close()
```

### Searcher Options

```python
searcher = ix.searcher(
    weighting=None,       # Custom weighting model
    childperm=None,       # Permutations for nested documents
    fromindex=None        # Source index for cached readers
)
```

## QueryParser

Convert query strings into query objects:

```python
from whoosh.qparser import QueryParser, OrGroup

# Default: AND between terms
qp = QueryParser("content", schema)
q = qp.parse("hello world")  # Equivalent to: content:hello AND content:world

# Change default operator
qp = QueryParser("content", schema, group=OrGroup)
q = qp.parse("hello world")  # Equivalent to: content:hello OR content:world
```

## Search Methods

### search()

```python
results = searcher.search(
    query,
    limit=10,           # Max results (None for all)
    sortedby=None,      # Sort key(s)
    reverse=False,      # Reverse sort order
    terms=False,        # Collect matched terms
    filter=None,        # Allow only these docnums
    mask=None,          # Exclude these docnums
    collapse=None,      # Collapse facet
    collapse_limit=1    # Max docs per collapse key
)
```

### search_page()

```python
# Get page 1, 10 results per page (default)
results = searcher.search_page(query, 1)

# Get page 3, 20 results per page
results = searcher.search_page(query, 3, pagelen=20)
```

### search_with_collector()

For advanced result collection:

```python
from whoosh.collectors import FacetCollector

collector = FacetCollector(facets=[sorting.FieldFacet("date")])
searcher.search_with_collector(query, collector)
```

## Results Object

`Results` acts like a list of matched documents:

```python
results = searcher.search(query)

# Slice support
first_five = results[0:5]

# Length (may trigger recount)
total = len(results)

# Scored length (usually what was actually returned)
scored = results.scored_length()

# Iteration
for hit in results:
    print(hit["title"], hit.score)
```

### Hit Object

```python
for hit in results:
    # Stored fields
    title = hit["title"]
    path = hit["path"]

    # Score
    print(hit.score)

    # Highlighting
    highlights = hit.highlights("content", top=3)

    # Matched terms (if terms=True was used)
    if results.has_matched_terms():
        print(hit.matched_terms())
```

## Scoring

The default scoring model is BM25F:

```python
from whoosh import scoring

with ix.searcher(weighting=scoring.BM25F()) as s:
    results = s.search(query)
```

### Custom Scoring

```python
class MyScorer(scoring.WeightingModel):
    def scorer(self, searcher, fieldname, text, qf=1):
        return MyCustomScorer(searcher, fieldname, text, qf)

with ix.searcher(weighting=MyScorer()) as s:
    results = s.search(query)
```

## Sorting

Sort by a field or facet:

```python
from whoosh import sorting

# Sort by a single field
results = searcher.search(query, sortedby="date")

# Reverse sort
results = searcher.search(query, sortedby="date", reverse=True)

# Multi-field sort
results = searcher.search(query, sortedby=[
    sorting.FieldFacet("category"),
    sorting.ScoreFacet()
])
```

## Faceting

Used for grouping results:

```python
from whoosh import sorting

facet = sorting.FieldFacet("category")
with searcher.all_features() as features:
    facets = features.facet(facet)
    for cat, count in facets.most_common():
        print(f"{cat}: {count}")
```

## Filtering and Masking

```python
# Only show documents matching a subquery
filter_q = Term("published", True)
results = searcher.search(query, filter=filter_q)

# Exclude documents
mask_q = Term("draft", True)
results = searcher.search(query, mask=mask_q)
```

## Collapsing

Remove duplicates or limit per-group:

```python
from whoosh import sorting

# Collapse by hostname, keep top 3 per host
results = searcher.search(
    query,
    collapse=sorting.FieldFacet("hostname"),
    collapse_limit=3
)

# Collapse ordering (keep highest rated per type)
results = searcher.search(
    query,
    sortedby=sorting.FieldFacet("price", reverse=True),
    collapse=sorting.FieldFacet("type"),
    collapse_order=sorting.FieldFacet("rating", reverse=True)
)
```

## Highlighting

Get highlighted snippets for query terms:

```python
results = searcher.search(query, terms=True)

for hit in results:
    print(hit.highlights("content", top=2))

# Custom fragmenter
from whoosh.highlight import highlight

fragments = hit.highlights(
    "content",
    top=3,
    fragmenter=...,
    formatter=...
)
```

## Time-Limited Searches

```python
from whoosh.collectors import TimeLimitCollector

with ix.searcher() as s:
    c = s.collector(limit=None)
    tlc = TimeLimitCollector(c, timelimit=5.0)
    try:
        s.search_with_collector(query, tlc)
    except TimeLimit:
        print("Search aborted: too slow")
    results = tlc.results()
```

## Combining Results

```python
# Run two queries
best_bet_results = s.search(best_bet_query, limit=5)
main_results = s.search(main_query, limit=10)

# Merge: duplicates go to top, then append rest
best_bet_results.upgrade_and_extend(main_results)
```


## DOCUMENT: Sorting

# Sorting

The `whoosh.sorting` module provides facets and sort-key computation for ordering and grouping search results.

## Quick start

```python
from whoosh import sorting

# Sort by a field
results = searcher.search(query, sortedby="date")

# Sort descending
results = searcher.search(query, sortedby=sorting.FieldFacet("price", reverse=True))
```

For the full API reference, see [Sorting API](/api/sorting).


## DOCUMENT: Spelling

# "Did you mean... ?" Correcting errors in user queries

## Overview

Whoosh can quickly suggest replacements for mis-typed words by returning a list
of words from the index (or a dictionary) that are close to the mis-typed word:

```python
with ix.searcher() as s:
    corrector = s.corrector("text")
    for mistyped_word in mistyped_words:
        print(corrector.suggest(mistyped_word, limit=3))
```

See the `whoosh.spelling.Corrector.suggest()` method documentation for
information on the arguments.

Currently the suggestion engine is more like a "typo corrector" than a real
"spell checker" since it doesn't do the kind of sophisticated phonetic matching
or semantic/contextual analysis a good spell checker might. However, it is
still very useful.

There are two main strategies for correcting words:

- Use the terms from an index field.
- Use words from a word list.

## Pulling suggestions from an indexed field

In Whoosh 2.7 and later, spelling suggestions are available on all fields.
However, if you have an analyzer that modifies the indexed words (such as
stemming), you can add `spelling=True` to a field to have it store separate
unmodified versions of the terms for spelling suggestions:

```python
ana = analysis.StemmingAnalyzer()
schema = fields.Schema(text=TEXT(analyzer=ana, spelling=True))
```

You can then use the `whoosh.searching.Searcher.corrector()` method to get a
corrector for a field:

```python
corrector = searcher.corrector("content")
```

The advantage of using the contents of an index field is that when you are
spell checking queries on that index, the suggestions are tailored to the
contents of the index. The disadvantage is that if the indexed documents
contain spelling errors, then the spelling suggestions will also be erroneous.

## Pulling suggestions from a word list

There are plenty of word lists available on the internet you can use to populate
the spelling dictionary. `word_list` can be a list of unicode strings, or a
file object with one word on each line.

```python
from whoosh.spelling import ListCorrector

# word_list must be a sorted list of unicode strings
corrector = ListCorrector(word_list)
```

## Merging two or more correctors

You can combine suggestions from two sources (for example, the contents of an
index field and a word list) using a `whoosh.spelling.MultiCorrector`:

```python
c1 = searcher.corrector("content")
c2 = spelling.ListCorrector(word_list)
corrector = MultiCorrector([c1, c2])
```

## Correcting user queries

You can spell-check a user query using the
`whoosh.searching.Searcher.correct_query()` method:

```python
from whoosh import qparser

# Parse the user query string
qp = qparser.QueryParser("content", myindex.schema)
q = qp.parse(qstring)

# Try correcting the query
with myindex.searcher() as s:
    corrected = s.correct_query(q, qstring)
    if corrected.query != q:
        print("Did you mean:", corrected.string)
```

The `correct_query` method returns an object with the following attributes:

- `query` — A corrected `whoosh.query.Query` tree. Compare it (`==`) with the
  original parsed query to check if the corrector changed anything.
- `string` — A corrected version of the user's query string.
- `tokens` — A list of corrected token objects representing the corrected terms.

You can use a `whoosh.highlight.Formatter` object to format the corrected query
string, for example the `HtmlFormatter` to format it as HTML:

```python
from whoosh import highlight

hf = highlight.HtmlFormatter()
corrected = s.correct_query(q, qstring, formatter=hf)
```

## See also

- [Highlighting](/core/highlight) — Format corrected query strings with a formatter
- [Query Language](/core/query) — Parsing user queries
- [API: spelling](../api/spelling) — Full `whoosh.spelling` reference


## DOCUMENT: Stemming

# Stemming and Stop Words

This guide covers using stemmers, stop-word filters, and language-specific
text analysis with Whoosh.

## Stemmers

A stemmer reduces words to their root form (e.g., "running" → "run",
"cats" → "cat"), so that different forms of the same word match in
searches.

### Using StemmerFilter

```python
from whoosh.analysis import RegexTokenizer, StemmerFilter
from whoosh.lang.porter import stem
from whoosh import fields

# English Porter stemmer
stem_analyzer = RegexTokenizer() | StemmerFilter(stemfn=stem)

schema = fields.Schema(
    title=fields.TEXT(stored=True),
    content=fields.TEXT(analyzer=stem_analyzer),
)
```

### Snowball Stemmers

Whoosh includes Snowball stemmers for multiple languages:

```python
from whoosh.analysis import StemmerFilter
from whoosh.lang.snowball import EnglishStemmer

stem_analyzer = RegexTokenizer() | StemmerFilter(stemfn=EnglishStemmer().stem)
```

### Language-Aware Stemmer Selection

```python
from whoosh.lang import stemmer_for_language, StemmerFilter
from whoosh.analysis import RegexTokenizer

stem = stemmer_for_language("en")
analyzer = RegexTokenizer() | StemmerFilter(stemfn=stem)

# Or use the analysis StemmingAnalyzer:
from whoosh.analysis import StemmingAnalyzer

analyzer = StemmingAnalyzer("en")
```

### Available Languages

```python
from whoosh.lang import languages, has_stemmer, has_stopwords

print(languages)  # ('ar', 'da', 'nl', 'en', 'fi', 'fr', ...)
print(has_stemmer("en"))  # True
print(has_stopwords("en"))  # True
```

## Stop Words

Stop words are common words (like "the", "a", "and") that are typically
filtered out during indexing since they appear in too many documents to be
useful for ranking.

### Using StopFilter

```python
from whoosh.analysis import RegexTokenizer, StopFilter
from whoosh.lang import stopwords_for_language

# English stop words
stop_words = set(stopwords_for_language("en"))
stop_analyzer = RegexTokenizer() | StopFilter(stoplist=stop_words)

schema = fields.Schema(
    content=fields.TEXT(analyzer=stop_analyzer),
)
```

### Combining Stemming and Stop Words

```python
from whoosh.analysis import StemmingAnalyzer

# StemmingAnalyzer automatically loads stemmer and stopwords for the language
analyzer = StemmingAnalyzer("en")

schema = fields.Schema(
    content=fields.TEXT(analyzer=analyzer),
)
```

### Custom Stop Words

```python
from whoosh.analysis import RegexTokenizer, StopFilter

# Custom stop words list
custom_stops = frozenset(["the", "a", "an", "foo", "bar"])
analyzer = RegexTokenizer() | StopFilter(stoplist=custom_stops)
```

## StemmingAnalyzer (Recommended)

The `StemmingAnalyzer` combines tokenizer, stemming, and stop word filtering:

```python
from whoosh.analysis import StemmingAnalyzer

# Automatically uses the correct stemmer and stop words for the language
analyzer = StemmingAnalyzer("en")

# You can override defaults
analyzer = StemmingAnalyzer("en",
                            use_stopwords=True,
                            use_stems=True)
```

### StemmingAnalyzer Options

- `lang`: Language code (e.g., `"en"`, `"fr"`, `"de"`)
- `use_stopwords`: Whether to load and apply stop words (default `True`)
- `use_stems`: Whether to apply stemming (default `True`)
- `args`: Arguments passed to the tokenizer
- `kwargs`: Keyword arguments for the stemmer or stopwords

## Language-Specific Considerations

### Arabic (ISRI Stemmer)

```python
from whoosh.analysis import StemmerFilter
from whoosh.lang.isri import ISRIStemmer

stem_analyzer = RegexTokenizer() | StemmerFilter(stemfn=ISRIStemmer().stem)
```

### Double Metaphone for Phonetic Matching

```python
from whoosh.analysis import RegexTokenizer, DoubleMetaphoneFilter

analyzer = RegexTokenizer() | DoubleMetaphoneFilter()
```

## Query-Side Stemming

The analyzer is applied at both index time and query time (via the query
parser), so stemming is automatically applied to search terms:

```python
from whoosh.qparser import QueryParser

# If the index uses stemming, queries are stemmed too
qp = QueryParser("content", schema=ix.schema)
q = qp.parse("running cats")  # Will match "run", "cat", etc.
```

## N-gram Analysis

For substring and prefix matching, use N-gram analyzers:

```python
from whoosh.analysis import NgramWordAnalyzer

analyzer = NgramWordAnalyzer(minsize=2, maxsize=4)
schema = fields.Schema(content=fields.TEXT(analyzer=analyzer))
```

See the [N-grams Guide](ngrams.md) for more details.

## Modern Stemmer Providers (Whoosh-NG 2.0)

Whoosh-NG 2.0 introduces a plugin-style stemmer provider system with auto-detection, PyStemmer support, and language-specific analyzers. For full details, see the [Stemmer Providers Guide](stemming-providers.md).


## DOCUMENT: Threads

# Concurrency, locking, and versioning

## Concurrency

The `FileIndex` object is "stateless" and should be share-able between threads.

A `Reader` object (which underlies the `Searcher` object) wraps open files and
often individual methods rely on consistent file cursor positions (e.g. they
do two `file.read()`s in a row, so if another thread moves the cursor between
the two read calls Bad Things would happen). You should use one Reader/Searcher
per thread in your code.

Readers/Searchers tend to cache information (such as field caches for
sorting), so if you can share one across multiple search requests, it's a big
performance win.

> Whoosh-NG also provides `AsyncWriter` and `BufferedWriter` in
> `whoosh.writing` (see [Indexing](/core/indexing)) as convenient wrappers for
> multi-process write scenarios.

## Locking

Only one thread/process can write to an index at a time. When you open a
writer, it locks the index. If you try to open a writer on the same index in
another thread/process, it will raise `whoosh.store.LockError`.

In a multi-threaded or multi-process environment your code needs to be aware
that opening a writer may raise this exception if a writer is already open.
Whoosh includes a couple of example implementations
(`whoosh.writing.AsyncWriter` and `whoosh.writing.BufferedWriter`) of ways to
work around the write lock.

While the writer is open and during the commit, **the index is still available
for reading**. Existing readers are unaffected and new readers can open the
current index normally.

### Lock files

Locking the index is accomplished by acquiring an exclusive file lock on the
`<indexname>_WRITELOCK` file in the index directory. The file is not deleted
after the file lock is released, so the fact that the file exists **does not**
mean the index is locked.

## Versioning

When you open a reader/searcher, the reader represents a view of the **current
version** of the index. If someone writes changes to the index, any readers
that are already open **will not** pick up the changes automatically. A reader
always sees the index as it existed when the reader was opened.

If you are re-using a Searcher across multiple search requests, you can check
whether the Searcher is a view of the latest version of the index using
`whoosh.searching.Searcher.up_to_date()`. If the searcher is not up to date,
you can get an up-to-date copy of the searcher using
`whoosh.searching.Searcher.refresh()`:

```python
# If 'searcher' is not up-to-date, replace it
searcher = searcher.refresh()
```

If the searcher has the latest version of the index, `refresh()` simply returns
it. Calling `Searcher.refresh()` is more efficient than closing the searcher
and opening a new one, since it will re-use any underlying readers and caches
that haven't changed.

## See also

- [Indexing](/core/indexing) — Writers, `AsyncWriter`, `BufferedWriter`
- [API: writing](../api/writing) — Writer concurrency helpers


## DOCUMENT: Translation Status

# Translation Completion Tracking

- [x] EN quickstart
- [x] EN guides
- [x] EN API pages
- [x] EN examples
- [x] FR quickstart
- [x] FR guides
- [x] FR API pages
- [x] FR examples


## DOCUMENT: Autocomplete

# Autocomplete with Whoosh-NG

Whoosh-NG provides autocomplete functionality through the `whoosh_modern.autocomplete` module.

## Install

```bash
pip install "whoosh-ng[autocomplete]"
```

## Schema with Keyword Field for Terms

```python
from whoosh import index
from whoosh.fields import Schema, TEXT, KEYWORD

schema = Schema(
    title=TEXT(stored=True),
    tags=KEYWORD(stored=True, commas=True),
)

ix = index.create_in("autocomplete_index", schema)
```

## Index Documents

```python
with ix.writer() as w:
    w.add_document(title="Python Programming", tags="python,programming,language")
    w.add_document(title="JavaScript Basics", tags="javascript,programming,web")
    w.add_document(title="Machine Learning", tags="ml,ai,data-science")
    w.add_document(title="Deep Learning", tags="ml,ai,neural-networks")
    w.commit()
```

## Basic Autocomplete

```python
from whoosh_modern.autocomplete import create_autocomplete

# Create an autocomplete provider (supports "inverted" provider type)
provider = create_autocomplete("inverted")

# Add phrases to index
provider.add(["python", "programming", "javascript", "machine learning", "deep learning"])

# Search for suggestions
hits = provider.search("py", limit=5)
for hit in hits:
    print(hit.text, hit.score)
# Output: python 1.5, programming 0.2
```

## Real-time Suggestion Endpoint

```python
from fastapi import FastAPI
from whoosh_modern.autocomplete import create_autocomplete

app = FastAPI()
provider = create_autocomplete("inverted")

# Populate provider with terms from your index
# (typically done during indexing)
provider.add(["python", "programming", "javascript", "machine learning"])

@app.get("/suggest")
async def suggest(q: str, limit: int = 5):
    hits = provider.search(q, limit=limit)
    return {"suggestions": [hit.text for hit in hits]}
```

## Key Points

- Install with `pip install whoosh-ng[autocomplete]`.
- Use `KEYWORD` fields to store multi-value tags/terms.
- Use `create_autocomplete("inverted")` to create a provider.
- The `InvertedIndexAutocomplete` provider supports prefix matching with scoring.
- Each result is an `AutocompleteHit` with `text` and `score` attributes.


## DOCUMENT: Basic Indexing

# Basic Indexing

Examples for indexing documents in Whoosh‑NG. Each section is a self-contained, **runnable** script.

> **Real-world scenario**: You're building a blog search engine. You have a CSV file
> of articles (`blog_posts.csv`) with `title`, `url`, `tags`, and `body` columns.

## 1. Define a Production Schema

```python
from whoosh import index
from whoosh.fields import Schema, TEXT, ID, KEYWORD, NUMERIC, DATETIME
from datetime import datetime

# Stored=True keeps the field value in the index so you can retrieve it
# in search results without querying an external DB.
schema = Schema(
    doc_id=ID(stored=True, unique=True),     # primary key
    title=TEXT(stored=True),                  # full-text searchable + retrievable
    url=ID(stored=True),                      # stored only, no full-text analysis
    tags=KEYWORD(stored=True, commas=True),   # multi-value: "python,search,guide"
    body=TEXT(stored=True, phrase=True),      # searchable text with phrase queries
    published_at=DATETIME(stored=True, sortable=True),
    word_count=NUMERIC(int, stored=True),
)
```

## 2. Build an Index from a CSV File

```python
import csv
import shutil
from whoosh import index

# Clean prior index (development only!)
shutil.rmtree("blog_index", ignore_errors=True)
ix = index.create_in("blog_index", schema)

# Simulate a CSV file with blog post data
# blog_posts.csv:
#   doc_id,title,url,tags,published_at,word_count,body
#   1,Building a Search Engine,/posts/1,python,search,2024-01-15,1200,"Learn how to build..."
#   2,Python Tips,/posts/2,python,tips,2024-02-20,800,"Ten tips for Python..."

with open("blog_posts.csv", newline="", encoding="utf-8") as f:
    reader = csv.DictReader(f)
    with ix.writer() as writer:
        for row in reader:
            writer.add_document(
                doc_id=row["doc_id"],
                title=row["title"],
                url=row["url"],
                tags=row["tags"],
                published_at=datetime.fromisoformat(row["published_at"]),
                word_count=int(row["word_count"]),
                body=row["body"],
            )
        writer.commit()
```

## 3. Incremental Update — Re-index Modified Documents

```python
# Suppose your CMS tells you which posts were updated since the last sync
updated_posts = [
    {"doc_id": "1", "title": "Building a Search Engine (Updated)", "body": "Updated content..."},
    {"doc_id": "3", "title": "New Post", "body": "Fresh content..."},
]

with ix.writer() as writer:
    for post in updated_posts:
        writer.update_document(
            doc_id=post["doc_id"],
            title=post["title"],
            url=f"/posts/{post['doc_id']}",
            tags="python,search",
            published_at=datetime(2024, 6, 1),
            word_count=len(post["body"].split()),
            body=post["body"],
        )
    writer.commit()
```

## 4. Delete Documents by Term

```python
from whoosh.query import Term

# Remove a post by its unique doc_id
with ix.writer() as writer:
    writer.delete_by_term("doc_id", "3")
    writer.commit()
```

## 5. Bulk Insert for Large Datasets (10k+ Documents)

```python
from whoosh.writing import BufferedWriter

# Use BufferedWriter for high-throughput indexing.
# It buffers documents and commits in batches.
buffered = BufferedWriter(ix, period=60, limit=500)

try:
    for doc in large_dataset:  # your generator/list of dicts
        with buffered:
            buffered.add_document(
                doc_id=doc["doc_id"],
                title=doc["title"],
                url=doc["url"],
                tags=",".join(doc["tags"]),
                published_at=doc["published_at"],
                word_count=doc["word_count"],
                body=doc["body"],
            )
finally:
    buffered.close()
```

## 6. Run a Search on the Indexed Data

```python
from whoosh.qparser import QueryParser

ix = index.open_dir("blog_index")

with ix.searcher() as s:
    qp = QueryParser("body", ix.schema)
    q = qp.parse("search engine")

    results = s.search(q, limit=10)
    for hit in results:
        print(f"Title: {hit['title']}")
        print(f"URL:   {hit['url']}")
        print(f"Score: {hit.score:.3f}")
        print(f"Snippet: {hit.highlights('body')}")
        print("---")


## DOCUMENT: Data Sources

# Data Sources

Whoosh-NG provides a flexible data source layer for indexing documents from SQL databases, REST APIs, GraphQL APIs, file-based formats, and custom data providers.

## DataSource Protocol

All data sources implement the `DataSource` protocol, which defines the interface for querying, schema discovery, and metadata retrieval.

```python
from whoosh_modern.data_sources import DataSource

class DataSource(Protocol):
    @property
    def name(self) -> str: ...

    def discover_schema(self) -> Schema: ...
    def iter_documents(self) -> Iterator[Document]: ...
    def stream_batches(self, batch_size: int = 1000) -> Iterator[list[dict[str, Any]]]: ...
    def health_check(self) -> bool: ...
```

### Capability Protocols

| Protocol | Description |
|----------|-------------|
| `DataSource` | Base protocol: name, schema, iteration, metadata |
| `IncrementalDataSource` | Supports `iter_changes(since)` |
| `AsyncDataSource` | Async document streaming via `aiter_documents()` |
| `RefreshableDataSource` | `refresh()` support |
| `CountableDataSource` | `document_count()` |
| `MetadataDataSource` | `metadata()` |
| `ObservableDataSource` | Observer callbacks for document changes |

---

## SQLSource

`SQLSource` connects to SQL databases and yields documents from query results with automatic connection pooling.

### Basic Usage

```python
from whoosh_modern.data_sources.sql import SQLSource
import sqlite3

conn = sqlite3.connect("mydb.db")
source = SQLSource(
    connection=conn,
    query="SELECT * FROM products",
)

# Discover schema from result-set metadata
schema = source.discover_schema()

# Iterate documents
for doc in source.iter_documents():
    print(doc["title"], doc["price"])

# Get metadata
meta = source.metadata()
# {"type": "sql", "query": "SELECT * FROM products", ...}

# Get document count
count = source.document_count()
```

### Connection Pooling (SQLSource)

Connection pooling is supported via `pool_size` for long-running processes:

```python
from whoosh_modern.data_sources.sql import SQLSource

source = SQLSource(
    connection="sqlite:///mydb.db",  # URL or connection object
    query="SELECT * FROM products",
    pool_size=10,          # Max connections in pool
)
```

### GROUP BY Aggregation

```python
source = SQLSource(
    connection=conn,
    query="""
        SELECT category, COUNT(*) as doc_count,
               AVG(price) as avg_price
        FROM products
        GROUP BY category
    """,
)

# Each aggregated row becomes a document
for doc in source.iter_documents():
    print(doc["category"], doc["doc_count"], doc["avg_price"])
```

### JOINs with Column Aliases

`SQLSource` uses result-set metadata for schema discovery. Always use column aliases in JOINs.

```python
source = SQLSource(
    connection=conn,
    query="""
        SELECT
            p.id AS product_id,
            p.name AS product_name,
            c.name AS category_name
        FROM products p
        JOIN categories c ON p.category_id = c.id
    """,
)
```

### Incremental Sync

```python
from datetime import datetime

source = SQLSource(
    connection=conn,
    query="SELECT * FROM articles",
    incremental_field="updated_at",
    id_field="id",
)

# Get documents changed since a timestamp
for doc in source.iter_changes(since=datetime(2025, 1, 1)):
    print(doc["id"], doc["updated_at"])
```

### SQLAlchemySource

For SQLAlchemy users, use `SQLAlchemySource` which supports engine-based connection management:

```python
from whoosh_modern.data_sources.sqlalchemy_ds import SQLAlchemySource
from sqlalchemy import create_engine

engine = create_engine("postgresql://user:pass@localhost/mydb")
source = SQLAlchemySource(
    engine=engine,
    query="SELECT * FROM articles",
    incremental_field="updated_at",
    id_field="id",
)

schema = source.discover_schema()
```

### PeeweeSource

For Peewee ORM users:

```python
from whoosh_modern.data_sources.peewee_ds import PeeweeSource
from peewee import SqliteDatabase

db = SqliteDatabase("mydb.db")
source = PeeweeSource(
    database=db,
    model=MyArticleModel,
    fields=["id", "title", "content"],
)

schema = source.discover_schema()
```

### TortoiseSource

For Tortoise ORM users (async):

```python
from whoosh_modern.data_sources.tortoise_ds import TortoiseSource

source = TortoiseSource(
    model="myapp.models.Article",
    fields=["id", "title", "content"],
)

schema = source.discover_schema()
count = source.document_count()
```

---

## RESTSource

`RESTSource` fetches documents from REST APIs with pagination and authentication.

### Basic Usage

```python
from whoosh_modern.data_sources.rest import RESTSource

source = RESTSource(
    url="https://api.example.com/v2/products",
    method="GET",
    headers={"Authorization": "Bearer your_token"},
    pagination="page",
    page_size=50,
)

# Discover schema from first page
schema = source.discover_schema()

# Iterate all documents (handles pagination automatically)
for doc in source.iter_documents():
    print(doc["name"], doc["price"])

# Get document count
count = source.document_count()
```

### Pagination Strategies

| Strategy | Parameters | Behavior |
|----------|-----------|----------|
| `page` | `?page=N&size=M` | Fetches page N with M items per page |
| `offset` | `?offset=N&limit=M` | Fetches M items starting at N |
| `cursor` | `?cursor=XYZ&size=M` | Follows `next_cursor` in response |

```python
# Page-based pagination
source = RESTSource(
    url="https://api.example.com/articles",
    pagination="page",
    page_size=50,
)

# Offset-based pagination
source = RESTSource(
    url="https://api.example.com/records",
    pagination="offset",
    page_size=100,
)

# Cursor-based pagination
source = RESTSource(
    url="https://api.example.com/feed",
    pagination="cursor",
    page_size=100,
)
```

### Authentication

```python
# Bearer token via headers
source = RESTSource(
    url="https://api.example.com/data",
    headers={"Authorization": "Bearer your_token"},
)

# API key via headers
source = RESTSource(
    url="https://api.example.com/data",
    headers={"X-API-Key": "your_api_key"},
)

# Basic auth via headers
import base64
creds = base64.b64encode(b"user:pass").decode()
source = RESTSource(
    url="https://api.example.com/data",
    headers={"Authorization": f"Basic {creds}"},
)
```

### Document Path

For nested API responses, use `document_path` to extract documents:

```python
# API returns: {"data": {"results": [...]}}
source = RESTSource(
    url="https://api.example.com/api/v2/products",
    document_path="data.results",
    pagination="page",
)
```

---

## GraphQLSource

`GraphQLSource` fetches documents from a GraphQL API endpoint:

```python
from whoosh_modern.data_sources.graphql import GraphQLSource

source = GraphQLSource(
    url="https://api.example.com/graphql",
    query="""
        query GetProducts($limit: Int!, $offset: Int!) {
            products(limit: $limit, offset: $offset) {
                id
                name
                price
                description
            }
        }
    """,
    pagination="offset",
    page_size=100,
    headers={"Authorization": "Bearer your_token"},
)

schema = source.discover_schema()
for doc in source.iter_documents():
    print(doc["id"], doc["name"])
```

---

## File-Based Data Sources

Whoosh-NG supports indexing from various file formats with optimized readers.

### FastCSVSource

High-performance CSV reader with configurable encoding and delimiter:

```python
from whoosh_modern.data_sources.fast_csv import FastCSVSource

source = FastCSVSource(
    file_path="data/products.csv",
    id_field="id",
    incremental_field="updated_at",
    delimiter=",",
    encoding="utf-8",
)

schema = source.discover_schema()
count = source.document_count()
for doc in source.iter_documents():
    print(doc)
```

### JSONSource

Index from JSON files or JSON Lines (.jsonl) files:

```python
from whoosh_modern.data_sources.json import JSONSource

# JSON array file
source = JSONSource(file_path="data/products.json")

# JSON Lines file (one JSON object per line)
source = JSONSource(
    file_path="data/logs.jsonl",
    format="jsonl",
)

schema = source.discover_schema()
```

### ParquetSource

Index from Parquet files using pyarrow or pandas backend:

```python
from whoosh_modern.data_sources.parquet_ds import ParquetSource

source = ParquetSource(
    file_path="data/large_dataset.parquet",
    engine="pyarrow",  # or "pandas"
    batch_size=1000,
)

schema = source.discover_schema()
```

### PandasSource

Index directly from a pandas DataFrame:

```python
from whoosh_modern.data_sources.pandas_ds import PandasSource
import pandas as pd

df = pd.read_csv("data/products.csv")
source = PandasSource(dataframe=df)

schema = source.discover_schema()
```

### PolarsSource

Index from a Polars DataFrame (faster, lazy evaluation):

```python
from whoosh_modern.data_sources.polars_ds import PolarsSource
import polars as pl

df = pl.read_csv("data/products.csv")
source = PolarsSource(dataframe=df)

schema = source.discover_schema()
```

---

## DataSourceConfig

For programmatic configuration, use `DataSourceConfig` to define data source properties:

```python
from whoosh_modern.data_sources.config import DataSourceConfig

config = DataSourceConfig(
    type="sql",
    connection=conn,
    query="SELECT * FROM products",
    id_field="id",
    incremental_field="updated_at",
)

source = config.create()
schema = source.discover_schema()
```

### Config File Support

Data source configurations can be loaded from dictionaries:

```python
from whoosh_modern.data_sources.config import DataSourceConfig

# From dict
config = DataSourceConfig.from_dict({
    "type": "rest",
    "url": "https://api.example.com/v2/products",
    "pagination": "page",
    "page_size": 50,
})
source = config.create()
```

Supported `type` values: `sql`, `sqlalchemy`, `rest`, `csv`, `json`, `graphql`,
`pydantic`, `pandas`, `polars`, `parquet`, `peewee`, `tortoise`.

### Available Data Sources

| Class | Source Type | Dependencies |
|-------|------------|--------------|
| `SQLSource` | SQLite, PostgreSQL, MySQL | `sqlite3` (stdlib) |
| `SQLAlchemySource` | Any SQLAlchemy-supported DB | `sqlalchemy` |
| `RESTSource` | REST APIs | none (stdlib `urllib`) |
| `GraphQLSource` | GraphQL APIs | none (stdlib `urllib`) |
| `FastCSVSource` | CSV files | none |
| `JSONSource` | JSON/JSONL files | none |
| `ParquetSource` | Parquet files | `pyarrow` or `pandas` |
| `PandasSource` | pandas DataFrames | `pandas` |
| `PolarsSource` | Polars DataFrames | `polars` |
| `PeeweeSource` | Peewee ORM | `peewee` |
| `TortoiseSource` | Tortoise ORM | `tortoise-orm` |
| `PydanticSource` | Pydantic models | `pydantic` |


## DOCUMENT: Facets

# Facet Manager

`FacetManager` manages facet configuration for a Whoosh `Schema`. It auto-discovers facetable fields and supports manual overrides.

## Basic Usage

```python
from whoosh.fields import Schema, TEXT, NUMERIC, BOOLEAN
from whoosh_modern.facets import FacetManager, TermsFacet, RangeFacet

schema = Schema(
    title=TEXT(stored=True),
    category=TEXT(sortable=True),
    price=NUMERIC(),
    active=BOOLEAN(),
)

manager = FacetManager(schema)
```

## Auto-Discovery

FacetManager automatically identifies facetable fields:

| Whoosh Field Type | Facet Type |
|-------------------|------------|
| `TEXT`, `KEYWORD`, `BOOLEAN`, `ID` | `TermsFacet` |
| `NUMERIC` | `RangeFacet` |
| `DATETIME` | `DateRangeFacet` |

```python
# Auto-discovered facets
facets = manager.get_facets()
# {"title": TermsFacet(limit=100), "category": TermsFacet(limit=100),
#  "price": RangeFacet("price", 0, 1000, 100), "active": TermsFacet(limit=100)}
```

> `RangeFacet` and `DateRangeFacet` are re-exports of the core
> `whoosh.sorting.RangeFacet` / `whoosh.sorting.DateRangeFacet`, so the facet objects
> returned by `FacetManager` can be passed directly to `searcher.search(..., groupedby=...)`.

## Manual Override

```python
from whoosh_modern.facets import TermsFacet

manager.set_manual_override("category", {
    "type": "terms",
    "limit": 50,
})
manager.set_manual_override("price", {
    "type": "range",
    "buckets": ["0-10", "10-50", "50-100", "100+"],
})
```

## Inspection

```python
# Is a field facetable?
manager.is_facetable("category")   # True
manager.is_facetable("title")      # False

# Get config for a field
config = manager.get_facet_config("category")
# {"type": "terms", "limit": 50}

# Get all configs
all_configs = manager.get_all_facet_configs()

# Get statistics
stats = manager.get_facet_stats()
# {
#     "total_fields": 4,
#     "auto_discovered_facets": 2,
#     "manual_overrides": 1,
#     "total_facets_configured": 3,
#     "facet_fields": ["category", "price", ...]
# }
```


## DOCUMENT: Fastapi Search

# FastAPI Integration

A complete, runnable FastAPI service exposing Whoosh‑NG search via HTTP.

## 1. Install

```bash
pip install whoosh-ng[api] fastapi uvicorn
```

## 2. Create the index

```python
# setup_index.py
import json
from whoosh import index
from whoosh.fields import Schema, TEXT, ID

schema = Schema(
    id=ID(stored=True, unique=True),
    title=TEXT(stored=True),
    content=TEXT,
)

ix = index.create_in("docs_index", schema)

with ix.writer() as w:
    for doc in json.load(open("documents.json")):
        w.add_document(
            id=doc["id"],
            title=doc["title"],
            content=doc["content"],
        )
    w.commit()
```

## 3. REST API Service

```python
# main.py
from fastapi import FastAPI, Query
from typing import Optional
from whoosh import index
from whoosh.qparser import QueryParser
from whoosh_fastapi import create_app

ix = index.open_dir("docs_index")

# Option A: Use the helper
app = create_app(ix, prefix="/api/v1")

# Option B: Manual endpoints
# app = FastAPI(title="Document Search API", version="1.0.0")
#
# @app.get("/api/v1/health")
# async def health():
#     return {"status": "ok"}
#
# @app.post("/api/v1/search")
# async def search(q: str = Query(...), limit: int = 10):
#     with ix.searcher() as s:
#         parser = QueryParser("content", ix.schema)
#         results = s.search(parser.parse(q), limit=limit)
#         return {"hits": [dict(h) for h in results], "total": len(results)}
#
# @app.get("/api/v1/documents/{doc_id}")
# async def get_doc(doc_id: str):
#     with ix.searcher() as s:
#         from whoosh.query import Term
#         results = s.search(Term("id", doc_id))
#         if results:
#             return dict(results[0])
#         return {"error": "not found"}

if __name__ == "__main__":
    import uvicorn
    uvicorn.run(app, host="0.0.0.0", port=8000)
```

## 4. Run the server

```bash
uvicorn main:app --reload --port 8000
```

## 5. Test the API

```bash
# Health check
curl http://localhost:8000/api/v1/health

# Search
curl -X POST http://localhost:8000/api/v1/search \
  -H "Content-Type: application/json" \
  -d '{"q": "python search"}'

# Get document by ID
curl http://localhost:8000/api/v1/documents/doc1
```

## 6. Bulk Indexing Endpoint

```python
# Add to main.py for dynamic indexing
from fastapi import FastAPI
from whoosh.writing import BufferedWriter

@app.post("/api/v1/index")
async def index_docs(docs: list[dict]):
    with BufferedWriter(ix, period=30, limit=50) as w:
        for doc in docs:
            w.add_document(**doc)
    return {"indexed": len(docs)}
```

## 7. Alternative: WhooshFastAPI Class

For more control, use the `WhooshFastAPI` class directly:

```python
from fastapi import FastAPI
from whoosh import index
from whoosh.fields import Schema, TEXT, ID
from whoosh.qparser import QueryParser
from whoosh_fastapi import WhooshFastAPI

app = FastAPI()

schema = Schema(title=TEXT(stored=True), content=TEXT)
ix = index.create_in("indexdir", schema)

api = WhooshFastAPI(ix)
api.register_search_endpoint("/search", "content")
api.register_index_endpoint("/documents", schema)
```

## Key points

- `create_app()` from `whoosh_fastapi` provides `/health`, `/search`, `/autocomplete`, and `/suggest` endpoints.
- All blocking calls run off the event loop via `run_sync`.
- Use `BufferedWriter` for high-throughput indexing via POST.
- `WhooshFastAPI` class offers per-endpoint registration for custom integrations.

## WebSocket autocomplete

Pass an ``AutocompleteProvider`` to ``create_app`` to enable the WebSocket
autocomplete endpoint. The client sends JSON messages with a ``q`` key and
receives ``{"suggestions": [...]}`` responses over a persistent connection.

```python
from whoosh_modern.autocomplete import EdgeNgramAutocomplete
from whoosh_fastapi import create_app

autocomplete = EdgeNgramAutocomplete(ix)
app = create_app(ix, prefix="/api/v1", autocomplete=autocomplete)
```

Client example using JavaScript:

```javascript
const ws = new WebSocket("ws://localhost:8000/api/v1/autocomplete/ws");
ws.onmessage = (event) => console.log(JSON.parse(event.data));
ws.send(JSON.stringify({ q: "pyth" }));
// {"suggestions": ["python", "pythagorean"]}

// Custom limit
ws.send(JSON.stringify({ q: "pyth", limit: 5 }));
// {"suggestions": ["python", "pythagorean", ...]} // up to 5 suggestions
```

When no autocomplete provider is configured, the endpoint returns an empty
suggestions list instead of raising.


## DOCUMENT: Middleware Pipeline

# Middleware Pipeline

The middleware pipeline wraps operations with cross-cutting concerns: retry, logging, etc.

## Architecture

```python
from whoosh_modern.middleware import Middleware, MiddlewarePipeline, RetryMiddleware, LoggingMiddleware

# Chain middlewares
pipeline = MiddlewarePipeline(
    RetryMiddleware(attempts=3, backoff="exponential"),
    LoggingMiddleware(),
)

# Execute an operation through the chain
result = pipeline.execute(my_operation)
```

## RetryMiddleware

```python
from whoosh_modern.middleware import RetryMiddleware

retry = RetryMiddleware(attempts=3, backoff="exponential")

def flaky_operation():
    # Will retry up to 3 times on exception
    return fetch_data()

wrapped_op = retry.wrap(flaky_operation)
result = wrapped_op()
```

Backoff strategies:
- `"exponential"`: 1s, 2s, 4s, 8s...
- `"linear"`: 1s, 2s, 3s, 4s...

## LoggingMiddleware

```python
from whoosh_modern.middleware import LoggingMiddleware
import logging

logger = logging.getLogger("benchmark")
logging_mw = LoggingMiddleware(logger=logger, level=logging.INFO)

tracked_op = logging_mw.wrap(lambda: fetch_data())
result = tracked_op()
# Logs: "Operation wrapped completed in 0.123s"
# On error: "Operation wrapped failed after 0.123s: <error>"
```

## CacheMiddleware

```python
from whoosh_modern.middleware import CacheMiddleware

cache = CacheMiddleware(maxsize=128)

cached_op = cache.wrap(expensive_query)
result1 = cached_op(args)  # cache miss
result2 = cached_op(args)  # cache hit

print(cache.stats)  # {"hits": 1, "misses": 1, "size": 1}
cache.clear()
```

## Custom Middleware

```python
from whoosh_modern.middleware import Middleware

class TimingMiddleware(Middleware):
    def __init__(self):
        self.timings = []

    def wrap(self, operation):
        def wrapped(*args, **kwargs):
            start = time.time()
            try:
                result = operation(*args, **kwargs)
                self.timings.append(time.time() - start)
                return result
            except Exception:
                raise
        return wrapped
```


## DOCUMENT: Middleware

# Middleware Examples

Practical examples for building and using Whoosh-NG middleware.

## 1. Logging Middleware

```python
from whoosh.middleware.base import Middleware
from whoosh.middleware.context import MiddlewareContext

class LoggingMiddleware(Middleware):
    def before_search(self, context: MiddlewareContext) -> MiddlewareContext:
        print(f"[SEARCH] Query: {context.query}")
        return context

    def after_search(self, context: MiddlewareContext) -> MiddlewareContext:
        if context.results is not None:
            print(f"[RESULTS] Found {len(context.results)} hits")
        return context
```

## 2. Metrics Middleware

```python
from whoosh.middleware.base import Middleware
from whoosh.middleware.context import MiddlewareContext

class MetricsMiddleware(Middleware):
    def __init__(self) -> None:
        self._metrics = {}

    def after_index(self, context: MiddlewareContext) -> MiddlewareContext:
        self._metrics["documents_indexed"] = self._metrics.get("documents_indexed", 0) + 1
        return context

    def after_search(self, context: MiddlewareContext) -> MiddlewareContext:
        self._metrics["searches_executed"] = self._metrics.get("searches_executed", 0) + 1
        return context

    def get_metrics(self) -> dict:
        return dict(self._metrics)
```

## 3. Cache Middleware

```python
from whoosh.middleware.base import Middleware
from whoosh.middleware.context import MiddlewareContext

class SearchCacheMiddleware(Middleware):
    def __init__(self) -> None:
        self._cache = {}

    def before_search(self, context: MiddlewareContext) -> MiddlewareContext:
        if context.query and str(context.query) in self._cache:
            context.metadata["_cached_result"] = self._cache[str(context.query)]
        return context

    def after_search(self, context: MiddlewareContext) -> MiddlewareContext:
        if context.query and context.results is not None:
            self._cache[str(context.query)] = context.results
        return context
```

## 4. Applying Middleware to an Index

```python
from whoosh.middleware.chain import MiddlewareChain
from whoosh.middleware.integration import apply_middleware_to_searcher

# Create middleware chain
chain = MiddlewareChain([
    LoggingMiddleware(),
    MetricsMiddleware(),
])

# Apply to a searcher
with ix.searcher() as base_searcher:
    searcher = apply_middleware_to_searcher(base_searcher, chain.middlewares)
    results = searcher.search(query)
```

## 5. Middleware Lifecycle

```python
class LifecycleMiddleware(Middleware):
    def startup(self, context):
        print("Middleware initialized")

    def shutdown(self, context):
        print("Middleware shutting down")

    def on_error(self, context, exc):
        print(f"Error: {exc}")
        raise exc
```

## Key Hooks

| Hook | Phase | Context |
|------|-------|---------|
| `startup` | Init | Called once on middleware init |
| `shutdown` | Cleanup | Called once on teardown |
| `before_index` | Indexing | Before document added |
| `after_index` | Indexing | After document added |
| `before_delete` | Deletion | Before document deleted |
| `after_delete` | Deletion | After document deleted |
| `before_search` | Search | Before query executed |
| `after_search` | Search | After results returned |
| `on_error` | Error | When exception occurs |
| `on_commit` | Commit | After writer.commit() |


## DOCUMENT: Movie Search

# Movie Search Application

A complete, runnable example showing how to build a small **movie search** application with Whoosh‑NG: schema design, indexing from a JSON dataset, faceted search, highlighting, and filtering.

## 1. Schema

```python
from whoosh.fields import Schema, TEXT, ID, KEYWORD, NUMERIC

schema = Schema(
    id=ID(stored=True, unique=True),
    title=TEXT(stored=True),
    director=TEXT(stored=True),
    genre=KEYWORD(stored=True, commas=True, scorable=True),
    year=NUMERIC(int, stored=True),
    synopsis=TEXT,
)
```

## 2. Index the dataset

```python
import json
import shutil
from whoosh import index
from whoosh.fields import Schema, TEXT, ID, KEYWORD, NUMERIC

schema = Schema(
    id=ID(stored=True, unique=True),
    title=TEXT(stored=True),
    director=TEXT(stored=True),
    genre=KEYWORD(stored=True, commas=True, scorable=True),
    year=NUMERIC(int, stored=True),
    synopsis=TEXT,
)

# Clean and create new index
shutil.rmtree("movies", ignore_errors=True)
ix = index.create_in("movies", schema)

movies = json.load(open("movies.json"))  # list of dicts

with ix.writer() as w:
    for m in movies:
        w.add_document(
            id=str(m["id"]),
            title=m["title"],
            director=m["director"],
            genre=",".join(m["genres"]),
            year=m["year"],
            synopsis=m["synopsis"],
        )
    w.commit()
```

Example `movies.json`:

```json
[
  {
    "id": 1,
    "title": "Blade Runner",
    "director": "Ridley Scott",
    "genres": ["sci-fi", "thriller"],
    "year": 1982,
    "synopsis": "A replicant hunter questions humanity in a rain-soaked future."
  },
  {
    "id": 2,
    "title": "Inception",
    "director": "Christopher Nolan",
    "genres": ["sci-fi", "action"],
    "year": 2010,
    "synopsis": "A thief who steals corporate secrets through dream-sharing technology."
  }
]
```

## 3. Search with facets and highlighting

```python
from whoosh import index
from whoosh.qparser import MultifieldParser
from whoosh.sorting import FieldFacet

ix = index.open_dir("movies")

qp = MultifieldParser(["title", "synopsis", "director"], ix.schema)

with ix.searcher() as s:
    q = qp.parse("future")

    # Search with sorting by year descending, grouped by genre
    results = s.search(
        q,
        sortedby=FieldFacet("year", reverse=True),
        groupedby=FieldFacet("genre", allow_overlap=True),
        limit=20,
    )

    for hit in results:
        print(hit["title"], hit["year"], "|", round(hit.score, 2))
        print("  ", hit.highlights("synopsis"))

    # Show genre facets
    print("\nGenres:", results.groups("genre"))
```

## 4. Filtering

Find sci-fi movies after 1990:

```python
from whoosh import index
from whoosh.qparser import QueryParser
from whoosh.query import Term, And, NumericRange

ix = index.open_dir("movies")
qp = QueryParser("synopsis", ix.schema)

with ix.searcher() as s:
    user_q = qp.parse("dream")
    filters = And([
        Term("genre", "sci-fi"),
        NumericRange("year", 1990, None),
    ])
    results = s.search(user_q, filter=filters)
    for hit in results:
        print(hit["title"], hit["year"])
```

## 5. Key takeaways

- `KEYWORD(commas=True)` stores multi-value fields that can be faceted.
- `MultifieldParser` searches multiple fields with optional boosts.
- `FieldFacet` enables faceted grouping and sorting.
- `hit.highlights()` returns highlighted snippets ready for display.


## DOCUMENT: Plugin Dev

# Plugin Development

Complete guide to building, registering, and testing custom Whoosh-NG plugins.

## 1. Plugin Base Class

All plugins inherit from `whoosh.plugins.base.Plugin`:

```python
from whoosh.plugins.base import Plugin

class MyPlugin(Plugin):
    name = "my_plugin"
    version = "1.0.0"
    depends_on = []  # Other plugins this requires
    conflicts_with = []  # Plugins that conflict
    priority = 0  # Load order (higher = later)
    middleware = []  # List of middleware class names

    def register(self, manager):
        """Called when plugin is loaded. Register your handlers here."""
        manager.register("my_handler", MyHandler())

    def register_hooks(self):
        """Register hooks using hookimpl decorator."""
        from whoosh.hooks import hookimpl, register_hook

        @hookimpl
        def on_search(request, response):
            # Hook logic here
            pass

        register_hook("on_search", hookimpl(on_search))
```

## 2. Registering a Plugin

### Manual Registration

```python
from whoosh.plugins.manager import PluginManager

plugin = MyPlugin()
PluginManager.register(plugin)
```

### Auto-Discovery via Entry Points

In `pyproject.toml`:

```toml
[project]
name = "whoosh-ng-my-plugin"

[project.entry-points."whoosh_ng.plugins"]
my_plugin = "my_package.plugin:MyPlugin"
```

Auto-load all registered plugins:

```python
from whoosh.plugins.manager import PluginManager

PluginManager.load_plugins()  # Uses 'whoosh.plugins' group by default
```

## 3. Provider Plugin Example

A provider plugin registers a new implementation for a registry:

```python
from whoosh.plugins.base import Plugin
from whoosh.registry import VectorRegistry

class MyVectorProvider:
    def search(self, query_vector, k=10):
        # Your vector similarity logic
        return [{"doc_id": "1", "score": 0.95}]

class MyVectorPlugin(Plugin):
    name = "my_vector"
    version = "1.0.0"

    def register(self, manager):
        provider = MyVectorProvider()
        VectorRegistry.register("my_vector", provider, self.name)
```

## 4. Creating a Custom Field Type

```python
from whoosh.fields import FieldType, TEXT
from whoosh.formats import Postings

class TagField(FieldType):
    scorable = True
    stored = True
    indexed = True
    format = Postings()

    def __init__(self, stored=True, scorable=True):
        super().__init__(format=Postings(), analyzer=None,
                        scorable=scorable, stored=stored)
```

## 5. Testing Your Plugin

```python
import pytest
from whoosh.plugins.manager import PluginManager
from whoosh.registry.base import Registry

class TestMyPlugin:
    def test_register(self):
        plugin = MyPlugin()
        manager = PluginManager()
        plugin.register(manager)
        assert "my_handler" in manager._plugins

    def test_entry_point(self):
        """Test that entry point loading works."""
        manager = PluginManager()
        manager.register(MyPlugin())
        assert "my_plugin" in manager.list_enabled()

    def test_conflict_detection(self):
        plugin1 = MyPlugin()
        plugin2 = ConflictingPlugin()
        manager = PluginManager()
        manager.register(plugin1)
        assert manager.detect_conflicts("my_plugin", "conflicting_plugin")
```

## 6. Async Plugin Methods

Plugins support async methods:

```python
from whoosh.plugins.base import Plugin
from typing import Awaitable

class AsyncPlugin(Plugin):
    name = "async_plugin"
    version = "1.0.0"

    async def register(self, manager):
        # Async initialization
        await some_async_setup()

    def register_hooks(self):
        from whoosh.hooks import hookimpl

        @hookimpl
        async def on_search(request, response):
            # Async hook
            await log_search_async(request)
```

## 7. Plugin Manager API

```python
from whoosh.plugins.manager import PluginManager

manager = PluginManager()

# Register a plugin instance
manager.register(MyPlugin())

# Enable/disable
manager.enable("my_plugin")
manager.disable("my_plugin")

# Check status
manager.list_plugins()   # All registered
manager.list_enabled()   # Enabled plugins

# Get plugin
plugin = manager.get("my_plugin")

# Version checking
manager.validate_version("my_plugin", "1.0.0")
```

## 8. Built-in Plugins

Whoosh-NG includes several built-in plugins:

- `whoosh_modern.vector` - Vector similarity search (NumPy provider)
- `whoosh_modern.autocomplete` - Inverted index autocomplete
- `whoosh_fastapi` - FastAPI REST endpoints

Load them:

```python
from whoosh.plugins.manager import PluginManager
from whoosh_modern.vector.plugin import VectorPlugin
from whoosh_modern.autocomplete.plugin import AutocompletePlugin

PluginManager.load_plugins()  # Auto-loads entry points
# Or manually:
manager = PluginManager()
manager.register(VectorPlugin())
manager.register(AutocompletePlugin())
```


## DOCUMENT: Schema Discovery

# Schema Discovery

Schema discovery infers Whoosh `Schema` from data source results. It operates on **actual result metadata and sample documents**, not SQL syntax.

## How It Works

### From Result Sets

```python
from whoosh_modern.schema_discovery import SchemaDiscovery
import sqlite3

conn = sqlite3.connect("benchmark/benchmark_data.db")
cursor = conn.cursor()
cursor.execute("PRAGMA table_info(reuters_articles)")
columns = [(row[1], row[2]) for row in cursor.fetchall()]
# [("id", "INTEGER"), ("article_date", "TEXT"), ("headline", "TEXT"), ...]

schema = SchemaDiscovery.from_result_set(columns)
```

### From Sample Documents

```python
from whoosh_modern.data_sources.sql import SQLSource

source = SQLSource(
    connection=conn,
    query="SELECT * FROM reuters_articles LIMIT 10",
)

# Get a few documents
docs = list(source.iter_documents())[:10]

# Infer schema from document values
schema = SchemaDiscovery.from_sample(docs)
```

### Detect ID Field

```python
id_field = SchemaDiscovery.detect_id_field(dict(schema))
# Returns "id" if an ID field is found, otherwise None
```

### Optimized Schema Discovery

```python
# Infer schema with optimization rules applied
# - drops non-searchable TEXT fields
# - converts TEXT fields ending in "id" to ID
# - converts boolean-like TEXT fields to BOOLEAN
schema = SchemaDiscovery.from_sample_optimized(
    docs, searchable_text=["title", "content"]
)
```

## SQL Type Mapping

| SQL Type | Whoosh Field |
|----------|-------------|
| `VARCHAR`, `TEXT`, `CHAR`, `STRING_AGG` | `TEXT` |
| `INTEGER`, `BIGINT`, `SMALLINT`, `COUNT`, `SUM`, `AVG`, `MIN`, `MAX` | `NUMERIC` |
| `FLOAT`, `DOUBLE`, `DECIMAL` | `NUMERIC` |
| `BOOLEAN` | `BOOLEAN` |
| `DATE`, `TIMESTAMP` | `DATETIME` |
| `UUID` | `ID` |
| `JSON`, `ENUM` | `KEYWORD` |

## Duplicate Column Detection

`from_result_set` raises `SchemaDiscoveryError` on duplicate column names.

```python
columns = [
    ("id", "INTEGER"),
    ("headline", "TEXT"),
    ("id", "INTEGER"),  # Duplicate!
]

try:
    schema = SchemaDiscovery.from_result_set(columns)
except SchemaDiscoveryError as e:
    print(f"Duplicate column: {e.field}")
```

Use explicit SQL aliases to avoid duplicates:

```sql
SELECT
    p.id AS product_id,
    c.id AS category_id
FROM products p
JOIN categories c ON p.category_id = c.id
```


## DOCUMENT: Search Models

# Search Models

Examples for auto-mapping Python models to Whoosh schemas.

## Dataclass

```python
from dataclasses import dataclass
from whoosh.fields import Schema, TEXT, NUMERIC
from whoosh_modern.models import register_dataclass_model
import tempfile
import shutil

@dataclass
class Book:
    title: str
    year: int
    tags: list[str] | None = None

idx = register_dataclass_model(Book)
print(idx.schema)
```

## Pydantic

```python
from pydantic import BaseModel
from whoosh_modern.models import register_pydantic_model

class BookModel(BaseModel):
    title: str
    year: int
    tags: list[str] | None = None

idx = register_pydantic_model(BookModel)
schema = idx.schema
```

## SQLAlchemy

```python
from sqlalchemy import Column, Integer, String
from whoosh_modern.models import register_sqlalchemy_model

class BookSQL:
    __tablename__ = "book"
    title = Column(String, info={"search": {"fulltext": True, "stored": True}})
    year = Column(Integer, info={"search": {"sortable": True}})

idx = register_sqlalchemy_model(BookSQL)
schema = idx.schema
```

## SQLModel

```python
from sqlmodel import SQLModel, Field
from whoosh_modern.models import register_sqlmodel_model

class Book(SQLModel, table=True):
    id: int = Field(primary_key=True)
    title: str = Field(sa_column_kwargs={"info": {"search": {"fulltext": True}}})
    year: int

idx = register_sqlmodel_model(Book)
schema = idx.schema
```

## msgspec

```python
import msgspec
from whoosh_modern.models import register_msgspec_model

class Book(msgspec.Struct):
    title: str = msgspec.field(metadata={"search": {"fulltext": True}})
    year: int

idx = register_msgspec_model(Book)
schema = idx.schema
```

## Indexing documents

```python
from whoosh import index

tmp = tempfile.mkdtemp()
ix = index.create_in(tmp, schema)

with ix.writer() as w:
    book = Book(title="Whoosh Guide", year=2024, tags=["python", "search"])
    doc = idx.to_whoosh_document(book)
    w.add_document(**doc)
    w.commit()
```

## Auto-indexing with AutoIndexer

```python
from whoosh_modern.models import AutoIndexer

auto = AutoIndexer(ix, on_error="raise")
auto.register(Book)

# Index a single instance
book = Book(title="New Book", year=2024, tags=["python"])
auto.index(book)

# Remove by ID
auto.remove(book)

# Async versions
await auto.index_async(book)
await auto.remove_async(book)
```

For SQLAlchemy models, `AutoIndexer` automatically hooks into `after_insert`, `after_update`, and `after_delete` events.

## Cleanup

```python
shutil.rmtree(tmp)
```


## DOCUMENT: Search View

# SearchView

`SearchView` integrates a `DataSource` with Whoosh indexing. It discovers
schema, validates the source, builds the index, and supports incremental
refresh and schema evolution.

## Basic Usage

```python
from whoosh_modern.views import SearchView
from whoosh_modern.data_sources.sql import SQLSource
import sqlite3

conn = sqlite3.connect("data/articles.db")
source = SQLSource(
    connection=conn,
    query="SELECT * FROM articles",
    incremental_field="updated_at",
    id_field="id",
)

view = SearchView(
    name="articles",
    source=source,
)

# Build index (discovers schema, validates, populates)
ix = view.build("indexdir")
```

## Full Reindex vs Incremental Refresh

```python
# Full reindex (clears and rebuilds the entire index)
count = view.reindex()

# Incremental refresh (only changed documents since last sync)
count = view.refresh()
```

## Validation

```python
# Run validation before building
results = view.validate()
for result in results:
    print(f"Level {result.level}: {'PASS' if result.passed else 'FAIL'}")
    for error in result.errors:
        print(f"  ERROR: {error}")
    for warning in result.warnings:
        print(f"  WARNING: {warning}")
```

Validation levels:
1. **Structural** — DataSource availability, schema detection
2. **Search** — Indexable fields, term vectors, searchable analyzers
3. **Performance** — Performance warnings (e.g., TEXT fields on large datasets)
4. **Runtime** — Sample iteration, type conformance

## Field Overrides

Customize field types after schema discovery:

```python
from whoosh.fields import TEXT, NUMERIC, DATETIME

view = SearchView(
    name="custom",
    source=source,
    fields={
        "title": TEXT(stored=True, phrase=False),
        "price": NUMERIC(int, sortable=True),
        "published": DATETIME(sortable=True),
    },
)
```

## Facets

Configure facet settings:

```python
view = SearchView(
    name="faceted",
    source=source,
    facets={
        "category": {"type": "terms", "limit": 50},
        "price": {"type": "range", "buckets": ["0-100", "100-500", "500+"]},
    },
)
```

## Middleware

Attach middleware to the search pipeline:

```python
from whoosh_modern.views import SearchView
from whoosh_modern.middleware import LoggingMiddleware, RetryMiddleware

view = SearchView(
    name="with_middleware",
    source=source,
    middleware=[
        RetryMiddleware(attempts=3, backoff="exponential"),
        LoggingMiddleware(),
    ],
)
```

## Strict Mode

```python
view = SearchView(
    name="strict",
    source=source,
    strict=True,  # Raise ValidationError on any validation failure
)
```

## Schema Evolution

Add new fields to an existing index without a full reindex:

```python
view = SearchView(name="articles", source=source)
view.build("indexdir")

# Add a new field
view.evolve_schema({
    "new_field": TEXT(stored=True),
})
```

### Schema Version Checking

```python
view = SearchView(name="articles", source=source, schema_version="2.1")
view.build("indexdir")

# Check if the stored schema version matches
if not view.check_schema_version():
    print("Schema version mismatch, consider reindexing")
```


## DOCUMENT: Search

# Search Examples

Real, runnable search examples with Whoosh‑NG. Each section is a self-contained
script you can copy into a `.py` file and run.

> **Real-world scenario**: You built a book‑catalogue index (see
> `docs/_en/examples/basic-indexing.md`). Below are the search patterns
> you'll need for a production‑ready book search page.

## Prerequisites

The examples assume an index exists at `book_index/` with this schema:

```python
from whoosh import index
from whoosh.fields import Schema, TEXT, ID, NUMERIC, DATETIME

schema = Schema(
    isbn=ID(stored=True, unique=True),
    title=TEXT(stored=True),
    author=TEXT(stored=True),
    content=TEXT,
    genre=KEYWORD(stored=True, commas=True),
    published_year=NUMERIC(int, stored=True, sortable=True),
    rating=NUMERIC(float, stored=True, sortable=True),
)
```

## 1. Basic Search — "Find books about Python"

```python
from whoosh import index
from whoosh.qparser import QueryParser

ix = index.open_dir("book_index")

with ix.searcher() as s:
    qp = QueryParser("content", ix.schema)
    q = qp.parse("python")

    results = s.search(q, limit=10)
    for hit in results:
        print(f"{hit['title']} by {hit['author']} (ISBN: {hit['isbn']}) — score={hit.score:.2f}")
```

## 2. Multi-field Search with Boosts

Search across `title`, `author`, and `content` simultaneously. Title matches
are boosted 3× so they rank higher:

```python
from whoosh.qparser import MultifieldParser

ix = index.open_dir("book_index")
qp = MultifieldParser(
    ["title", "author", "content"],
    ix.schema,
    fieldboosts={"title": 3.0, "author": 2.0, "content": 1.0},
)

q = qp.parse("clean code")

with ix.searcher() as s:
    results = s.search(q, limit=10)
    for hit in results:
        print(f"{hit['title']} — {hit['author']}")
```

## 3. Pagination — "Page 3 of search results"

```python
ix = index.open_dir("book_index")
qp = QueryParser("content", ix.schema)
q = qp.parse("machine learning")

with ix.searcher() as s:
    page = s.search_page(q, 3, pagelen=15)  # Page 3, 15 results per page

    print(f"Page {page.number} / {page.pagecount}  ({page.total} results total)")
    for hit in page:
        print(f"  {hit['title']}")
```

## 4. Sort and Filter — "High-rated sci-fi books after 2010"

```python
from whoosh.query import Term, And, NumericRange
from whoosh.sorting import FieldFacet, ScoreFacet

ix = index.open_dir("book_index")
qp = QueryParser("content", ix.schema)
q = qp.parse("space")

with ix.searcher() as s:
    # Filter: genre must be "sci-fi" AND year >= 2010
    filters = And([
        Term("genre", "sci-fi"),
        NumericRange("published_year", 2010, None),
    ])

    results = s.search(
        q,
        filter=filters,
        sortedby=FieldFacet("rating", reverse=True),  # highest-rated first
        limit=20,
    )
    for hit in results:
        print(f"{hit['title']} ({hit['published_year']}) — rating: {hit['rating']}")
```

## 5. Highlighting — "Show users where their query matched"

```python
ix = index.open_dir("book_index")
qp = QueryParser("content", ix.schema)
q = qp.parse("neural networks")

with ix.searcher() as s:
    results = s.search(q, limit=5)

    for hit in results:
        snippet = hit.highlights("content", top=2)  # show 2 best fragments
        print(f"{hit['title']}:")
        print(f"  {snippet}")
        print()
```

## 6. Date / Numeric Range Search — "Books published in 2023"

```python
from whoosh.query import NumericRange

ix = index.open_dir("book_index")

with ix.searcher() as s:
    q = NumericRange("published_year", 2023, 2023)
    results = s.search(q)
    print(f"{results.total} books published in 2023")
```

## 7. Prefix Search — "All books starting with 'Deep'"

```python
from whoosh.query import Prefix

ix = index.open_dir("book_index")

with ix.searcher() as s:
    q = Prefix("title", "Deep")  # titles starting with "Deep"
    results = s.search(q)
    for hit in results:
        print(hit["title"])
```

## 8. Faceted Search — "Group results by genre"

```python
from whoosh.sorting import FieldFacet

ix = index.open_dir("book_index")
qp = QueryParser("content", ix.schema)
q = qp.parse("programming")

with ix.searcher() as s:
    results = s.search(q, groupedby=FieldFacet("genre"))

    # Show top genres alongside results
    for genre, group in results.groups("genre").items():
        print(f"{genre}: {len(group)} hits")
```

## Key points

- `QueryParser` parses a string into a `Query` object.
- `MultifieldParser` searches multiple fields with optional per-field boosts.
- `search_page()` handles pagination automatically.
- `filter` restricts results without affecting relevance scores.
- `sortedby` sorts by field value or relevance score.
- `hit.highlights()` returns highlighted snippets ready for display.
- `groupedby` enables faceted result grouping.


## DOCUMENT: Validation

# Validation Framework

The validation framework runs checks against a data source before indexing. It provides 4 distinct validation levels with different failure modes.

## Validation Levels

| Level | Method | Purpose |
|-------|--------|---------|
| **Level 1** | `validate_structural(source)` | DataSource availability, schema detection |
| **Level 2** | `validate_search(schema)` | Indexable fields, analyzer compatibility |
| **Level 3** | `validate_performance(schema, source)` | Performance warnings (TEXT fields, etc.) |
| **Level 4** | `validate_runtime(source, sample_size)` | Sample iteration, type validation |

## Basic Usage

```python
from whoosh_modern.validation import ValidationFramework, ValidationResult
from whoosh_modern.data_sources.sql import SQLSource
import sqlite3

conn = sqlite3.connect("benchmark/benchmark_data.db")
source = SQLSource(connection=conn, query="SELECT * FROM reuters_articles")

validator = ValidationFramework()

# Run all 4 validation levels
results: list[ValidationResult] = validator.validate(source)

for result in results:
    level_name = f"Level {result.level}"
    status = "PASS" if result.passed else "FAIL"
    print(f"{level_name}: {status}")
    for error in result.errors:
        print(f"  ERROR: {error}")
    for warning in result.warnings:
        print(f"  WARN: {warning}")
```

## Individual Level Validation

```python
# Level 1: Structural
errors = validator.validate_structural(source)

# Level 2: Search
from whoosh.fields import Schema
schema = source.discover_schema()
errors = validator.validate_search(schema)

# Level 3: Performance
warnings = validator.validate_performance(schema, source)

# Level 4: Runtime
errors = validator.validate_runtime(source, sample_size=100)
```

## Validation Results

```python
@dataclass
class ValidationResult:
    level: int
    passed: bool
    warnings: list[str]
    errors: list[str]
```


## DOCUMENT: Vector Search

# Vector Search with Whoosh‑NG

This example shows how to enable **semantic/vector search** using the optional `vector` extra. We index document embeddings and perform k-nearest neighbour (k-NN) search.

## 1. Install Optional Dependencies

```bash
pip install "whoosh-ng[vector]" numpy
```

## 2. Schema with a Vector Field

```python
from whoosh.fields import Schema, TEXT, ID, VECTOR
from whoosh.vector import VectorField

schema = Schema(
    doc_id=ID(stored=True, unique=True),
    title=TEXT(stored=True),
    content=TEXT,
    embedding=VECTOR(stored=True, dim=128),  # 128-dimensional embedding
)
```

## 3. Indexing Vectors

```python
import numpy as np
from whoosh import index

shutil.rmtree("vector_index", ignore_errors=True)
ix = index.create_in("vector_index", schema)

# Simulate embeddings (in practice, use a model like SentenceTransformer)
documents = [
    {"doc_id": "doc1", "title": "Python Basics", "content": "Learn Python programming fundamentals."},
    {"doc_id": "doc2", "title": "Advanced Python", "content": "Deep dive into Python decorators and metaclasses."},
    {"doc_id": "doc3", "title": "Data Science", "content": "Pandas and NumPy for data analysis."},
]

# Generate random embeddings for demo
np.random.seed(42)
embeddings = {d["doc_id"]: np.random.rand(128).astype(np.float32) for d in documents}

with ix.writer() as w:
    for doc in documents:
        w.add_document(
            doc_id=doc["doc_id"],
            title=doc["title"],
            content=doc["content"],
            embedding=embeddings[doc["doc_id"]].tobytes(),
        )
    w.commit()
```

## 4. Vector Search with NumpyProvider

```python
from whoosh_modern.vector import VectorField
from whoosh_modern.vector.numpy_provider import NumpyProvider
from whoosh_modern.vector.plugin import VectorPlugin
from whoosh.plugins.manager import PluginManager

# Register the vector plugin
VectorPlugin().register(PluginManager())

# Create provider and add vectors
provider = NumpyProvider()
for doc_id, vec in embeddings.items():
    provider.add([(doc_id, vec.tolist())])

# Search: find 2 most similar docs to a query vector
query_vec = embeddings["doc1"]  # use doc1's embedding as query
hits = provider.search(query_vec, k=2)

for hit in hits:
    print(f"doc_id={hit.doc_id}, score={hit.score:.3f}")
```

## 5. Using VectorField for Serialization

```python
from whoosh.vector import VectorField

vf = VectorField(dimension=128, name="embedding")

# Convert list to bytes for storage
values = [0.1, 0.2, 0.3, 0.4] + [0.0] * 124  # 128 values
raw = vf.vector_to_bytes(values)

# Restore from bytes
restored = vf.bytes_to_vector(raw)
print(restored == tuple(values))  # True
```

## 6. Key Takeaways

- Install with `pip install whoosh-ng[vector]` to get `whoosh_modern.vector`.
- `VECTOR` field stores raw bytes; use `VectorField` to convert to/from Python lists.
- `NumpyProvider` implements cosine similarity via dot product.
- Register the plugin via `VectorPlugin().register(manager)` or use `PluginManager.load_plugins()`.
- Use `filter_ids` in `provider.search()` to restrict to a subset of documents.


## DOCUMENT: Auto Indexing

# Auto-Indexing

Whoosh-NG provides utilities for automatic schema discovery and data-source driven indexing.

## Schema Discovery

The `SchemaDiscovery` utility inspects a data source and auto-generates a Whoosh schema:

```python
from whoosh_modern.discovery import SchemaDiscovery

discovery = SchemaDiscovery(source=data_source)
schema = discovery.discover()
```

See [SearchView](/examples/search-view) and [Data Sources](/examples/data-sources) for usage examples.


## DOCUMENT: Autocomplete Providers

# Autocomplete Providers

Module: `whoosh_modern.autocomplete`
Version: 2.0.0

The autocomplete module provides multiple provider strategies for query suggestion and type-ahead search. All providers implement a common interface so you can swap strategies at runtime. Providers are registered via the `AutocompleteRegistry` and loaded through entry points.

## Module Overview

```text
whoosh_modern.autocomplete
    ├── provider.py   # AutocompleteHit, AutocompleteProvider (Protocol)
    ├── ngram.py      # NGramProvider (character n-gram based)
    ├── edge_ngram.py # InvertedIndexAutocomplete (inverted index prefix matching)
    ├── fuzzy.py      # FuzzySuggestProvider (approximate matching via rapidfuzz)
    ├── factory.py    # create_autocomplete() factory
    └── plugin.py     # AutocompletePlugin (entry-point plugin)
```

## AutocompleteProvider (Base Class)

Located in `whoosh_modern.autocomplete.provider`:

```python
from whoosh_modern.autocomplete.provider import AutocompleteProvider, AutocompleteHit

class MyProvider(AutocompleteProvider):
    def add(self, phrases: Iterable[str]) -> None:
        """Add phrases to the provider's index."""
        ...

    def search(self, prefix: str, limit: int = 10) -> list[AutocompleteHit]:
        """Return autocomplete suggestions for the given prefix."""
        ...
```

### AutocompleteHit

A simple result object returned by providers:

```python
class AutocompleteHit:
    def __init__(self, text: str, score: float) -> None:
        self.text = text    # The matched phrase
        self.score = score  # Relevance score (higher = better)
```

## Built-in Providers

### InvertedIndexAutocomplete

Located in `whoosh_modern.autocomplete.edge_ngram`. Uses simple prefix matching against an in-memory list:

```python
from whoosh_modern.autocomplete.edge_ngram import InvertedIndexAutocomplete

provider = InvertedIndexAutocomplete()
provider.add(["python", "pyramid", "pytorch", "java", "javascript"])

hits = provider.search("py", limit=5)
for hit in hits:
    print(f"{hit.text} (score: {hit.score})")
# Output:
# python (score: 0.45)
# pyramid (score: 0.43)
# pytorch (score: 0.43)
```

**Scoring**: Exact prefix matches get a 1.5x bonus; base score is `1.0 / (len(phrase) + 1)`.

### NGramProvider

Located in `whoosh_modern.autocomplete.ngram`. Builds a character n-gram index for fuzzy substring matching:

```python
from whoosh_modern.autocomplete.ngram import NGramProvider

provider = NGramProvider(n=3)
provider.add(["python programming", "java development", "rust language"])

hits = provider.search("pyt", limit=5)
for hit in hits:
    print(f"{hit.text} (score: {hit.score})")
```

**Parameters:**

| Parameter | Type | Default | Description                          |
|-----------|------|---------|--------------------------------------|
| `n`       | `int` | `3`     | Size of character n-grams            |

**How it works**: N-grams are extracted from each phrase (lowercased). During search, n-grams from the prefix are matched against the index. Phrases with more matching n-gram occurrences receive higher scores.

### FuzzySuggestProvider

Located in `whoosh_modern.autocomplete.fuzzy`. Uses `rapidfuzz` for approximate string matching (typos, partial matches):

```python
from whoosh_modern.autocomplete.fuzzy import FuzzySuggestProvider

# Requires: pip install whoosh-ng[fuzzy]
provider = FuzzySuggestProvider(max_distance=2, score_cutoff=50.0)
provider.add(["python", "pyramid", "pytorch", "java", "javascript"])

hits = provider.search("pythn", limit=5)  # Typo in "python"
for hit in hits:
    print(f"{hit.text} (score: {hit.score})")
# Output: python (score: 0.95), ...
```

**Parameters:**

| Parameter       | Type  | Default  | Description                              |
|-----------------|-------|----------|------------------------------------------|
| `max_distance`  | `int` | `2`      | Maximum edit distance (unused by rapidfuzz directly, reserved for future use) |
| `score_cutoff`  | `float` | `50.0` | Minimum similarity score (0-100 scale)   |

**Note**: Requires `rapidfuzz` (`pip install whoosh-ng[fuzzy]`). Falls back to `ImportError` if not installed.

## Factory Function

Located in `whoosh_modern.autocomplete.factory`:

```python
from whoosh_modern.autocomplete import create_autocomplete

# Create any provider by name
provider = create_autocomplete("inverted")   # InvertedIndexAutocomplete
provider = create_autocomplete("ngram", n=3) # NGramProvider with custom n
provider = create_autocomplete("fuzzy", max_distance=2, score_cutoff=60.0)
```

**Available providers:**

| Name        | Class                    | Optional Dependency |
|-------------|--------------------------|---------------------|
| `"inverted"`| `InvertedIndexAutocomplete` | None              |
| `"ngram"`   | `NGramProvider`          | None               |
| `"fuzzy"`   | `FuzzySuggestProvider`   | `rapidfuzz`        |

## Registering with the AutocompleteRegistry

Providers are registered into `whoosh.registry.AutocompleteRegistry` (a `Registry` instance):

```python
from whoosh.registry import AutocompleteRegistry
from whoosh_modern.autocomplete import create_autocomplete

# Register a provider
provider = create_autocomplete("ngram", n=3)
AutocompleteRegistry.register("ngram-suggester", provider, owner="my_app")

# Retrieve it later
suggester = AutocompleteRegistry.get("ngram-suggester")

# List all registered providers
print(AutocompleteRegistry.list_keys())
```

## AutocompletePlugin (Entry Point)

Located in `whoosh_modern.autocomplete.plugin`, this is the built-in plugin registered via the `whoosh_ng.plugins` entry-point group:

```python
from whoosh_modern.autocomplete.plugin import AutocompletePlugin

# Automatically loaded by PluginManager.load_plugins()
# Registers "inverted" provider in AutocompleteRegistry
```

### Entry Point Declaration

In `pyproject.toml`:

```toml
[project.entry-points."whoosh_ng.plugins"]
whoosh_autocomplete = "whoosh_modern.autocomplete.plugin:AutocompletePlugin"
```

### Plugin Details

```python
class AutocompletePlugin(Plugin):
    name = "whoosh_autocomplete"
    version = "3.0.0"

    def register(self, manager):
        # Registers InvertedIndexAutocomplete as "inverted"
        AutocompleteRegistry.register(
            "inverted", create_autocomplete("inverted"), self.name
        )

    def register_hooks(self):
        # Registers an on_search hook (currently a no-op)
        from whoosh.hooks import hookimpl, register_hook
        register_hook("on_search", hookimpl(on_search))
```

## Usage Examples

### Basic Usage

```python
from whoosh_modern.autocomplete import create_autocomplete

# Create and populate a provider
provider = create_autocomplete("inverted")
provider.add([
    "python programming",
    "python tutorial",
    "java tutorial",
    "javascript framework",
])

# Search for suggestions
hits = provider.search("py", limit=3)
for hit in hits:
    print(f"{hit.text}: {hit.score:.3f}")
```

### Using Fuzzy Matching with Typo Tolerance

```python
from whoosh_modern.autocomplete import create_autocomplete

provider = create_autocomplete("fuzzy", score_cutoff=70.0)
provider.add(["python", "pytorch", "tensorflow", "keras"])

# Even with a typo, relevant suggestions are returned
hits = provider.search("pyton", limit=5)
for hit in hits:
    print(hit.text, hit.score)
```

### Using N-Gram Matching for Partial Words

```python
from whoosh_modern.autocomplete import create_autocomplete

# Use 3-grams for better substring matching
provider = create_autocomplete("ngram", n=3)
provider.add(["machine learning", "deep learning", "neural networks"])

# Finds phrases containing the n-grams of "machin"
hits = provider.search("machin", limit=5)
```

### Integration with Search

```python
from whoosh_modern.autocomplete import create_autocomplete

# Build the autocomplete provider
provider = create_autocomplete("inverted")
provider.add(["python", "java", "javascript", "go", "rust"])

# Use in a search endpoint
def suggest(prefix: str, limit: int = 5):
    hits = provider.search(prefix, limit=limit)
    return [{"text": h.text, "score": h.score} for h in hits]

# In your FastAPI/REST endpoint:
# GET /api/suggest?q=py&limit=5
# Response: [{"text": "python", "score": 0.45}, ...]
```

## Comparison of Providers

| Provider              | Matching       | Strengths                    | Weaknesses                | Dependency    |
|-----------------------|----------------|------------------------------|---------------------------|---------------|
| `inverted`            | Prefix         | Simple, fast, no deps        | No typo tolerance         | None          |
| `ngram`               | N-gram overlap | Substring matching, flexible | Slower than prefix        | None          |
| `fuzzy`               | Edit distance  | Typo tolerance, flexible     | Requires rapidfuzz        | `rapidfuzz`   |

## Installation

```bash
# Core autocomplete (inverted + n-gram)
pip install whoosh-ng

# With fuzzy matching
pip install whoosh-ng[fuzzy]

# Full modern analysis
pip install whoosh-ng[modern]
```

## See Also

- [Plugin System Guide](plugins-advanced.md) — Plugin registration and discovery
- [Middleware Guide](middleware-pipeline.md) — Middleware pipeline integration
- [Provider Integration Guide](provider-integration.md) — Complete pipeline guide for all providers
- [API: Modern](../api/modern.md) — Full API reference for autocomplete extensions


## DOCUMENT: Autocomplete

# Autocomplete

An optional edge-ngram style autocomplete layer for Whoosh-NG.

## Install

```bash
pip install whoosh-ng[autocomplete]
```

## Minimal index

```python
from whoosh.fields import Schema, TEXT, KEYWORD

schema = Schema(
    title=TEXT(stored=True),
    tags=KEYWORD(stored=True, commas=True),
)

with ix.writer() as writer:
    writer.add_document(title="Python Quickstart", tags="python,quickstart")
    writer.commit()
```

## Query autocomplete

```python
from whoosh_modern.autocomplete import create_autocomplete

# Create provider (inverted = prefix matching by default)
provider = create_autocomplete("inverted")

# Index terms (typically done at index time)
provider.add(["python", "quickstart", "programming", "pyramid"])

# Search for suggestions
hits = provider.search("py", limit=5)
for hit in hits:
    print(hit.text, hit.score)
# Output: python 0.9, pyramid 0.8
```

## Modern Autocomplete Providers (Whoosh-NG 2.0)

Whoosh-NG 2.0 introduces multiple autocomplete provider strategies: `InvertedIndexAutocomplete`, `NGramProvider`, and `FuzzySuggestProvider`. For full details on creating, registering, and switching providers, see the [Autocomplete Providers Guide](autocomplete-providers.md).

## Autocomplete Provider Integration in the Pipeline

Autocomplete providers operate in **two modes**: standalone (in-memory index of phrases) and registry-based (discovered via `AutocompleteRegistry`). The `AutocompletePlugin` registers the default `"inverted"` provider at startup.

### Architecture

```text
┌─────────────────────────────────────────────────────────────────┐
│  Registration (startup)                                         │
│                                                                 │
│  AutocompletePlugin.register(PluginManager)                     │
│    └── AutocompleteRegistry.register("inverted", provider)      │
│                                                                 │
│  The "inverted" provider is now available globally              │
└─────────────────────────────────────────────────────────────────┘

┌─────────────────────────────────────────────────────────────────┐
│  Mode 1: Standalone (in-memory index)                           │
│                                                                 │
│  provider = create_autocomplete("inverted")                     │
│  provider.add(["python", "java", "javascript"])                 │
│  hits = provider.search("py", limit=5)                          │
│  └── [AutocompleteHit(text="python", score=0.9), ...]           │
│                                                                 │
│  No Whoosh index required. Pure in-memory lookup.               │
└─────────────────────────────────────────────────────────────────┘

┌─────────────────────────────────────────────────────────────────┐
│  Mode 2: Registry-based (tied to a Whoosh index)                │
│                                                                 │
│  provider = AutocompleteRegistry.get("inverted")                │
│                                                                 │
│  # Populate from index terms                                    │
│  with ix.searcher() as s:                                       │
│      for term in s.reader().all_terms():                       │
│          provider.add_term(term, s.doc_freq(term))              │
│                                                                 │
│  # Query suggestions                                            │
│  hits = provider.suggest("py", maxdist=1, limit=5)              │
│  └── [AutocompleteHit(text="python", score=...), ...]           │
└─────────────────────────────────────────────────────────────────┘
```

### Full workflow: indexing + autocomplete

```python
from whoosh import index, fields
from whoosh_modern.autocomplete import create_autocomplete
from whoosh_modern.autocomplete.plugin import AutocompletePlugin
from whoosh.plugins.manager import PluginManager

# 1. Register autocomplete plugin at startup
manager = PluginManager()
AutocompletePlugin().register(manager)

# 2. Create index
schema = fields.Schema(
    title=fields.TEXT(stored=True),
    content=fields.TEXT,
)
ix = index.create_in("indexdir", schema)

# 3. Index documents
with ix.writer() as writer:
    writer.add_document(title="Python programming", content="Learn Python")
    writer.add_document(title="Java development", content="Learn Java")
    writer.add_document(title="JavaScript basics", content="Learn JS")
    writer.commit()

# 4. Build autocomplete provider from index terms
provider = create_autocomplete("inverted")

with ix.searcher() as searcher:
    reader = searcher.reader()
    for term in reader.all_terms():
        # Add term with its document frequency as score weight
        provider.add([term.decode("utf-8")])

# 5. Query autocomplete
hits = provider.search("py", limit=5)
for hit in hits:
    print(f"{hit.text} (score: {hit.score:.3f})")
# Output: python (score: 0.429)
```

### Provider strategies

| Provider | Strategy | Best for |
|----------|----------|----------|
| `InvertedIndexAutocomplete` | Prefix matching with scoring | Simple autocomplete, small vocabularies |
| `NGramProvider` | Character n-gram indexing | Substring matching, typo tolerance |
| `FuzzySuggestProvider` | Approximate matching via rapidfuzz | Typo-tolerant suggestions |

```python
# Prefix matching (default)
provider = create_autocomplete("inverted")
provider.add(["python", "pyramid", "pyodbc"])
hits = provider.search("py")
# → python, pyramid, pyodbc

# N-gram matching
provider = create_autocomplete("ngram", n=3)
provider.add(["python", "java", "javascript"])
hits = provider.search("pyt")
# → python (matched by "pyt" n-gram)

# Fuzzy matching
provider = create_autocomplete("fuzzy", max_distance=2, score_cutoff=60.0)
provider.add(["python", "pyramid", "pyodbc"])
hits = provider.search("pythn")  # typo: missing 'o'
# → python (fuzzy match)
```

### Integration with search middleware

Autocomplete providers can be combined with search middleware for real-time suggestions:

```python
from whoosh.middleware.chain import MiddlewareChain
from whoosh.middleware.wrappers import MiddlewareSearcher

provider = create_autocomplete("inverted")
provider.add(["python", "java", "javascript"])

# In a search handler
def suggest(query: str, limit: int = 5):
    hits = provider.search(query, limit=limit)
    return {"suggestions": [hit.text for hit in hits]}
```


## DOCUMENT: Configuration Engine

# Configuration Engine

Module: `whoosh_modern.config`
Version: 3.1.0

Whoosh-NG includes a **Configuration Engine** (`ConfigEngine`) that loads,
validates, and merges application configuration from YAML or JSON files. It is
built on Pydantic models and supports hierarchical layering so that environment-
specific overrides can cleanly extend base settings.

## Core concepts

### Pydantic models

All configuration is expressed through typed Pydantic models:

- `WhooshNGConfig` — top-level application config
- `FieldConfig` — per-field indexing options
- `SearchConfig` / `FuzzyConfig` / `RankingConfig` / `AIConfig`
- `DataSourceConfigModel` — data source connection and sync settings
- `StorageConfigModel` — storage backend selection

### Loaders

Two loaders are provided:

- `load_yaml(path)` — parse a YAML file into a `dict`
- `load_json(path)` — parse a JSON file into a `dict`
- `load_config(path)` — auto-detect format from extension and return a validated
  `WhooshNGConfig`

### Hierarchical merging

`ConfigEngine.load(path, priority=...)` and `ConfigEngine.merge(overrides, priority=...)`
stack configuration sources with the following precedence (highest wins):

1. `runtime`
2. `instance`
3. `application`
4. `language`

Invalid ``priority`` values raise ``ValueError`` immediately, so misconfigured
layers cannot silently affect the merge order.

Merging is deep: nested dictionaries are merged recursively. Scalar values and
lists are **replaced entirely** by the override values; lists are NOT appended
or combined. For example, a base config with ``{"plugins": ["a", "b"]}``
overridden by ``{"plugins": ["c"]}`` produces ``{"plugins": ["c"]}``, not
``{"plugins": ["a", "b", "c"]}``. If additive list merging is required, handle
it at the application level before calling :meth:`ConfigEngine.merge`.

> [!WARNING]
> **List replacement behavior**: When merging configurations, lists are
> **completely overwritten** by higher-priority layers. They are not appended,
> concatenated, or deduplicated. This is an intentional design choice that
> ensures explicit control over list contents across layers and avoids
> unpredictable merged states. If you need additive behavior (e.g., extending
> a list of plugins or middlewares), perform the merge logic in your application
> code before passing the final dictionary to :meth:`ConfigEngine.merge`.

## Quick start

```python
from whoosh_modern.config import ConfigEngine

engine = ConfigEngine()
engine.load("whoosh-ng.yml", priority="application")
engine.load("whoosh-ng.local.yml", priority="instance")
engine.merge({"search": {"fuzzy": {"distance": 5}}}, priority="runtime")

config = engine.get_config()
print(config.index)
print(config.fields["title"].stemming)
print(config.search.fuzzy.distance)
```

## YAML example

```yaml
# whoosh-ng.yml
index: products
languages:
  default: fr
fields:
  title:
    type: text
    language: fr
    stemming: true
    stored: true
  price:
    type: numeric
    sortable: true
search:
  fuzzy:
    enabled: true
    distance: 2
storage:
  type: file
  path: ./index
```

## JSON example

```json
{
  "index": "products",
  "languages": {"default": "en"},
  "fields": {
    "title": {"type": "text", "language": "en", "stemming": true},
    "price": {"type": "numeric", "sortable": true}
  },
  "search": {"fuzzy": {"enabled": true, "distance": 2}},
  "storage": {"type": "file", "path": "./index"}
}
```

## Complete YAML examples

### Minimal config

```yaml
# whoosh-ng.yml
index: my_index
fields:
  title:
    type: text
    stored: true
storage:
  type: file
  path: ./index
```

### E-commerce catalog with CSV source

```yaml
# whoosh-ng.yml
index: products
fields:
  sku:
    type: text
    stored: true
    unique: true
  name:
    type: text
    language: fr
    stemming: true
    stored: true
  description:
    type: text
    language: fr
    stemming: true
  price:
    type: numeric
    sortable: true
    faceted: true
  category:
    type: text
    faceted: true
  published_at:
    type: datetime
    faceted: true
search:
  fuzzy:
    enabled: true
    distance: 2
data_source:
  type: csv
  path: Datas/products.csv
  delimiter: ","
  encoding: utf-8
  id_field: sku
storage:
  type: file
  path: ./index
```

### Layered configuration (base + instance + runtime)

```yaml
# whoosh-ng.yml  (application layer)
index: app
fields:
  title:
    type: text
    stemming: true
search:
  fuzzy:
    enabled: true
    distance: 2
storage:
  type: file
  path: ./index
```

```yaml
# whoosh-ng.local.yml  (instance layer)
index: app-staging
storage:
  type: file
  path: ./index-staging
```

```python
# runtime override in code
engine = ConfigEngine()
engine.load("whoosh-ng.yml", priority="application")
engine.load("whoosh-ng.local.yml", priority="instance")
engine.merge({"search": {"fuzzy": {"distance": 3}}}, priority="runtime")
app = engine.build()
```

## Complete JSON examples

### Minimal config

```json
{
  "index": "my_index",
  "fields": {
    "title": {"type": "text", "stored": true}
  },
  "storage": {"type": "file", "path": "./index"}
}
```

### Full-stack config with SQL source and hybrid storage

```json
{
  "index": "customers",
  "fields": {
    "customer_id": {"type": "numeric", "stored": true, "sortable": true},
    "first_name": {"type": "text", "language": "en", "stemming": true, "stored": true},
    "last_name": {"type": "text", "language": "en", "stemming": true, "stored": true},
    "city": {"type": "text", "language": "en", "stemming": true, "stored": true},
    "country": {"type": "text", "stored": true},
    "signup_date": {"type": "datetime", "faceted": true}
  },
  "search": {
    "fuzzy": {"enabled": true, "distance": 2},
    "highlight": {"enabled": true, "fragment_size": 200}
  },
  "data_source": {
    "type": "sql",
    "connection_string": "sqlite:///benchmark_data.db",
    "query": "SELECT * FROM customers",
    "id_field": "customer_id"
  },
  "storage": {
    "type": "hybrid",
    "local_path": "./index-cache",
    "remote": {
      "type": "s3",
      "bucket": "my-bucket",
      "prefix": "whoosh-indexes/"
    }
  }
}
```

## Using ConfigEngine.build() for zero-code setup

```python
from whoosh_modern.config import ConfigEngine

engine = ConfigEngine()
engine.load("whoosh-ng.yml")
app = engine.build()
app.build()

# Add documents through the index writer
writer = app.index.writer()
writer.add_document(title="Premier cours de Python", body="...")
writer.add_document(title="Whoosh-NG avancé", body="...")
writer.commit()

# Or use the source directly for streaming/batch indexing
for doc in app._source.iter_documents():
    with app.index.writer() as writer:
        writer.add_document(**doc)

results = app.search("python")
```

## Module reference

| Module | Purpose |
|---|---|
| `whoosh_modern.config.models` | Pydantic models for validation |
| `whoosh_modern.config.loader` | YAML / JSON file loaders |
| `whoosh_modern.config.engine` | `ConfigEngine` with hierarchical merging |

## See Also

- [Storage Providers](storage-providers.md) — Storage backends configurable via `StorageConfigModel`
- [Data Sources](data-sources.md) — `DataSourceConfigModel` and provider configuration


## DOCUMENT: Linguistics

# Synonyms & Linguistics

Module: `whoosh_modern.linguistics.synonyms`, `whoosh_modern.linguistics.stemmers`
Version: 2.0.0

The linguistics module provides a comprehensive synonym expansion engine and language-specific text analyzers. It integrates with the middleware pipeline to expand queries and documents with synonyms at both index time and query time.

## Module Overview

```text
whoosh_modern.linguistics
    ├── synonyms/
    │   ├── provider.py       # SynonymProvider protocol + StaticSynonymProvider
    │   ├── yaml_provider.py  # YAMLSynonymProvider
    │   ├── json_provider.py  # JSONSynonymProvider
    │   ├── store.py          # SQLiteSynonymStore
    │   ├── compiler.py       # SynonymCompiler
    │   ├── manager.py        # SynonymManager
    │   ├── middleware.py     # SynonymExpansionMiddleware
    │   └── languages.py      # LANG_SYNONYMS (FR/EN/DE/ES/IT)
    └── stemmers/
        └── __init__.py       # Language-specific analyzers (FR/EN/DE/ES/IT)
```

## Synonym Providers

### SynonymProvider (Protocol)

The base protocol that all synonym providers implement:

```python
from whoosh_modern.linguistics.synonyms import SynonymProvider

class MyProvider(SynonymProvider):
    def get_synonyms(self, word: str) -> list[str]:
        """Return synonyms for the given word."""
        ...

    def add_synonym(self, word: str, synonyms: list[str]) -> None:
        """Add synonyms for the given word."""
        ...

    def remove_synonym(self, word: str, synonym: str) -> None:
        """Remove a synonym for the given word."""
        ...
```

### StaticSynonymProvider

In-memory provider backed by a dictionary:

```python
from whoosh_modern.linguistics.synonyms import StaticSynonymProvider

provider = StaticSynonymProvider({
    "car": ["automobile", "vehicle", "auto"],
    "house": ["home", "residence"],
})

print(provider.get_synonyms("car"))  # ['automobile', 'vehicle', 'auto']
```

### YAMLSynonymProvider

Loads synonyms from a YAML file:

```yaml
# synonyms.yaml
car:
  - automobile
  - vehicle
  - auto
house:
  - home
  - residence
```

```python
from whoosh_modern.linguistics.synonyms import YAMLSynonymProvider

# Requires: pip install pyyaml
provider = YAMLSynonymProvider("synonyms.yaml")
print(provider.get_synonyms("car"))  # ['automobile', 'vehicle', 'auto']
```

### JSONSynonymProvider

Loads synonyms from a JSON file:

```json
{
    "car": ["automobile", "vehicle", "auto"],
    "house": ["home", "residence"]
}
```

```python
from whoosh_modern.linguistics.synonyms import JSONSynonymProvider

provider = JSONSynonymProvider("synonyms.json")
print(provider.get_synonyms("car"))
```

### SQLiteSynonymStore

Persistent synonym store backed by SQLite:

```python
from whoosh_modern.linguistics.synonyms import SQLiteSynonymStore

store = SQLiteSynonymStore("synonyms.db")

# CRUD operations
store.add_synonym("car", ["automobile", "vehicle"])
print(store.get_synonyms("car"))  # ['automobile', 'vehicle']
store.remove_synonym("car", "automobile")
print(store.get_synonyms("car"))  # ['vehicle']
store.close()
```

### SynonymCompiler

Precompiles raw synonym data into a fast lookup format:

```python
from whoosh_modern.linguistics.synonyms import SynonymCompiler

compiler = SynonymCompiler({"car": ["automobile", "vehicle"]})
compiler.add("house", ["home", "residence"])
compiler.merge({"book": ["publication", "work"]})

compiled = compiler.compile()
print(compiled)
# {'car': ['automobile', 'vehicle'], 'house': ['home', 'residence'], 'book': ['publication', 'work']}
```

## SynonymManager

The `SynonymManager` is the high-level interface for managing synonyms. It wraps a `StaticSynonymProvider` internally and supports import/export:

```python
from whoosh_modern.linguistics.synonyms import SynonymManager

manager = SynonymManager({"car": ["automobile", "vehicle"]})

# CRUD
manager.add_synonyms("house", ["home", "residence"])
print(manager.get_synonyms("house"))  # ['home', 'residence']
manager.remove_synonym("house", "home")

# Import from external sources
manager.import_yaml("synonyms.yaml")   # Requires PyYAML
manager.import_json("synonyms.json")

# Export
manager.export_json("output.json")
```

### Import/Export Workflow

```python
# Import from YAML
manager = SynonymManager()
manager.import_yaml("my_synonyms.yaml")

# Export to JSON (e.g., for migration or backup)
manager.export_json("backup.json")
```

## Prebuilt Language Synonyms

The `LANG_SYNONYMS` dictionary contains starter synonym mappings for five languages:

```python
from whoosh_modern.linguistics.synonyms import LANG_SYNONYMS

# Available languages: fr, en, de, es, it
french_syns = LANG_SYNONYMS["fr"]
print(french_syns["voiture"])  # ['automobile', 'véhicule']

english_syns = LANG_SYNONYMS["en"]
print(english_syns["car"])  # ['automobile', 'vehicle']

# Bootstrap a SynonymManager with a language
manager = SynonymManager(LANG_SYNONYMS["fr"])
```

| Language | Code | Sample Entry                          |
|----------|------|---------------------------------------|
| French   | `fr` | `"voiture": ["automobile", "véhicule"]` |
| English  | `en` | `"car": ["automobile", "vehicle"]`    |
| German   | `de` | `"auto": ["wagen", "fahrzeug"]`       |
| Spanish  | `es` | `"coche": ["automóvil", "vehículo"]`  |
| Italian  | `it` | `"auto": ["automobile", "veicolo"]`   |

> **Note**: These are minimal starter dictionaries for demonstration. Production deployments should load from curated or domain-specific sources.

## SynonymExpansionMiddleware

Integrates synonym expansion into the middleware pipeline. It expands both search queries and indexed document fields:

```python
from whoosh_modern.linguistics.synonyms import (
    SynonymManager,
    SynonymExpansionMiddleware,
)

# Create a manager with your synonyms
manager = SynonymManager({
    "car": ["automobile", "vehicle"],
    "house": ["home", "residence"],
})

# Create the middleware
middleware = SynonymExpansionMiddleware(manager)

# Register with the PluginManager or MiddlewareChain
from whoosh.plugins.manager import PluginManager
PluginManager._default.register_middleware("synonym", middleware)
```

### How It Works

- **`before_search`**: Expands `context.query` by appending synonyms for each token
- **`before_index`**: Expands string values in `context.document` by appending synonyms

```python
# Before: query = "car"
# After:  query = "car automobile vehicle"

# Before: document = {"title": "house for sale"}
# After:  document = {"title": "house for sale home residence"}
```

## Language-Specific Stemming Analyzers

Located in `whoosh_modern.linguistics.stemmers`, these analyzers combine tokenization, stemming, and stop-word removal:

```python
from whoosh_modern.linguistics.stemmers import (
    EnglishAnalyzer,
    FrenchAnalyzer,
    GermanAnalyzer,
    SpanishAnalyzer,
    ItalianAnalyzer,
)

# Each analyzer is an instance of LanguageAnalyzer and is callable: it returns a list of tokens
analyzer = EnglishAnalyzer
tokens = analyzer("The running cats")
# tokens are stemmed: ["run", "cat"] (stop words removed)

# Backward-compatible "class-style" usage also works: calling the analyzer
# with no arguments returns a fresh analyzer instance, so historical code
# written as EnglishAnalyzer()(text) keeps working unchanged.
tokens = EnglishAnalyzer()("The running cats")
```

### Stemmer Backend Selection

Under the hood, the stemmers use `whoosh_modern.analysis.stemmer_providers`:

```python
from whoosh_modern.analysis.stemmer_providers import (
    get_stemmer,
    register_stemmer,
    list_available_backends,
)

# Auto-detect best available stemmer (PyStemmer preferred)
stemmer = get_stemmer("auto", "english")

# Explicit backend
stemmer = get_stemmer("internal", "english")   # Whoosh's built-in stemmer
stemmer = get_stemmer("pystemmer", "english")   # PyStemmer (faster)

# List available backends
print(list_available_backends())
# {'internal': 'available', 'pystemmer': 'available', ...}

# Register a custom stemmer
@register_stemmer("my_stemmer")
class MyStemmer:
    def stem(self, word: str) -> str:
        return word.lower()
```

| Backend       | Requires                          | Speed   |
|---------------|-----------------------------------|---------|
| `auto`        | None (falls back automatically)  | Fastest available |
| `internal`    | None (built-in Porter stemmer)   | Medium  |
| `pystemmer`   | `pip install whoosh-ng[fast-stemming]` | Fast |

## Integration Example: Full Pipeline

```python
from whoosh_modern.linguistics import (
    EnglishAnalyzer,
    LANG_SYNONYMS,
    SynonymExpansionMiddleware,
    SynonymManager,
)
from whoosh.middleware.chain import MiddlewareChain
from whoosh.middleware.wrappers import MiddlewareWriter, MiddlewareSearcher

# 1. Build synonym manager with English synonyms
syn_manager = SynonymManager(LANG_SYNONYMS["en"])
syn_manager.add_synonyms("search", ["query", "find", "lookup"])

# 2. Create synonym expansion middleware
syn_middleware = SynonymExpansionMiddleware(syn_manager)

# 3. Build middleware chain
chain = MiddlewareChain([syn_middleware])

# 4. Wrap writer and searcher
with MiddlewareWriter(ix.writer(), chain) as writer:
    writer.add_document(title="How to search in Whoosh")

with MiddlewareSearcher(ix.searcher(), chain) as searcher:
    # Query "search" is expanded to "search query find lookup"
    results = searcher.search("search")
```

## Language Registry

The `LanguageRegistry` maps language codes to `LanguageProfile` instances, centralizing analyzer, stemmer, synonym provider, and language detector resolution.

```python
from whoosh_modern.linguistics.registry import (
    LanguageRegistry,
    LanguageProfile,
    StemmerRegistry,
    get_default_registry,
)

# Use the pre-populated default registry (FR/EN/DE/ES/IT)
registry = get_default_registry()

# Resolve a language profile
profile = registry.resolve("fr")
print(profile.language)   # "fr"
print(profile.analyzer)   # FrenchAnalyzer instance

# Register a custom language profile
custom = LanguageProfile(
    language="pt",
    analyzer=...,  # your analyzer
    stemmer=...,   # your stemmer
)
registry.register(custom)

# StemmerRegistry adds stemmer-specific helpers
stem_registry = StemmerRegistry(registry._profiles.values())
stemmer = stem_registry.get_stemmer("fr")
```

## Multi-Language Analyzer

`MultiLanguageAnalyzer` applies multiple language analyzers simultaneously for multilingual indexing.

```python
from whoosh_modern.linguistics.analyzers import MultiLanguageAnalyzer

# Default: FR/EN/DE/ES/IT
analyzer = MultiLanguageAnalyzer()

# Custom language set
analyzer = MultiLanguageAnalyzer(languages=["fr", "en"])

tokens = analyzer("hello bonjour")
# Returns combined tokens from all configured analyzers
```

## Language Auto-Detection

`StopwordDetector` and `LangDetectProvider` enable automatic language detection:

```python
from whoosh_modern.linguistics.detection import StopwordDetector

detector = StopwordDetector(supported_languages=["fr", "en", "de"])
lang = detector.detect("Ceci est un texte en français")
print(lang)  # "fr"
```

Use with `SearchApplication` for automatic language resolution:

```python
from whoosh_modern import SearchApplication
from whoosh_modern.linguistics.detection import StopwordDetector

app = SearchApplication(
    source=my_source,
    language_detector=StopwordDetector(),
)

# FieldConfig supports language="auto"
# The detector resolves the language per document
```

## Explain Analyzer

`ExplainAnalyzer` exposes the tokenization/stemming pipeline for Search Studio:

```python
from whoosh_modern.linguistics.explain import ExplainAnalyzer

explainer = ExplainAnalyzer(EnglishAnalyzer)
result = explainer.explain("The running cats")

print(result.text)       # "The running cats"
print(result.tokens)     # ["run", "cat"]
```

## Debugging Analysis with ExplainAnalyzer

`ExplainAnalyzer` wraps any existing analyzer and returns an
`AnalysisExplanation` describing how a text is transformed. It is useful
for debugging complex analyzer chains, especially when mixing multilingual
analyzers, stopword filters, or dictionary stem overrides.

```python
from whoosh_modern.linguistics.explain import ExplainAnalyzer
from whoosh.analysis import StandardAnalyzer

explainer = ExplainAnalyzer(StandardAnalyzer())
explanation = explainer.explain("A quick brown fox jumps over the lazy dog")

print(f"Original text: {explanation.text}")
print(f"Final tokens : {explanation.tokens}")

print("\nStep-by-step explanations:")
for step in explanation.explanations:
    print(
        f"  - {step.step}: '{step.original}' -> '{step.result}'"
    )
```

Example output:

```text
Original text: A quick brown fox jumps over the lazy dog
Final tokens : ['quick', 'brown', 'fox', 'jumps', 'lazy', 'dog']

Step-by-step explanations:
  - tokenize: 'A' -> 'A'
  - lowercase: 'A' -> 'a'
  - stop: 'a' -> ''
  - tokenize: 'quick' -> 'quick'
  - lowercase: 'quick' -> 'quick'
  ...
```

### Interpreting the output

- `explanation.text` — the original input text.
- `explanation.tokens` — the final token list after all analyzer steps.
- `explanation.explanations` — a chronological list of
  `TokenExplanation` objects showing each transformation step.

Use this when:
- an analyzer pipeline behaves differently than expected,
- you need to verify which stopwords or stemming rules are applied,
- you want to compare behavior across languages with `MultiLanguageAnalyzer`.

## Dictionary Stem Override

Override Snowball stemming with business dictionaries:

```python
from whoosh_modern.linguistics.dictionary_stem_override import DictionaryStemOverride

override = DictionaryStemOverride({
    "voiture": "voitur",
    "maison": "maison",
})

print(override.stem("voiture"))  # "voitur"
print(override.stem("maison"))   # "maison"

# Add rules dynamically
override.add_rule("chien", "chien")
```

Use with `SearchApplication`:

```python
from whoosh_modern import SearchApplication

app = SearchApplication(
    source=my_source,
    dictionary_stem_overrides={"voiture": "voitur"},
)
```

## Cached Stemming Analyzer

`CachedStemmingAnalyzer` wraps language analyzers with LRU caching:

```python
from whoosh_modern.analysis.cached_stemming_analyzer import CachedStemmingAnalyzer
from whoosh_modern.linguistics.stemmers import FrenchAnalyzer

cached = CachedStemmingAnalyzer(FrenchAnalyzer, cache_size=50000)
tokens = cached("les maisons")
```

## Stemmer Profiler

Measure stemming impact on vocabulary and performance:

```python
from whoosh_modern.profiling.stemmer_profiler import StemmerProfiler

profiler = StemmerProfiler(stemmer=my_stemmer)
report = profiler.profile(["document 1", "document 2", ...])

print(report.original_tokens)        # Total tokens before stemming
print(report.stemmed_tokens)         # Unique tokens after stemming
print(report.reduction_ratio)        # Vocabulary reduction ratio
print(report.estimated_size_reduction)  # Estimated index size reduction %
print(report.avg_stem_time_ms)       # Average stemming time per token
```

## Analyzer Presets

Preconfigured analyzers for common search scenarios:

```python
from whoosh_modern.analysis.stemmer_presets import AnalyzerPresets

# Autocomplete
autocomplete_analyzer = AnalyzerPresets.autocomplete()

# Partial match
partial_analyzer = AnalyzerPresets.partial_match()

# Ecommerce
ecommerce_analyzer = AnalyzerPresets.ecommerce()

# Blog
blog_analyzer = AnalyzerPresets.blog()

# Multilingual
multilingual_analyzer = AnalyzerPresets.multilingual()

# Get by name
analyzer = AnalyzerPresets.get("autocomplete")
```

## See Also

- [Synonyms](synonyms.md) — Synonym providers, manager, and Wiktionary dictionaries
- [Stemming Guide](stemming-providers.md) — Stemmer providers and language analyzers
- [Middleware Guide](middleware-pipeline.md) — Middleware pipeline integration
- [Provider Integration Guide](provider-integration.md) — Complete pipeline guide for all providers
- [API: Linguistics](../api/modern.md) — Full API reference


## DOCUMENT: Middleware Pipeline

# Middleware & Plugin Pipeline

Module: `whoosh.middleware`, `whoosh.middleware.chain`, `whoosh.middleware.context`, `whoosh_modern.middleware`
Version: 3.0.0

The middleware pipeline allows you to intercept and modify indexing and search operations. It is the primary extension mechanism for cross-cutting concerns like logging, caching, metrics, query rewriting, and security. Middleware can come from both the core `whoosh.middleware` package and from plugins loaded via the `PluginManager`.

## Architecture Overview

```text
Writer/Searcher  ───►  MiddlewareChain
                           ├── Middleware 1 (before hook)
                           ├── Middleware 2 (before hook)
                           ├── ─── core operation ───
                           ├── Middleware 2 (after hook, reverse)
                           └── Middleware 1 (after hook, reverse)
```

- **Before hooks** execute in registration order
- **After hooks** execute in reverse order (like a stack / onion)
- If a hook raises `StopOperation`, the pipeline aborts gracefully
- If `fail_open=False` (default), exceptions propagate immediately

## Core Middleware Classes

### Middleware (Base Class)

Located in `whoosh.middleware.base`. Subclasses implement lifecycle hooks:

```python
from whoosh.middleware.base import Middleware
from whoosh.middleware.context import MiddlewareContext

class MyMiddleware(Middleware):
    def startup(self, context: MiddlewareContext) -> None:
        """Called once when middleware is initialized."""
        pass

    def shutdown(self, context: MiddlewareContext) -> None:
        """Called once when middleware is torn down."""
        pass

    def before_index(self, context: MiddlewareContext) -> MiddlewareContext:
        """Called before a document is indexed. Modify context.document."""
        return context

    def after_index(self, context: MiddlewareContext) -> MiddlewareContext:
        """Called after a document is indexed."""
        return context

    def before_delete(self, context: MiddlewareContext) -> MiddlewareContext:
        """Called before a document is deleted."""
        return context

    def after_delete(self, context: MiddlewareContext) -> MiddlewareContext:
        """Called after a document is deleted."""
        return context

    def before_search(self, context: MiddlewareContext) -> MiddlewareContext:
        """Called before a search query is executed. Modify context.query."""
        return context

    def after_search(self, context: MiddlewareContext) -> MiddlewareContext:
        """Called after results are returned. Access context.results."""
        return context

    def on_error(self, context: MiddlewareContext, exc: Exception) -> None:
        """Called when an exception occurs. Re-raise by default."""
        raise exc

    def on_commit(self, context: MiddlewareContext) -> None:
        """Called after a commit operation."""
        pass
```

### MiddlewareContext

Located in `whoosh.middleware.context`. The context object passed to every hook:

```python
class MiddlewareContext:
    def __init__(self, operation: str) -> None:
        self.operation: str           # e.g., "add_document", "search"
        self.index: Any = None        # The Index instance
        self.backend: Any = None       # The storage backend
        self.writer: Any = None        # The IndexWriter (if applicable)
        self.searcher: Any = None      # The Searcher (if applicable)
        self.document: dict[str, Any] | None  # Document being indexed
        self.query: str = ""           # The search query string
        self.collector: Any = None     # The collector (if applicable)
        self.results: Any = None       # Search results
        self.labels: dict[str, Any] = {}    # Arbitrary labels/key-value pairs
        self.metadata: dict[str, Any] = {} # Per-request metadata
```

Use `context.copy()` to create a shallow copy if you need to preserve state.

### MiddlewareChain

Located in `whoosh.middleware.chain`. Orchestrates middleware execution:

```python
from whoosh.middleware.chain import MiddlewareChain
from whoosh.middleware.context import MiddlewareContext

chain = MiddlewareChain([
    MetricsMiddleware(),
    CacheMiddleware(),
])

# Before hooks (in order)
context = MiddlewareContext("search")
context.query = "hello world"
context = chain.run_before("before_search", context)

# ... core search operation ...

# After hooks (in reverse order)
context = chain.run_after("after_search", context)
print(context.results)
```

**Async support**: Use `async_run_before()`, `async_run_after()`, `async_run_on_error()`, and `run_hook()` for async middleware.

### MiddlewareRegistry

Located in `whoosh.middleware.registry`. A class-level registry for named middleware:

```python
from whoosh.middleware.registry import MiddlewareRegistry

MiddlewareRegistry.register("my_mw", MyMiddleware(), owner="my_plugin")
mw = MiddlewareRegistry.get("my_mw")
MiddlewareRegistry.unregister("my_mw")
print(MiddlewareRegistry.list_all())  # ['my_mw', ...]
```

## Middleware Integration

### Wrappers: MiddlewareWriter & MiddlewareSearcher

Located in `whoosh.middleware.wrappers`. These wrap the core writer/searcher to automatically execute middleware hooks:

```python
from whoosh.middleware.wrappers import MiddlewareWriter, MiddlewareSearcher
from whoosh.middleware.chain import MiddlewareChain

chain = MiddlewareChain([MetricsMiddleware(), CacheMiddleware()])

# Wrap a writer
with MiddlewareWriter(ix.writer(), chain) as writer:
    writer.add_document(title="Hello", content="World")

# Wrap a searcher
with MiddlewareSearcher(ix.searcher(), chain) as searcher:
    results = searcher.search(query)
```

### Integration Helpers

Located in `whoosh.middleware.integration`:

```python
from whoosh.middleware.integration import apply_middleware_to_writer, apply_middleware_to_searcher

# Auto-loads middleware from PluginManager if chain is not provided
writer = apply_middleware_to_writer(ix.writer())
searcher = apply_middleware_to_searcher(ix.searcher())
```

## Built-in Middleware

### Core Middleware (`whoosh.middleware.base`)

| Class                  | Hooks              | Description                              |
|------------------------|--------------------|------------------------------------------|
| `CompressionMiddleware` | `before_index`    | Marks documents with `_compressed = True` |
| `EncryptionMiddleware`  | `before_index`    | Marks documents with `_encrypted = True`  |
| `MetricsMiddleware`     | `after_index`, `after_search` | Tracks indexed docs and search count |
| `CacheMiddleware`       | `before_search`, `after_search` | In-memory result caching |

### Observability (`whoosh.middleware.metrics`)

`PrometheusMiddleware` — exports metrics to Prometheus (requires `prometheus-client`):

```python
from whoosh.middleware.metrics import PrometheusMiddleware

# Requires: pip install whoosh-ng[metrics]
prom = PrometheusMiddleware()
# Exports: whoosh_searches_total, whoosh_documents_indexed_total, whoosh_search_duration_seconds
```

### Modern Middleware (`whoosh_modern.middleware`)

#### Resilience Middleware (core subclasses)

`RetryMiddleware`, `LoggingMiddleware`, and `CacheMiddleware` are now **subclasses of the
core `whoosh.middleware.base.Middleware`** (the same base class re-exported as
`whoosh_modern.middleware.Middleware`). They participate in the standard hook pipeline
(`before_index` / `after_index` / `before_search` / `after_search` / `on_error` /
`on_commit`) and additionally keep a `wrap(operation)` helper so arbitrary callables can
still be decorated.

`MiddlewarePipeline` is a thin wrapper around `whoosh.middleware.chain.MiddlewareChain`
that executes a callable through the chain hooks and returns its result. The previous
`whoosh_modern.middleware.pipeline` module has been removed — import these names directly
from `whoosh_modern.middleware`.

| Class                  | Description                              |
|------------------------|------------------------------------------|
| `RetryMiddleware`      | Retries failed operations with exponential backoff |
| `LoggingMiddleware`    | Logs operation execution time and errors |
| `CacheMiddleware`      | Caches operation results (LRU eviction)  |
| `MiddlewarePipeline`   | Chains multiple middlewares via `MiddlewareChain` |

```python
from whoosh_modern.middleware import MiddlewarePipeline, RetryMiddleware, LoggingMiddleware

pipeline = MiddlewarePipeline(
    LoggingMiddleware(),
    RetryMiddleware(attempts=3, backoff="exponential", jitter=True),
)

result = pipeline.execute(lambda: my_index_operation())
```

#### Storage Middleware (`whoosh_modern.middleware.storage`)

| Class                  | Description                              |
|------------------------|------------------------------------------|
| `StorageMiddleware`    | Routes persistence through pluggable storage providers |
| `FileStorageProvider`  | Local filesystem storage                 |
| `SQLiteStorageProvider`| SQLite-backed blob storage               |
| `S3StorageProvider`    | S3 / S3-compatible cloud storage         |

```python
from whoosh_modern.middleware.storage import StorageMiddleware, FileStorageProvider

storage = StorageMiddleware(FileStorageProvider("/data/index"), name="primary")
```

#### Search Middleware (`whoosh_modern.middleware.search`)

| Class                      | Description                              |
|----------------------------|------------------------------------------|
| `QueryRewriteMiddleware`   | Rewrites `context.query` before search   |
| `RankingMiddleware`        | Re-ranks `context.results` after search  |

```python
from whoosh_modern.middleware.search import QueryRewriteMiddleware

def add_synonyms(query: str) -> str:
    # Expand query with synonyms before execution
    return query + " " + get_synonyms(query)

rewriter = QueryRewriteMiddleware(rewriter=add_synonyms)
```

#### Analyzer Middleware (`whoosh_modern.middleware.analyzer`)

| Class                  | Description                              |
|------------------------|------------------------------------------|
| `StemmingMiddleware`   | Applies a stemmer to document fields and query |
| `SynonymMiddleware`    | Expands text with synonyms (placeholder) |

## Creating Custom Middleware

### Hook-Based Middleware

```python
from whoosh.middleware.base import Middleware
from whoosh.middleware.context import MiddlewareContext

class RequestLoggingMiddleware(Middleware):
    """Log all search requests with timing."""

    def before_search(self, context: MiddlewareContext) -> MiddlewareContext:
        import time
        context.metadata["_start_time"] = time.time()
        logger.info(f"[SEARCH] Query: {context.query}")
        return context

    def after_search(self, context: MiddlewareContext) -> MiddlewareContext:
        elapsed = time.time() - context.metadata.get("_start_time", time.time())
        result_count = len(context.results) if context.results is not None else 0
        logger.info(f"[RESULTS] Found {result_count} hits in {elapsed:.3f}s")
        return context
```

### Custom Middleware (Hook-Based)

The `Middleware` base class is `whoosh.middleware.base.Middleware`, re-exported from
`whoosh_modern.middleware`. Subclass it and implement the lifecycle hooks:

```python
from whoosh_modern.middleware import Middleware
from whoosh.middleware.context import MiddlewareContext

class RetryMiddleware(Middleware):
    """Retry failed operations with backoff (illustrative)."""

    def __init__(self, attempts: int = 3) -> None:
        self._attempts = attempts

    def on_error(self, context: MiddlewareContext, exc: Exception) -> None:
        # Built-in resilience middlewares already implement this pattern.
        # Custom logic can record failures in context.metadata here.
        context.metadata.setdefault("retry_errors", 0)
        context.metadata["retry_errors"] += 1
        raise exc
```

> Note: the built-in `RetryMiddleware`, `LoggingMiddleware`, and `CacheMiddleware` also
> expose a `wrap(operation)` helper (preserved for backwards compatibility) so they can
> decorate plain callables, but their primary mechanism is the hook pipeline above.

### Middleware with Plugin Integration

Register middleware via a plugin so it's automatically discovered:

```python
from whoosh.plugins.manager import Plugin

class LoggingPlugin(Plugin):
    name = "logging"
    version = "1.0.0"
    middleware = ["whoosh_modern.middleware.LoggingMiddleware"]

    def register(self, manager):
        manager.register_middleware(
            "logging",
            LoggingMiddleware(),
        )
```

## Error Handling

### StopOperation

Abort a pipeline operation gracefully:

```python
from whoosh.middleware.exceptions import StopOperation

class RateLimitMiddleware(Middleware):
    def before_search(self, context: MiddlewareContext) -> MiddlewareContext:
        if not rate_limiter.allow(context):
            raise StopOperation("Rate limit exceeded")
        return context
```

### fail_open Behavior

```python
class ResilientMiddleware(Middleware):
    def on_error(self, context: MiddlewareContext, exc: Exception) -> None:
        try:
            send_to_analytics(context.results)
        except Exception:
            # Log but don't fail the search
            logger.warning("Analytics failed", exc_info=True)
        # Middleware chain continues
```

## Middleware Discovery from Plugins

When `PluginManager.load_plugins()` is called, all plugins that declare a `middleware` list will have those middleware classes imported and instantiated. The `get_middleware_chain()` method builds a `MiddlewareChain` from all registered middleware:

```python
from whoosh.plugins.manager import PluginManager

PluginManager.load_plugins()  # Discovers plugins and their middleware

manager = PluginManager._default
chain = manager.get_middleware_chain()
# chain is a MiddlewareChain ready for use
```

## Best Practices

1. **Statelessness**: Use `context.metadata` for per-request data, not instance attributes
2. **Lightweight hooks**: Keep `before_*` and `after_*` hooks fast; use async for I/O
3. **Order matters**: Place caching before metrics, authentication before routing
4. **Fail fast**: Only use `fail_open=True` for non-critical middleware
5. **Test isolation**: Mock the `MiddlewareContext` to test middleware independently
6. **Clean up**: Implement `shutdown()` for resources like connections and timers

## See Also

- [Plugin System Guide](plugins-advanced.md) — Plugin registration and entry points
- [Provider Integration Guide](provider-integration.md) — Complete pipeline guide for all providers
- [Middleware Examples](../examples/middleware.md) — Practical middleware patterns
- [API: Middleware](../api/middleware.md) — Full API reference
- [API: Middleware Pipeline (modern)](../api/modern.md) — Modern middleware extensions


## DOCUMENT: Middleware

# Middleware

The middleware pipeline allows you to intercept and modify indexing and search operations. It is the primary extension mechanism for cross-cutting concerns like logging, caching, metrics, and security.

## Core Concepts

A middleware is a class that implements hooks into the indexing and search lifecycle:

```python
from whoosh.middleware.base import Middleware
from whoosh.middleware.context import MiddlewareContext

class MyMiddleware(Middleware):
    def before_search(self, context: MiddlewareContext) -> MiddlewareContext:
        # Modify context.query or context.metadata
        return context

    def after_search(self, context: MiddlewareContext) -> MiddlewareContext:
        # Access context.results
        return context
```

## Available Hooks

| Hook | When | Common Uses |
|------|------|-------------|
| `startup(context)` | Middleware initialized | Open connections, warm caches |
| `shutdown(context)` | Middleware torn down | Close connections, flush buffers |
| `before_index(context)` | Before document added | Validation, enrichment, compression flags |
| `after_index(context)` | After document added | Metrics, events, cache invalidation |
| `before_delete(context)` | Before document deleted | Audit logging, access control |
| `after_delete(context)` | After document deleted | Metrics, cache invalidation |
| `before_search(context)` | Before query executes | Query rewriting, caching, auth |
| `after_search(context)` | After results returned | Logging, metrics, result modification |
| `on_error(context, exc)` | On exception | Error handling, fallbacks |
| `on_commit(context)` | After commit | Metrics, notifications |

## Built-in Middlewares

### MetricsMiddleware

Tracks basic statistics:

```python
from whoosh.middleware import MetricsMiddleware

metrics = MetricsMiddleware()
# After operations:
stats = metrics.get_metrics()
# Returns: {"documents_indexed": N, "searches_executed": N}
```

### CacheMiddleware

Caches search results in memory:

```python
from whoosh.middleware import CacheMiddleware

cache = CacheMiddleware()

# Check cache
cached = cache.get_cached("user query string")

# Store manually
cache.set_cached("user query string", results)
```

### CompressionMiddleware

Marks documents for compression at the backend level:

```python
from whoosh.middleware import CompressionMiddleware

compression = CompressionMiddleware()
# Sets document["_compressed"] = True
```

### EncryptionMiddleware

Marks documents for encryption at the backend level:

```python
from whoosh.middleware import EncryptionMiddleware

encryption = EncryptionMiddleware()
# Sets document["_encrypted"] = True
```

## MiddlewareChain

Orchestrates middleware execution:

```python
from whoosh.middleware import MiddlewareChain

chain = MiddlewareChain([
    MetricsMiddleware(),
    CacheMiddleware()
])

# Execute before hook
context = MiddlewareContext("search")
context.query = "test"
context = chain.run_before("before_search", context)

# ... core operation ...

# Execute after hook
context = chain.run_after("after_search", context)
```

### Execution Order

- `before_*` hooks run in registration order
- `after_*` hooks run in reverse order
- If a hook raises `StopOperation`, the pipeline aborts
- If `fail_open=False`, exceptions propagate immediately

## Integration

### With Writer

```python
from whoosh.middleware.integration import apply_middleware_to_writer

writer = apply_middleware_to_writer(ix.writer(), chain.middlewares)

with writer:
    writer.add_document(title="Hello", content="World")
```

### With Searcher

```python
from whoosh.middleware.integration import apply_middleware_to_searcher

searcher = apply_middleware_to_searcher(ix.searcher(), chain.middlewares)
results = searcher.search("query")
```

### With PluginManager

```python
from whoosh.plugins.manager import PluginManager

# Plugins can provide middleware
PluginManager.load_plugins()
chain = PluginManager.get_middleware_chain()
```

## Custom Middleware Example

```python
class RequestLoggingMiddleware(Middleware):
    """Log all search requests."""

    def before_search(self, context: MiddlewareContext) -> MiddlewareContext:
        context.metadata["request_id"] = generate_request_id()
        logger.info(f"Search: {context.query}")
        return context

    def after_search(self, context: MiddlewareContext) -> MiddlewareContext:
        logger.info(f"Found {len(context.results)} results")
        return context

class RateLimitMiddleware(Middleware):
    """Abort searches exceeding rate limit."""

    def before_search(self, context: MiddlewareContext) -> MiddlewareContext:
        if not rate_limiter.allow(context):
            raise StopOperation("Rate limit exceeded")
        return context

class QueryEnrichmentMiddleware(Middleware):
    """Add synonyms to the query."""

    def before_search(self, context: MiddlewareContext) -> MiddlewareContext:
        if context.query:
            context.query += " " + get_synonyms(context.query)
        return context
```

## Error Handling

```python
class ResilientMiddleware(Middleware):
    """Continue on non-critical errors."""

    def after_search(self, context: MiddlewareContext) -> MiddlewareContext:
        try:
            send_to_analytics(context.results)
        except Exception:
            # Log but don't fail the search
            logger.warning("Analytics failed", exc_info=True)
        return context
```

## Best Practices

1. **Stateless**: Use `context.metadata` for per-request data
2. **Fail fast**: Only use `fail_open=True` for non-critical middleware
3. **Order matters**: Place caching before metrics, auth before routing
4. **Performance**: Keep hooks lightweight; use async for I/O
5. **Testing**: Mock the context object to test middleware in isolation

## Provider Integration via Middleware

Many Whoosh-NG providers integrate into the indexing and search pipeline through
middleware hooks. This is the standard pattern for cross-cutting concerns that
need to transform documents, queries, or results.

### Provider-to-Middleware Mapping

| Provider | Middleware | Hooks Used | Purpose |
|----------|-----------|------------|---------|
| `StorageProvider` | `StorageMiddleware` | `before_index`, `on_commit` | Tags context with storage backend; writes commit checkpoints |
| `StemmerProvider` | `StemmingMiddleware` | `before_index`, `before_search` | Stems document fields and query text |
| `SynonymProvider` | `SynonymExpansionMiddleware` | `before_index`, `before_search` | Expands documents and queries with synonyms |
| `VectorProvider` | (built into Whoosh core) | N/A (segment format) | Registered in `VectorRegistry`; resolved at search time from segment metadata |
| `AutocompleteProvider` | (standalone or registry) | N/A | Used directly via `.search()` or via `AutocompleteRegistry` |

### How providers flow through the pipeline

```text
                    ┌──────────────────┐
                    │  PluginManager   │
                    │  .load_plugins() │
                    └────────┬─────────┘
                             │
              ┌──────────────┼──────────────┐
              │              │              │
     ┌────────▼──────┐ ┌────▼─────┐ ┌──────▼────────┐
     │ VectorPlugin  │ │AutoPlugin│ │ Other Plugins │
     │               │ │         │ │               │
     │ VectorRegistry│ │AutoReg. │ │ ProviderReg.  │
     │ .register()   │ │.register│ │ .register()   │
     └───────────────┘ └─────────┘ └───────────────┘

                    ┌──────────────────┐
                    │ MiddlewareChain  │
                    │                  │
                    │ ┌──────────────┐ │
                    │ │ before_index │ │
                    │ │  hooks       │ │
                    │ │  (ordered)   │ │
                    │ └──────┬───────┘ │
                    │        │         │
                    │ ┌──────▼───────┐ │
                    │ │   Writer     │ │
                    │ │  .add_doc()  │ │
                    │ └──────┬───────┘ │
                    │        │         │
                    │ ┌──────▼───────┐ │
                    │ │  on_commit   │ │
                    │ │  hooks       │ │
                    │ └──────────────┘ │
                    └──────────────────┘

                    ┌──────────────────┐
                    │  Searcher         │
                    │                  │
                    │ ┌──────────────┐ │
                    │ │before_search │ │
                    │ │  hooks       │ │
                    │ └──────┬───────┘ │
                    │        │         │
                    │ ┌──────▼───────┐ │
                    │ │   Query      │ │
                    │ │  execution   │ │
                    │ └──────┬───────┘ │
                    │        │         │
                    │ ┌──────▼───────┐ │
                    │ │ after_search │ │
                    │ │  hooks       │ │
                    │ └──────────────┘ │
                    └──────────────────┘
```

### Example: Full provider pipeline

```python
from whoosh.middleware.chain import MiddlewareChain
from whoosh.middleware.wrappers import MiddlewareWriter, MiddlewareSearcher
from whoosh_modern.middleware import (
    StorageMiddleware,
    StemmingMiddleware,
    FileStorageProvider,
)
from whoosh_modern.linguistics.synonyms import (
    SynonymExpansionMiddleware,
    SynonymManager,
)
from whoosh_modern.analysis import get_stemmer

# 1. Build providers
storage = FileStorageProvider("/data/index")
stemmer = get_stemmer("auto", "english")
syn_manager = SynonymManager({"car": ["automobile", "vehicle"]})

# 2. Build middleware chain
chain = MiddlewareChain([
    StorageMiddleware(storage, name="primary"),
    StemmingMiddleware(stemmer=stemmer.stem, fields=["title", "content"]),
    SynonymExpansionMiddleware(syn_manager),
])

# 3. Index with middleware
with MiddlewareWriter(ix.writer(), chain) as writer:
    writer.add_document(title="Car for sale", content="House near beach")
    # StorageMiddleware.before_index() tags context
    # StemmingMiddleware stems "Car" → "car"
    # SynonymExpansionMiddleware expands "Car" → "Car automobile vehicle"
    writer.commit()
    # StorageMiddleware.on_commit() writes checkpoint

# 4. Search with middleware
with MiddlewareSearcher(ix.searcher(), chain) as searcher:
    # StemmingMiddleware stems query "cars" → "car"
    # SynonymExpansionMiddleware expands "car" → "car automobile vehicle"
    results = searcher.search("cars")
```

### Key insight: middleware as provider adapter

Middleware acts as the **adapter** between providers and Whoosh's core pipeline:

```text
Provider (domain logic)
    │
    ▼
Middleware (pipeline integration)
    │
    ▼
Whoosh core (generic engine)
```

- **StorageProvider** → **StorageMiddleware** → Whoosh writer/searcher
- **StemmerProvider** → **StemmingMiddleware** → Whoosh document/query
- **SynonymProvider** → **SynonymExpansionMiddleware** → Whoosh text fields

This separation keeps providers simple (single responsibility) while middleware
handles the lifecycle integration (when and how to apply the provider).

## Modern Middleware (Whoosh-NG 2.0)

Whoosh-NG 2.0 adds a modern middleware package (`whoosh_modern.middleware`) with wrap-style resilience middleware (retry, caching, logging) and hook-based middleware for storage, search, and analysis. For full details on the modern middleware architecture, plugin integration, and deployment, see the [Middleware & Plugin Pipeline Guide](middleware-pipeline.md).


## DOCUMENT: Modern Indexing

# Modern Indexing API

Whoosh-NG provides an optimized indexing layer in `whoosh_modern.indexing` for high-throughput document ingestion. These utilities wrap the core Whoosh writer without modifying the library internals.

## BatchIndexWriter

`BatchIndexWriter` wraps a core Whoosh writer with optimizations for batch processing of large datasets.

### Key Optimizations

- Pre-computes schema field names for fast filtering (O(1) per-field validation)
- Skips fields not in the schema (avoiding per-document overhead)
- Uses `multisegment=True` to defer merging during indexing
- Supports configurable batch commits to reduce I/O pressure
- Accepts a callback for post-commit hooks

### Basic Usage

```python
from whoosh_modern.indexing import BatchIndexWriter
from whoosh import index

ix = index.open_dir("indexdir")

writer = BatchIndexWriter(ix, batch_size=5000, commit_every=10)

for batch in source.stream_batches(batch_size=5000):
    writer.add_batch(batch)

writer.close()
```

### Context Manager

```python
with BatchIndexWriter(ix, batch_size=10000) as writer:
    for doc in documents:
        writer.add_document(doc)
```

### Parameters

| Parameter | Default | Description |
|-----------|---------|-------------|
| `batch_size` | 5000 | Number of documents per batch |
| `limitmb` | 512 | Memory limit for the writer (MB) |
| `commit_every` | None | Commit after N batches (None = no auto-commit during indexing) |
| `multisegment` | True | Use multisegment mode to defer merging |
| `callback` | None | Callback invoked after each commit |
| `**writer_kwargs` | None | Additional keyword args passed to `index.writer()` |

### With Commit Profiler

```python
from whoosh_modern.indexing import BatchIndexWriter
from whoosh_modern.profiling import CommitProfilerV2

profiler = CommitProfilerV2()
with BatchIndexWriter(ix, batch_size=5000, commit_every=5, commit_profiler=profiler) as writer:
    for batch in source.stream_batches(batch_size=5000):
        writer.add_batch(batch)

print(profiler.report())
```

---

## AnalyzerCache

`AnalyzerCache` provides an LRU cache for analyzer results, avoiding redundant analysis work on repeated field values.

### Basic Usage

```python
from whoosh_modern.indexing import BatchIndexWriter
from whoosh_modern.profiling import AnalyzerCache

cache = AnalyzerCache(maxsize=50000)
analyzer = StandardAnalyzer()

for doc in docs:
    cache_key = f"title:{doc['title']}"
    tokens = cache.get(cache_key)
    if tokens is None:
        tokens = list(analyzer(doc['title']))
        cache.put(cache_key, tokens)
```

### With get_or_compute

```python
from whoosh_modern.profiling import AnalyzerCache

cache = AnalyzerCache(maxsize=50000)
analyzer = StandardAnalyzer()

for doc in docs:
    tokens = cache.get_or_compute(
        f"title:{doc['title']}",
        lambda: list(analyzer(doc['title']))
    )
```

### Cache Statistics

```python
cache = AnalyzerCache(maxsize=50000)
# ... use cache ...

print(f"Hit rate: {cache.hit_rate:.1%}")
print(f"Size: {cache.size}/{cache.maxsize}")
print(cache.report())
# Analyzer Cache Report
# ==================================================
#   Size: 4823/50000
#   Hits: 12543
#   Misses: 3421
#   Hit rate: 78.6%
```

### Sizing from Profiling Data

```python
from whoosh_modern.profiling import AnalyzerCache, CacheAnalyzer

analyzer = CacheAnalyzer()
analysis = analyzer.analyze(source.iter_documents())

cache = AnalyzerCache.from_profiling(analysis.to_dict())
# Creates an optimally sized cache based on field repetition ratios
```

---

## FieldAnalyzerCache

`FieldAnalyzerCache` wraps an analyzer and caches results per field, automatically generating cache keys from field name and value.

### Basic Usage

```python
from whoosh_modern.profiling import FieldAnalyzerCache

field_cache = FieldAnalyzerCache(
    analyzer=StandardAnalyzer(),
    fields=["Country", "City"],
    cache_size=50000,
)

for doc in docs:
    for field in ["Country", "City"]:
        tokens = field_cache.analyze(field, doc[field])
```

### Invalidating Cache Entries

```python
# Invalidate a specific entry
field_cache.invalidate("Country", "USA")

# Clear entire cache
field_cache.clear()
```

### Cache Statistics

```python
print(f"Hit rate: {field_cache.hit_rate:.1%}")
print(field_cache.report())
# Field Analyzer Cache Report
# ==================================================
#   Fields: ['City', 'Country']
#   Cache size: 4823/50000
#   Hit rate: 96.5%
#   Hits: 12543
#   Misses: 3421
```

---

## Available Data Sources

| Class | Type | Dependencies |
|-------|------|-------------|
| `SQLSource` | SQLite, PostgreSQL, MySQL | `sqlite3` (stdlib) |
| `SQLAlchemySource` | Any SQLAlchemy-supported DB | `sqlalchemy` |
| `RESTSource` | REST APIs | none (stdlib `urllib`) |
| `GraphQLSource` | GraphQL APIs | none (stdlib `urllib`) |
| `FastCSVSource` | CSV files | none |
| `JSONSource` | JSON/JSONL files | none |
| `ParquetSource` | Parquet files | `pyarrow` or `pandas` |
| `PandasSource` | pandas DataFrames | `pandas` |
| `PolarsSource` | Polars DataFrames | `polars` |
| `PeeweeSource` | Peewee ORM | `peewee` |
| `TortoiseSource` | Tortoise ORM | `tortoise-orm` |
| `PydanticSource` | Pydantic models | `pydantic` |


## DOCUMENT: Monitoring

# Monitoring

Whoosh-NG ships with hooks for observability and a Prometheus plugin for production use.

## Built-in Metrics

```python
from whoosh.middleware import MetricsMiddleware, MiddlewareChain
from whoosh.middleware.integration import apply_middleware_to_writer, apply_middleware_to_searcher

chain = MiddlewareChain([MetricsMiddleware()])
writer = apply_middleware_to_writer(ix.writer(), chain.middlewares)
searcher = apply_middleware_to_searcher(ix.searcher(), chain.middlewares)

metrics = chain.get_metrics()
print(metrics)
```

## Prometheus

```bash
pip install whoosh-ng[metrics]
```

| Metric | Type | Description |
|--------|------|-------------|
| `whoosh_documents_indexed_total` | Counter | Total documents indexed |
| `whoosh_searches_executed_total` | Counter | Total searches executed |
| `whoosh_indexing_duration_seconds` | Histogram | Indexing latency |
| `whoosh_search_duration_seconds` | Histogram | Search latency |
| `whoosh_index_size_bytes` | Gauge | Current index size |

## Best practices

1. Add `MetricsMiddleware` early in your base chain.
2. Expose `/metrics` in production.
3. Use `/health` for load balancer health checks.
4. Emit `DocumentIndexed` and `SearchExecuted` events.


## DOCUMENT: Performance

# Performance Benchmarking

Whoosh-NG includes a comprehensive benchmarking toolkit in `whoosh_modern.profiling` for measuring and comparing analyzer performance. This guide explains how to use these tools and documents the optimizations shipped in 2.0.0.

## Quick Start

```python
from whoosh_modern.profiling.benchmarks.regex_tokenizer import run_p5_1
from whoosh_modern.profiling.benchmarks.token_optimization import run_p5_2
from whoosh_modern.profiling.synthetic_datasets import SyntheticDatasetGenerator
from whoosh_modern.profiling.stemmer_benchmark import StemmerBenchmark

# Generate synthetic datasets for consistent benchmarks
gen = SyntheticDatasetGenerator(seed=42)
datasets = gen.generate_all(count=5000)

# Run tokenizer benchmark (P5.1)
run_p5_1(datasets)

# Run token creation benchmark (P5.2)
run_p5_2(token_count=100_000)

# Run stemmer benchmark
bench = StemmerBenchmark()
bench.run(gen.generate_dataset("A", 5000))
print(bench.report())
```

## Benchmarking Tools

### SyntheticDatasetGenerator

Generates deterministic text datasets of varying complexity:

```python
from whoosh_modern.profiling.synthetic_datasets import SyntheticDatasetGenerator

gen = SyntheticDatasetGenerator(seed=42)
datasets = gen.generate_all(count=5000)

# Dataset A: 2 tokens/doc (short)
# Dataset B: 50 tokens/doc (medium)
# Dataset C: 500 tokens/doc (large)
# Dataset D: 1200 tokens/doc (very large)
for name, texts in datasets.items():
    print(f"{name}: {len(texts)} documents")
```

### P5.1: RegexTokenizer Benchmark

Compares different tokenizer implementations:

```python
from whoosh_modern.profiling.benchmarks.regex_tokenizer import run_p5_1

results = run_p5_1(datasets)

# Compare:
# - Current Regex (whoosh default)
# - Compiled Global regex
# - Manual Python tokenizer
# - C extension (re2, if available)
```

### P5.2: Token Optimization Benchmark

Compares Token object implementations:

```python
from whoosh_modern.profiling.benchmarks.token_optimization import run_p5_2

# Compare:
# - Current Token (dict-based)
# - __slots__ optimization
# - namedtuple
# - dataclass(slots=True)
results = run_p5_2(token_count=100_000)
```

### StemmerBenchmark

Compares stemmer backends:

```python
from whoosh_modern.profiling.stemmer_benchmark import StemmerBenchmark

bench = StemmerBenchmark()
bench.run(texts, warmup=True)
print(bench.report())
# Output:
# Stemmer Benchmark
# ==================================================
# Stemmer         Tokens/s        Time (s)    Tokens
# ------------------------------------------------------
# StemFilter      1,004,172       0.1503      150,983
# PyStemmer       2,100,000+      0.0719+     150,983
```

## Performance Optimizations

### 2.0.0 Performance Summary

| Optimization | Component | Measurable Gain |
|---|---|---|
| `__slots__` on Token | `whoosh.analysis.acore` | +35% token creation |
| Global compiled regex | `RegexTokenizer` | +50% regex throughput |
| Compact postings (1-posting) | `W3TermInfo` / `W3PostingsWriter` | +35% commit speed |
| Compact postings (2-8 postings) | `W3TermInfo` / `W3PostingsWriter` | +35% commit speed |
| Field cache in add_postings | `whoosh.codec.base` | -93% write_block calls |
| Varint position encoding | `whoosh.formats` | reduced per-term overhead |
| Stemmer cache tuning | `whoosh.analysis.morph` | 96.5% hit rate, 4.12x on repetitive fields |
| Analyzer cache | `whoosh_modern.profiling.analyzer_cache` | 4.12x on high-repetition fields |
| Batch writer optimization | `whoosh_modern.indexing.batch_writer` | optimized filtered batches |
| Stopword setdefault optimization | `whoosh.formats` | reduced dict overhead |

### Benchmark Results: 20k Documents (`customers_csv`)

```
before:
  commit total      : 18.653s
  analyzing         : 8.641s  (51.5%)
  committing        : 10.012s (27.1%)
  write_postings    : 6.5s
  write_block calls : ~72612

after:
  commit total      : 6.806s   (-63.5%)
  analyzing         : ~2.7s    (-68%)
  committing        : 6.806s   (-32%)
  write_postings    : 6.5s -> reduced allocation
  write_block calls : 7565     (-93%)
  throughput        : 1275 docs/s
```

### Benchmark Results: Stemmer Backends (1.5M tokens)

| Stemmer | Throughput | Relative |
|---|---|---|
| StemFilter (internal) | 1,004,172 tokens/s | 1.0x |
| PyStemmer | ~2,100,000 tokens/s | ~2.1x |

### Benchmark Results: Regex Tokenizer

| Tokenizer | Throughput | Relative |
|---|---|---|
| Current regex | ~1,000,000 tokens/s | 1.0x |
| Compiled global | ~2,300,000 tokens/s | 2.3x |

### Benchmark Results: Token Object

| Implementation | Tokens/sec | Relative |
|---|---|---|
| Current (dict) | 1,000,000 | 1.0x |
| `__slots__` | ~1,350,000 | 1.35x |

## Profiling Tools

### IndexingPipelineProfiler

Profiles the complete indexing pipeline:

```python
from whoosh_modern.profiling.indexing_pipeline_profiler import IndexingPipelineProfiler

profiler = IndexingPipelineProfiler()
for doc in documents:
    profiler.before_tokenize(doc, analyzer)
    analyzer(doc)
    profiler.after_tokenize()

report = profiler.report()
print(report)
```

### CommitProfiler

Profiles commit performance including field writing and posting flushing:

```python
from whoosh_modern.profiling.commit_profiler_v2 import CommitProfiler

profiler = CommitProfiler()
# ... index documents ...
ix.commit()

report = profiler.report()
# Shows: analyze, convert_fields, write_postings, flush, commit breakdown
```

### FieldIndexProfiler

Profiles field conversion costs:

```python
from whoosh_modern.profiling.field_index_profiler import FieldIndexProfiler

profiler = FieldIndexProfiler()
# ... index documents ...
report = profiler.report()
# Identifies expensive field types and conversion bottlenecks
```

### IndexQualityAnalyzer

Analyzes index quality metrics including singleton terms:

```python
from whoosh_modern.profiling.index_quality_analyzer import IndexQualityAnalyzer

analyzer = IndexQualityAnalyzer(index_reader)
report = analyzer.analyze()
print(f"Singleton terms: {report['singleton_terms']}/{report['total_terms']} ({report['singleton_percent']}%)")
```

## Stemmer Provider System

Whoosh-NG provides a pluggable stemmer provider system:

```python
from whoosh_modern.analysis import get_stemmer, StemmingAnalyzer, list_available_backends

# Check available backends
print(list_available_backends())
# {'internal': 'available', 'pystemmer': 'not installed'}

# Use auto-detection (default)
analyzer = StemmingAnalyzer(stemmer="auto")

# Explicit internal stemmer
analyzer = StemmingAnalyzer(stemmer="internal")

# PyStemmer (requires: pip install whoosh-ng[fast-stemming])
analyzer = StemmingAnalyzer(stemmer="pystemmer")

# Custom stemmer provider
from whoosh_modern.analysis.stemmer_providers import StemmerProvider

class MyStemmer(StemmerProvider):
    def __init__(self, language="english"):
        self._lang = language

    def stem(self, word):
        return word.lower()

    @property
    def name(self):
        return "my_stemmer"

    @property
    def language(self):
        return self._lang

analyzer = StemmingAnalyzer(stemmer=MyStemmer())
```

## Performance Recommendations

1. **Use `StemmingAnalyzer`** from `whoosh_modern.analysis` for automatic PyStemmer selection
2. **Enable stemmer cache** for repetitive content (`cachesize=50000` by default)
3. **Minimize TEXT fields** — use KEYWORD or ID for low-cardinality fields
4. **Avoid stored positions/chars** unless highlighting requires them
5. **Use batch indexing** with larger segments for better throughput
6. **Monitor singleton terms** — reduce rare terms via stopword lists

## Running the Full Benchmark Suite

```bash
cd whoosh-ng

# Run all P5 benchmarks
uv run python -m pytest tests/test_regex_tokenizer_unicode.py tests/test_token_slots.py tests/test_stemmer_compatibility.py -v

# Run full test suite
uv run python -m pytest -q
```


## DOCUMENT: Plugins Advanced

# Plugin System

Module: `whoosh.plugins.manager`
Version: 2.0.0

Whoosh-NG's plugin architecture enables external packages to extend the core indexing, search, and analysis pipeline. Plugins are discovered via Python [entry points](https://docs.python.org/3/library/importlib.metadata.html#entry-points) declared in `pyproject.toml` and managed by the `PluginManager`.

## Architecture Overview

```text
PluginManager (singleton)
    ├── load_plugins(group)          # Auto-discover from entry points
    ├── register(plugin)             # Manual registration
    ├── enable(name) / disable(name) # Toggle lifecycle
    ├── get(name) / list_plugins()   # Inspection
    ├── get_middleware_chain()       # Build MiddlewareChain from plugin middleware
    ├── register_datasource()        # Register a datasource provider
    ├── register_vector_provider()   # Register a vector provider
    ├── register_middleware()        # Register a middleware instance
    ├── register_embedding()         # Register an embedding provider
    ├── register_analyzer()          # Register a named analyzer
    └── register_query_rewriter()    # Register a query rewriter
```

## Plugin Base Classes

### Plugin (ABC)

The root plugin class. Subclasses set class-level attributes and implement `register()`.

```python
from whoosh.plugins.manager import Plugin, PluginMetadata

class MyPlugin(Plugin):
    name = "my_plugin"
    version = "1.0.0"

    def register(self, manager: PluginManager) -> None:
        """Called when the plugin is loaded; register providers here."""
        manager.register_middleware("my_module.MyMiddleware", MyMiddleware())

    def register_hooks(self) -> None:
        """Register event hooks (optional)."""
        from whoosh.hooks import hookimpl, register_hook

        @hookimpl
        def on_search(request, response):
            pass
        register_hook("on_search", hookimpl(on_search))
```

### AnalyzerPlugin

For plugins that provide custom tokenizers/analyzers:

```python
from whoosh.plugins.manager import AnalyzerPlugin

class MyAnalyzerPlugin(AnalyzerPlugin):
    name = "my_analyzer"

    def register(self, manager):
        manager.register_analyzer("my_analyzer", MyTokenizer())
```

### QueryRewritePlugin

For plugins that transform queries before execution:

```python
from whoosh.plugins.manager import QueryRewritePlugin

class SynonymRewriterPlugin(QueryRewritePlugin):
    name = "synonym_rewriter"

    def rewrite(self, query, searcher):
        # Return modified query
        return query
```

## PluginMetadata

A dataclass describing plugin metadata:

| Field         | Type              | Description                            |
|---------------|-------------------|----------------------------------------|
| `name`        | `str`             | Unique plugin name                     |
| `version`     | `str`             | SemVer version string                  |
| `depends_on`  | `list[str]`       | Names of required plugins              |
| `priority`    | `int`             | Load ordering priority (higher = later)|
| `middleware`  | `list[str]`       | Dotted paths to middleware classes     |

## Entry Point Groups

The `PluginManager` discovers plugins from these standard entry-point groups:

| Group                | Purpose                              |
|----------------------|--------------------------------------|
| `whoosh.plugins`     | General plugins                      |
| `whoosh.datasources` | Data source providers                |
| `whoosh.vector.providers` | Vector similarity providers     |
| `whoosh.middlewares` | Middleware classes                    |
| `whoosh.embeddings`  | Embedding model providers            |
| `whoosh.language`    | Language-specific analyzers          |
| `whoosh.apps`        | App factories (FastAPI, admin, etc.) |

## Creating and Deploying a Plugin

### Step 1: Define the Plugin Class

```python
# my_plugin/plugin.py
from whoosh.plugins.manager import Plugin
from whoosh.registry import VectorRegistry

class MyVectorPlugin(Plugin):
    name = "my_vector"
    version = "1.0.0"
    depends_on = []
    conflicts_with = []
    priority = 0
    middleware = []

    def register(self, manager):
        """Register a vector provider with the VectorRegistry."""
        provider = MyCustomVectorProvider()
        VectorRegistry.register("my_vector", provider, owner=self.name)

    def register_hooks(self):
        """Register optional hooks (e.g., on_search, on_index)."""
        pass
```

### Step 2: Declare the Entry Point

In your `pyproject.toml`:

```toml
[project]
name = "whoosh-ng-my-vector"
version = "1.0.0"
dependencies = ["whoosh-ng>=2.0"]

[project.entry-points."whoosh_ng.plugins"]
my_vector = "my_plugin.plugin:MyVectorPlugin"
```

### Step 3: Install and Verify

```bash
pip install -e .
```

```python
# Verify the plugin is registered
from whoosh.plugins.manager import PluginManager

PluginManager.load_plugins()  # Auto-discovers all entry points

manager = PluginManager._default
print(manager.list_plugins())
# ['whoosh_autocomplete', 'whoosh_vector', ..., 'my_vector']

# Check the registry
from whoosh.registry import VectorRegistry
print(VectorRegistry.list_keys())
# ['my_vector', 'numpy']
```

## Manual Registration (No Entry Point)

For testing or programmatic use:

```python
from whoosh.plugins.manager import PluginManager

manager = PluginManager()
manager.register(MyVectorPlugin())
manager.enable("my_vector")
```

## Plugin Lifecycle

```
1. Entry point discovered  ───►  2. register() called  ───►  3. register_hooks()
   │                               │                            │
   └── load_plugins(group)          └── register provider/     └── register_hook()
                                      middleware/analyzer
```

### Enabling / Disabling

```python
from whoosh.plugins.manager import PluginManager

manager = PluginManager._default

manager.enable("my_vector")    # Activate a plugin
manager.disable("my_vector")   # Deactivate a plugin
print(manager.list_enabled())  # Only enabled plugins
```

### Version Validation

```python
# Check if a plugin meets a minimum version
ok = manager.validate_version("my_vector", "1.0.0")
print(ok)  # True if plugin version >= 1.0.0
```

### Conflict Detection

```python
# Check if two plugins conflict
if manager.detect_conflicts("plugin_a", "plugin_b"):
    print("These plugins cannot be loaded together")
```

## Plugin Manager API Reference

### `PluginManager.load_plugins(group=None)`

Load all plugins from entry-point groups. If `group` is `None`, loads from all standard groups (`STANDARD_GROUPS`).

### `PluginManager.register(plugin)`

Register a plugin instance. Calls `plugin.register(self)` and `plugin.register_hooks()`. Supports async `register()` via `asyncio`.

### `PluginManager.get_middleware_chain()`

Builds and returns a `MiddlewareChain` from all plugins that declare `middleware` entries. Middleware classes are imported and instantiated by dotted path.

### Registry Registration Methods

| Method                       | Description                          |
|------------------------------|--------------------------------------|
| `register_analyzer(name, analyzer)` | Register a named analyzer   |
| `register_datasource(name, datasource)` | Register a datasource  |
| `register_vector_provider(name, provider)` | Register a vector provider |
| `register_middleware(name, middleware)` | Register a middleware instance |
| `register_embedding(name, embedding)` | Register an embedding provider |
| `register_query_rewriter(plugin)` | Register a query rewriter plugin |

### Lookup Methods

| Method                       | Returns                          |
|------------------------------|----------------------------------|
| `get(name)`                  | `Plugin` instance                |
| `list_plugins()`             | All registered plugin names      |
| `list_enabled()`             | Enabled plugin names             |
| `get_analyzer(name)`         | Analyzer callable                |
| `list_analyzers()`           | Registered analyzer names        |
| `list_datasources()`         | Registered datasource names      |
| `list_vector_providers()`    | Registered vector provider names |
| `list_middlewares()`         | Registered middleware names      |
| `list_embeddings()`          | Registered embedding names       |
| `list_query_rewriters()`     | Registered query rewriter names  |

## Built-in Plugins

| Plugin            | Module                  | Entry-point Group       |
|-------------------|-------------------------|-------------------------|
| `whoosh_autocomplete` | `whoosh_modern.autocomplete.plugin` | `whoosh.plugins` |
| `whoosh_vector`   | `whoosh_modern.vector.plugin`      | `whoosh.plugins` |
| `whoosh_fastapi`  | `whoosh_fastapi`                  | `whoosh.apps` |
| `whoosh_observability` | `whoosh.middleware.metrics`  | `whoosh.middlewares` |
| `whoosh_admin`    | `whoosh_admin`                   | `whoosh.apps` |

## Best Practices

1. **Single responsibility**: One plugin, one feature
2. **Declare dependencies**: Use `depends_on` for required plugins
3. **Semantic versioning**: Increment version for API changes
4. **Graceful degradation**: Check for optional dependencies in `register()`
5. **Avoid side effects in `__init__`**: All setup in `register()`
6. **Clean up**: If applicable, provide teardown logic

## See Also

- [Middleware Guide](middleware-pipeline.md) — Pipeline hooks and custom middleware
- [Provider Integration Guide](provider-integration.md) — Complete pipeline guide for all providers
- [Plugin Development Example](../examples/plugin-dev.md) — Step-by-step plugin tutorial
- [API: Plugins](../api/plugins.md) — Full API reference


## DOCUMENT: Plugins

# Plugins

Whoosh-NG uses a plugin architecture to keep the core lightweight while enabling advanced features. Plugins are loaded via entry points and managed by the `PluginManager`.

## Plugin Architecture

```text
PluginManager
    ├── load_plugins()     # Auto-discover from entry points
    ├── register(name, plugin)  # Manual registration
    ├── enable(name)       # Enable a plugin
    ├── disable(name)      # Disable a plugin
    ├── get(name)          # Retrieve a plugin
    └── list_plugins()     # List all plugins
```

## Built-in Plugins

| Plugin | Package | Description |
|--------|---------|-------------|
| whoosh-ng-vector | `whoosh_modern.vector` | Vector search providers (NumPy, HNSW, Faiss) |
| whoosh-ng-autocomplete | `whoosh_modern.autocomplete` | Edge ngram autocomplete |
| whoosh-ng-fastapi | `whoosh_fastapi` | FastAPI app factory |
| whoosh-ng-observability | `whoosh_observability` | Prometheus metrics |
| whoosh-ng-admin | `whoosh_admin` | Admin UI |

## Creating a Plugin

Every plugin is a subclass of `BasePlugin`:

```python
from whoosh.plugins.base import BasePlugin

class MyPlugin(BasePlugin):
    name = "my_plugin"
    version = "1.0.0"
    dependencies = []

    def setup(self, registry):
        """Called when the plugin is enabled."""
        registry.register("my_provider", MyProvider())

    def teardown(self, registry):
        """Called when the plugin is disabled."""
        registry.unregister("my_provider")

    def middleware(self):
        """Return middleware to inject into the pipeline."""
        return [MyMiddleware()]

    def on_startup(self):
        """Called once at application startup."""
        pass

    def on_shutdown(self):
        """Called once at application shutdown."""
        pass
```

## Plugin Registration

### Via entry_points (pyproject.toml)

```toml
[project.entry-points."whoosh_ng.plugins"]
my_plugin = "my_package.plugin:MyPlugin"
```

### Programmatic

```python
from whoosh.plugins.manager import PluginManager

plugin = MyPlugin()
PluginManager.register("my_plugin", plugin)
PluginManager.enable("my_plugin")
```

## Plugin Lifecycle

```
register() -> setup() -> enable() -> middleware hooks -> teardown() -> disable()
```

## Plugin Dependencies

Plugins can declare dependencies on other plugins:

```python
class VectorPlugin(BasePlugin):
    name = "vector"
    version = "1.0.0"
    dependencies = ["metrics"]  # Requires metrics plugin
```

The `PluginManager` resolves dependency order and detects conflicts.

## Example: Vector Plugin

```python
from whoosh.plugins.base import BasePlugin
from whoosh.vector.base import VectorProvider, VectorField
from whoosh.vector.numpy_provider import NumpyProvider

class VectorPlugin(BasePlugin):
    name = "vector"
    version = "1.0.0"
    dependencies = []

    def setup(self, registry):
        provider = NumpyProvider()
        registry.register("numpy", provider, owner="vector")

    def middleware(self):
        from whoosh.middleware import MetricsMiddleware
        return [MetricsMiddleware()]

    def on_startup(self):
        print("Vector plugin loaded")
```

## Example: FastAPI Plugin

```python
from whoosh.plugins.base import BasePlugin

class FastAPIPlugin(BasePlugin):
    name = "fastapi"
    version = "1.0.0"

    def setup(self, registry):
        self.app = None

    def create_app(self, index, **kwargs):
        from whoosh_fastapi import create_app
        self.app = create_app(index=index, **kwargs)
        return self.app

    def middleware(self):
        from whoosh.middleware import CacheMiddleware
        return [CacheMiddleware()]
```

## Best Practices

1. **Keep plugins small**: One plugin, one responsibility
2. **Declare dependencies**: Help PluginManager resolve load order
3. **Handle conflicts**: Check for existing registrations before adding
4. **Clean up**: Implement `teardown()` to remove registries and middleware
5. **Version your plugin**: Semver for compatibility checking

## Modern Plugin System (Whoosh-NG 2.0)

Whoosh-NG 2.0 introduces an enhanced `PluginManager` with registry support for datasources, vector providers, embeddings, and middleware. For full details on the modern plugin architecture, entry point groups, and deployment, see the [Plugin System Guide](plugins-advanced.md).


## DOCUMENT: Provider Integration

# Provider Integration: Complete Pipeline Guide

Module: `whoosh_modern.storage`, `whoosh_modern.analysis.stemmer_providers`, `whoosh_modern.linguistics.synonyms`, `whoosh_modern.vector`, `whoosh_modern.autocomplete`
Version: 3.0.0

This guide explains how all Whoosh-NG providers integrate into the indexing and
search pipeline. It is the definitive reference for understanding the data flow
from raw documents to search results.

## Overview

Whoosh-NG uses a **provider pattern** to keep the core engine lean while enabling
pluggable behavior for storage, text analysis, vector search, and autocomplete.

```
┌──────────────────────────────────────────────────────────────────────┐
│                        Whoosh-NG Provider Stack                      │
│                                                                      │
│  ┌─────────────┐  ┌──────────────┐  ┌─────────────┐  ┌───────────┐ │
│  │ Storage     │  │ Stemmer      │  │ Synonym     │  │ Vector    │ │
│  │ Providers   │  │ Providers    │  │ Providers   │  │ Providers │ │
│  │             │  │              │  │             │  │           │ │
│  │ FileStorage │  │ Internal     │  │ Static      │  │ Numpy     │ │
│  │ S3Storage   │  │ PyStemmer    │  │ YAML        │  │ HNSW      │ │
│  │ Hybrid      │  │ Identity     │  │ JSON        │  │ Faiss     │ │
│  │ SQLite      │  │ Custom       │  │ SQLite      │  │ Qdrant    │ │
│  └──────┬──────┘  └──────┬──────┘  └──────┬──────┘  └─────┬─────┘ │
│         │                │                │                │       │
│         ▼                ▼                ▼                ▼       │
│  ┌─────────────────────────────────────────────────────────────────┐│
│  │              Middleware Pipeline (hooks)                         ││
│  │  ┌─────────────┐  ┌──────────────┐  ┌─────────────────────┐    ││
│  │  │Storage      │  │Stemming      │  │Synonym              │    ││
│  │  │Middleware   │  │Middleware    │  │ExpansionMiddleware  │    ││
│  │  └─────────────┘  └──────────────┘  └─────────────────────┘    ││
│  └─────────────────────────────────────────────────────────────────┘│
│                              │                                       │
│                              ▼                                       │
│  ┌─────────────────────────────────────────────────────────────────┐│
│  │              Whoosh Core Engine                                  ││
│  │  Index │ Writer │ Searcher │ QueryParser │ Segment files         ││
│  └─────────────────────────────────────────────────────────────────┘│
└──────────────────────────────────────────────────────────────────────┘
```

## Complete Indexing Pipeline

### Step-by-step flow

```text
┌─────────────────┐
│   DataSource    │  (SQL, JSON, REST, CSV, DataFrame, etc.)
│   .stream_batches() │
└────────┬────────┘
         │ batches of documents
         ▼
┌─────────────────┐
│  SchemaDiscovery │  Infers Whoosh Schema from data source columns
│  .discover_schema() │
└────────┬────────┘
         │ Schema(TEXT, ID, NUMERIC, VECTOR, ...)
         ▼
┌─────────────────────────────────────────┐
│  Storage Provider Resolution             │
│                                         │
│  storage._root (if any)                 │
│    └──► whoosh.index.create_in(root)    │
│  No root                                 │
│    └──► tempfile.mkdtemp() → create_in() │
└────────┬────────────────────────────────┘
         │ Index instance
         ▼
┌─────────────────────────────────────────┐
│  Writer + MiddlewareChain                │
│                                         │
│  chain.run_before("before_index")        │
│    ├── StorageMiddleware                 │
│    │   └── tags context with provider    │
│    ├── StemmingMiddleware                │
│    │   └── stems document fields         │
│    └── SynonymExpansionMiddleware        │
│        └── expands fields with synonyms  │
│                                         │
│  writer.add_document(**doc)              │
│    └── Whoosh core applies field         │
│        analyzers (TEXT.analyzer)          │
│        and writes to segment             │
│                                         │
│  writer.commit()                         │
│    └── chain.run_after("on_commit")      │
│        └── StorageMiddleware             │
│            └── writes commit checkpoint  │
└────────┬────────────────────────────────┘
         │ Segment files on disk/S3/cache
         ▼
┌─────────────────┐
│  Whoosh Index    │
│  (segments)      │
└─────────────────┘
```

### Concrete example

```python
from whoosh import index, fields
from whoosh_modern import (
    SearchApplication,
    SQLSource,
    HybridStorage,
    S3Storage,
    StemmingAnalyzer,
    get_stemmer,
    SynonymManager,
    SynonymExpansionMiddleware,
    StorageMiddleware,
    StemmingMiddleware,
)
from sqlalchemy import create_engine

# 1. Data source
engine = create_engine("sqlite:///products.db")
source = SQLSource(query="SELECT id, name, description FROM products", connection=engine)

# 2. Storage
remote = S3Storage(bucket="my-index-bucket", prefix="segments")
storage = HybridStorage(local_cache="./cache", remote=remote)

# 3. Schema (auto-discovered from SQL columns)
#    But we customize the analyzer
stemmer = get_stemmer("auto", "english")
schema = fields.Schema(
    name=fields.TEXT(analyzer=StemmingAnalyzer(stemmer=stemmer), stored=True),
    description=fields.TEXT(analyzer=StemmingAnalyzer(stemmer=stemmer)),
    id=fields.ID(stored=True, unique=True),
)

# 4. Create index in storage root
ix = index.create_in(storage._cache_root, schema)

# 5. Build middleware chain
syn_manager = SynonymManager({"laptop": ["notebook", "portable"]})
chain = MiddlewareChain([
    StorageMiddleware(storage, name="products"),
    StemmingMiddleware(stemmer=stemmer.stem),
    SynonymExpansionMiddleware(syn_manager),
])

# 6. Index with middleware
with MiddlewareWriter(ix.writer(), chain) as writer:
    for batch in source.stream_batches():
        for doc in batch:
            writer.add_document(**doc)
    writer.commit()
```

## Complete Search Pipeline

### Step-by-step flow

```text
┌─────────────────┐
│   User Query    │  "running cats"
└────────┬────────┘
         │
         ▼
┌─────────────────────────────────────────┐
│  MiddlewareChain.run_before("search")    │
│                                         │
│  ├── StemmingMiddleware                  │
│  │   └── "running cats" → "run cat"      │
│  ├── SynonymExpansionMiddleware          │
│  │   └── "run cat" → "run cat running feline" │
│  └── QueryRewriteMiddleware              │
│      └── custom rewrites                 │
└────────┬────────────────────────────────┘
         │ Modified query
         ▼
┌─────────────────────────────────────────┐
│  QueryParser.parse(query)                │
│    └── Query object (Term, And, Or...)  │
└────────┬────────────────────────────────┘
         │ Query object
         ▼
┌─────────────────────────────────────────┐
│  Searcher.search(query)                  │
│                                         │
│  ┌───────────────────────────────────┐  │
│  │  Keyword search path              │  │
│  │  └── reads posting lists from     │  │
│  │      segment files                │  │
│  └───────────────────────────────────┘  │
│                                         │
│  ┌───────────────────────────────────┐  │
│  │  Vector search path (if VECTOR)   │  │
│  │  └── VectorRegistry.get(provider) │  │
│  │      └── NumpyProvider.search()   │  │
│  │          └── cosine similarity    │  │
│  └───────────────────────────────────┘  │
│                                         │
│  ┌───────────────────────────────────┐  │
│  │  Autocomplete path                │  │
│  │  └── AutocompleteRegistry.get()   │  │
│  │      └── provider.suggest()       │  │
│  └───────────────────────────────────┘  │
└────────┬────────────────────────────────┘
         │ Raw Results
         ▼
┌─────────────────────────────────────────┐
│  MiddlewareChain.run_after("search")     │
│                                         │
│  └── RankingMiddleware                   │
│      └── re-sorts results                │
└────────┬────────────────────────────────┘
         │ Final Results
         ▼
┌─────────────────┐
│  Hits returned   │
└─────────────────┘
```

### Concrete example

```python
from whoosh.qparser import QueryParser
from whoosh_modern.middleware import (
    StemmingMiddleware,
    RankingMiddleware,
    QueryRewriteMiddleware,
)
from whoosh_modern.analysis import get_stemmer
from whoosh_modern.vector import NumpyProvider
from whoosh_modern.vector.plugin import VectorPlugin
from whoosh.plugins.manager import PluginManager
import numpy as np

# 1. Setup plugins at startup
manager = PluginManager()
VectorPlugin().register(manager)

# 2. Open index
ix = index.open_dir("indexdir")

# 3. Build middleware chain
stemmer = get_stemmer("auto", "english")
chain = MiddlewareChain([
    StemmingMiddleware(stemmer=stemmer.stem),
    QueryRewriteMiddleware(rewriter=lambda q: q + " portable"),  # add synonym
    RankingMiddleware(ranker=lambda r: sorted(r, key=lambda h: h.score, reverse=True)),
])

# 4. Search with middleware
with MiddlewareSearcher(ix.searcher(), chain) as searcher:
    # Query is transformed by middleware before execution
    results = searcher.search("laptop")
    for hit in results:
        print(f"{hit['name']}: {hit.score:.4f}")

    # 5. Vector search (parallel)
    query_vec = np.random.rand(384).tolist()
    vector_results = searcher.vector_search("embedding", query_vec, limit=10)
    for hit in vector_results:
        print(f"doc_id={hit.doc_id}, score={hit.score:.4f}")
```

## Provider Comparison Matrix

| Aspect | Storage | Stemmer | Synonym | Vector | Autocomplete |
|--------|---------|---------|---------|--------|--------------|
| **Integration point** | `StorageMiddleware` + `create_in()` | `StemmingAnalyzer` (field) + `StemmingMiddleware` | `SynonymExpansionMiddleware` | `VectorRegistry` + segment format | `AutocompleteRegistry` + standalone |
| **Registration** | Manual or `__getattr__` | `register_stemmer()` decorator | `SynonymManager` CRUD | `VectorPlugin.register()` | `AutocompletePlugin.register()` |
| **Used at index time** | Yes (commit checkpoints) | Yes (field analyzer + middleware) | Yes (before_index) | Yes (VECTOR field) | No (standalone or post-index) |
| **Used at search time** | Yes (segment reads via filesystem) | Yes (field analyzer + middleware) | Yes (before_search) | Yes (vector_search) | Yes (suggest/search) |
| **Persistence** | Segment files / S3 / SQLite | In-memory (stateless) | In-memory / YAML / JSON / SQLite | Segment files (metadata) | In-memory (phrase list) |
| **Configuration** | Provider class + kwargs | Backend name + language | Mapping dict or file | Provider name + metric | Provider type + params |

## Common Patterns

### Pattern 1: Provider as Field Analyzer

Used by: Stemmer providers, language analyzers

```python
schema = Schema(
    content=TEXT(analyzer=StemmingAnalyzer(stemmer="auto"))
)
```

The provider is wrapped in a Whoosh analyzer and applied automatically.

### Pattern 2: Provider as Middleware

Used by: Storage, Stemmer, Synonyms

```python
chain = MiddlewareChain([
    StorageMiddleware(storage),
    StemmingMiddleware(stemmer=stemmer.stem),
    SynonymExpansionMiddleware(manager),
])
```

The provider is consumed by middleware hooks in the pipeline.

### Pattern 3: Provider as Registry Entry

Used by: Vector, Autocomplete

```python
VectorRegistry.register("numpy", NumpyProvider(), owner="my_app")
provider = VectorRegistry.get("numpy", "my_app")
```

The provider is stored in a global registry and resolved by name at runtime.

### Pattern 4: Provider as Standalone Service

Used by: Autocomplete, Vector (manual mode)

```python
provider = NumpyProvider()
provider.add([(doc_id, vec)])
results = provider.search(query_vec)
```

The provider operates independently of Whoosh's index/searcher.

## Best Practices

1. **Choose the right integration pattern**: Field analyzers for static schemas, middleware for dynamic behavior, registry for pluggable backends.
2. **Avoid double-application**: Don't use both field-level analyzers and middleware for the same transformation (e.g., stemming).
3. **Register providers at startup**: Call `VectorPlugin().register(manager)` and `AutocompletePlugin().register(manager)` before creating indexes.
4. **Use the highest-level API when possible**: `SearchApplication` for end-to-end, `create_autocomplete()` for suggestions, `get_stemmer()` for stemming.
5. **Keep providers stateless**: Providers should not hold index-specific state; use middleware context for per-request data.
6. **Test providers in isolation**: Each provider should be testable without Whoosh core (unit tests for `provider.search()`, `provider.add()`).
7. **Document provider dependencies**: Note optional dependencies (boto3, PyStemmer, PyYAML) in your project's requirements.

## See Also

- [Storage Providers Guide](storage-providers.md) — Storage backend integration
- [Stemming Guide](../core/stemming.md) — Stemmer provider integration
- [Vector Search Guide](vector.md) — Vector provider integration
- [Autocomplete Guide](autocomplete.md) — Autocomplete provider integration
- [Middleware Guide](middleware-pipeline.md) — Pipeline hooks and provider adapters
- [Plugins Guide](plugins-advanced.md) — Plugin registration and entry points
- [API: Modern](../api/modern.md) — Full API reference for all providers


## DOCUMENT: Stemming Providers

# Stemmer Providers

Module: `whoosh_modern.analysis.stemmer_providers`, `whoosh_modern.analysis.stemming_analyzer`, `whoosh_modern.linguistics.stemmers`
Version: 3.0.0

The stemmer provider system gives you flexible control over which stemming backend is used for text analysis. It supports auto-detection, explicit backend selection, and custom stemmer registration—all with a clean plugin-style API.

## Module Overview

```text
whoosh_modern.analysis
    ├── stemmer_providers.py   # StemmerProvider protocol, Internal/PyStemmer backends, register_stemmer, get_stemmer
    └── stemming_analyzer.py   # Enhanced StemmingAnalyzer with plugin support

whoosh_modern.linguistics.stemmers
    └── __init__.py            # Language-specific analyzers (FR/EN/DE/ES/IT)
```

## StemmerProvider Protocol

Located in `whoosh_modern.analysis.stemmer_providers`:

```python
from whoosh_modern.analysis.stemmer_providers import StemmerProvider

class MyStemmer(StemmerProvider):
    def stem(self, word: str) -> str:
        """Stem a single word."""
        ...

    @property
    def name(self) -> str:
        """Return the stemmer name."""
        return "my_stemmer"

    @property
    def language(self) -> str:
        """Return the language code."""
        return "english"
```

## Getting a Stemmer

### Auto-Detection (Recommended)

The `get_stemmer("auto", language)` function automatically selects the best available backend:

```python
from whoosh_modern.analysis.stemmer_providers import get_stemmer

# Auto-detect: prefers PyStemmer if installed, falls back to internal
stemmer = get_stemmer("auto", "english")
print(stemmer.stem("running"))  # "run"
print(stemmer.name)             # "pystemmer" or "internal"
```

**Priority order:**
1. **PyStemmer** (fastest, requires `pip install whoosh-ng[fast-stemming]`)
2. **Internal** stemmer (built-in Porter stemmer, always available)

### Explicit Backend Selection

```python
from whoosh_modern.analysis.stemmer_providers import get_stemmer

# Force internal stemmer
stemmer = get_stemmer("internal", "english")

# Force PyStemmer (requires installation)
stemmer = get_stemmer("pystemmer", "english")
```

### List Available Backends

```python
from whoosh_modern.analysis.stemmer_providers import list_available_backends

backends = list_available_backends()
print(backends)
# {'internal': 'available', 'pystemmer': 'available', 'my_custom': 'registered'}
```

| Backend       | Status String    | Requires                          |
|---------------|------------------|-----------------------------------|
| `internal`    | `"available"`    | None (always bundled)             |
| `pystemmer`   | `"available"` / `"not installed"` | `pip install whoosh-ng[fast-stemming]` |
| Custom        | `"registered"`   | Registered via `@register_stemmer` |

## Built-in Stemmer Providers

### InternalStemmerProvider

Wraps Whoosh's built-in Porter stemmer. Always available (no extra dependencies):

```python
from whoosh_modern.analysis.stemmer_providers import InternalStemmerProvider

stemmer = InternalStemmerProvider("english")
print(stemmer.stem("cats"))    # "cat"
print(stemmer.stem("running")) # "run"
```

### PyStemmerProvider

Wraps the `Stemmer` library for high-performance stemming. Supports all Snowball languages:

```python
from whoosh_modern.analysis.stemmer_providers import PyStemmerProvider

# Requires: pip install whoosh-ng[fast-stemming]
stemmer = PyStemmerProvider("english")
print(stemmer.stem("cats"))    # "cat"
```

**Note**: This provider calls `self._stemmer.stemWord(word)` to stem words. Ensure PyStemmer is installed or auto-detection will fall back to the internal stemmer.

### IdentityStemmerProvider

A no-op stemmer for testing or when stemming is not desired:

```python
from whoosh_modern.analysis.stemmer_providers import IdentityStemmerProvider

stemmer = IdentityStemmerProvider()
print(stemmer.stem("anything"))  # "anything"
```

## Registering a Custom Stemmer

Use the `@register_stemmer` decorator:

```python
from whoosh_modern.analysis.stemmer_providers import register_stemmer

@register_stemmer("simple")
class SimpleStemmer:
    def stem(self, word: str) -> str:
        # Simple suffix stripping
        if word.endswith("s") and len(word) > 3:
            return word[:-1]
        return word

    @property
    def name(self) -> str:
        return "simple"

    @property
    def language(self) -> str:
        return "english"

# Now use it
from whoosh_modern.analysis.stemmer_providers import get_stemmer

stemmer = get_stemmer("simple", "english")
print(stemmer.stem("cats"))  # "cat"
```

## StemmingAnalyzer (Enhanced)

Located in `whoosh_modern.analysis.stemming_analyzer`, this is the main entry point for creating language-aware analyzers:

```python
from whoosh_modern.analysis import StemmingAnalyzer

# Auto-detect best stemmer for English
analyzer = StemmingAnalyzer(stemmer="auto", language="english")

# Explicit internal stemmer
analyzer = StemmingAnalyzer(stemmer="internal", language="english")

# PyStemmer backend (if installed)
analyzer = StemmingAnalyzer(stemmer="pystemmer", language="french")

# Custom stemmer provider
analyzer = StemmingAnalyzer(stemmer=my_stemmer_instance)
```

### StemmingAnalyzer Parameters

| Parameter   | Type                          | Default                  | Description                      |
|-------------|-------------------------------|--------------------------|----------------------------------|
| `expression`| Regex pattern                 | default token pattern    | Tokenization regex              |
| `stoplist`  | Iterable of stop words        | `whoosh.analysis.STOP_WORDS` | Stop words to filter         |
| `minsize`   | `int`                         | `2`                      | Minimum token length            |
| `maxsize`   | `int \| None`                 | `None`                   | Maximum token length            |
| `gaps`      | `bool`                        | `False`                  | Split on expression vs. match  |
| `stemmer`   | `str \| StemmerProvider`      | `"auto"`                 | Stemmer backend                 |
| `language`  | `str`                         | `"english"`              | Language code                   |
| `ignore`    | `set[str] \| None`            | `None`                   | Words to skip                   |
| `cachesize` | `int`                         | `50000`                  | Stem cache size                 |

### Using with Field Types

```python
from whoosh_modern.analysis import StemmingAnalyzer
from whoosh.fields import Schema, TEXT

# English stemmer with stop words
en_analyzer = StemmingAnalyzer("auto", language="english")

# French stemmer
fr_analyzer = StemmingAnalyzer("auto", language="french")

schema = Schema(
    title=TEXT(stored=True),
    content_en=TEXT(analyzer=en_analyzer),
    content_fr=TEXT(analyzer=fr_analyzer),
)
```

## Language-Specific Analyzers

Pre-built analyzers for five languages, available in `whoosh_modern.linguistics.stemmers`:

```python
from whoosh_modern.linguistics.stemmers import (
    EnglishAnalyzer,
    FrenchAnalyzer,
    GermanAnalyzer,
    SpanishAnalyzer,
    ItalianAnalyzer,
)

# Each is callable and returns a list of tokens
en = EnglishAnalyzer()
tokens = en("The quick brown foxes")
# tokens are stemmed: ["quick", "brown", "fox"] (stop words like "the" removed)
```

### Available Language Analyzers

| Class             | Language  | Module                              |
|-------------------|-----------|-------------------------------------|
| `EnglishAnalyzer` | English   | `whoosh_modern.linguistics.stemmers` |
| `FrenchAnalyzer`  | French    | `whoosh_modern.linguistics.stemmers` |
| `GermanAnalyzer`  | German    | `whoosh_modern.linguistics.stemmers` |
| `SpanishAnalyzer` | Spanish   | `whoosh_modern.linguistics.stemmers` |
| `ItalianAnalyzer` | Italian   | `whoosh_modern.linguistics.stemmers` |

Each internally uses `get_stemmer("auto", language)` to select the best available backend and applies language-specific stop words.

## Stemmer Compatibility Validation

Validate that a stemmer provider works correctly with a set of test words:

```python
from whoosh_modern.analysis.stemmer_providers import (
    get_stemmer,
    validate_stemmer_compatibility,
)

stemmer = get_stemmer("auto", "english")
report = validate_stemmer_compatibility(stemmer, ["running", "cats", "jumps", "houses"])

print(report["total_words"])   # 4
print(report["successful"])    # 4 (or fewer if errors)
print(report["failed"])        # 0
print(report["results"])       # [{'word': 'running', 'stemmed': 'run', 'success': True}, ...]
```

### Compatibility Report Structure

| Field          | Type       | Description                          |
|----------------|------------|--------------------------------------|
| `provider`     | `str`      | Stemmer provider name                |
| `language`     | `str`      | Language code                        |
| `total_words`  | `int`      | Total test words                     |
| `successful`   | `int`      | Words stemmed successfully           |
| `failed`       | `int`      | Words that failed                    |
| `results`      | `list[dict]` | Per-word results with `word`, `stemmed`, `success` |

## Integration with StemmingMiddleware

The stemmer providers can be used with the `StemmingMiddleware` from `whoosh_modern.middleware.analyzer`:

```python
from whoosh_modern.analysis.stemmer_providers import get_stemmer
from whoosh_modern.middleware.analyzer import StemmingMiddleware

stemmer = get_stemmer("auto", "english")
middleware = StemmingMiddleware(
    stemmer=stemmer.stem,
    fields=["title", "content"],  # Only stem these fields
    stem_query=True,              # Also stem the search query
)
```

## Migration from Classic Whoosh

### Old API (Whoosh 1.x/2.x)

```python
from whoosh.analysis import StemmingAnalyzer as OldAnalyzer
analyzer = OldAnalyzer("en")  # Hardcoded to "english"
```

### New API (Whoosh-NG 2.0)

```python
from whoosh_modern.analysis import StemmingAnalyzer

# Auto-detect backend (preferred)
analyzer = StemmingAnalyzer("auto", language="en")

# Or use a language-specific analyzer
from whoosh_modern.linguistics.stemmers import EnglishAnalyzer
analyzer = EnglishAnalyzer()
```

> **Note**: The old `StemmingAnalyzer("en")` hardcoded the language to `"english"`. The new `StemmingAnalyzer(stemmer, language)` parameter is explicit and supports all Snowball languages via PyStemmer.

## Installation

```bash
# Without PyStemmer (uses internal stemmer, slower)
pip install whoosh-ng

# With PyStemmer (recommended, faster)
pip install whoosh-ng[fast-stemming]

# Full modern analysis
pip install whoosh-ng[modern]
```

## Stemmer Provider Integration in the Pipeline

The `StemmerProvider` system integrates at **two levels**: field-level analyzers and
pipeline middleware. Understanding both is key to avoiding double-stemming.

### Architecture

```text
┌─────────────────────────────────────────────────────────────────┐
│  StemmingAnalyzer (field-level, in Schema)                      │
│  ┌───────────────────────────────────────────────────────────┐  │
│  │ RegexTokenizer() │ StopFilter │ StemmingAnalyzer          │  │
│  │                    (stop words)    │                       │  │
│  │                                   ▼                       │  │
│  │                         stemfn = provider.stem            │  │
│  │                                   │                       │  │
│  │                                   ▼                       │  │
│  │                         Token(stemmed=True)                │  │
│  └───────────────────────────────────────────────────────────┘  │
│                                                                 │
│  Applied by Whoosh core at index time AND query time            │
│  (via QueryParser). Automatic, no middleware needed.             │
└─────────────────────────────────────────────────────────────────┘

┌─────────────────────────────────────────────────────────────────┐
│  StemmingMiddleware (pipeline-level)                            │
│  ┌───────────────────────────────────────────────────────────┐  │
│  │ before_index(context)                                     │  │
│  │   └── stem all str values in context.document             │  │
│  │                                                             │  │
│  │ before_search(context)                                     │  │
│  │   └── stem context.query if stem_query=True                │  │
│  └───────────────────────────────────────────────────────────┘  │
│                                                                 │
│  Hooked into MiddlewareChain. Manual opt-in.                    │
└─────────────────────────────────────────────────────────────────┘
```

### Level 1: Field-level (automatic)

The `StemmingAnalyzer` wraps Whoosh's built-in `StemmingAnalyzer` and injects
a `StemmerProvider`'s `.stem` method as the `stemfn`. Whoosh core applies it
automatically to the field at both index time and query time.

```python
from whoosh.fields import Schema, TEXT
from whoosh_modern.analysis import StemmingAnalyzer, get_stemmer

# Auto-detect best stemmer (PyStemmer preferred)
stemmer = get_stemmer("auto", "english")

# Create analyzer with the provider's stem function
analyzer = StemmingAnalyzer(stemmer=stemmer)

schema = Schema(
    title=TEXT(stored=True),
    content=TEXT(analyzer=analyzer),
)

# At index time: "running cats" → ["run", "cat"]
# At query time: QueryParser also uses the same analyzer
# so "running cats" matches documents containing "run cat"
```

**Pros**: Automatic, no middleware configuration needed, consistent index/query behavior.

**Cons**: Requires the analyzer to be set on each `TEXT` field. Harder to change at runtime.

### Level 2: Middleware-level (opt-in)

`StemmingMiddleware` applies stemming at the pipeline level, operating on raw
string values in `context.document` and `context.query` before Whoosh's analyzers
see them.

```python
from whoosh_modern.middleware import StemmingMiddleware
from whoosh_modern.analysis import get_stemmer
from whoosh.middleware.chain import MiddlewareChain
from whoosh.middleware.wrappers import MiddlewareWriter

stemmer = get_stemmer("auto", "english")

chain = MiddlewareChain([
    StemmingMiddleware(
        stemmer=stemmer.stem,
        fields=["title", "content"],  # None = all str fields
        stem_query=True,
    ),
])

with MiddlewareWriter(ix.writer(), chain) as writer:
    writer.add_document(title="Running cats", content="Fast dogs")
    # before_index stems: "Running cats" → "run cat"
    writer.commit()
```

**Pros**: Works on any field without modifying the schema. Can be toggled at runtime.

**Cons**: Must be manually wired into the pipeline. Risk of double-stemming if the field also uses `StemmingAnalyzer`.

### Full pipeline example: index + search

```python
from whoosh import index, fields
from whoosh.qparser import QueryParser
from whoosh_modern.analysis import StemmingAnalyzer, get_stemmer
from whoosh_modern.middleware import StemmingMiddleware
from whoosh.middleware.chain import MiddlewareChain
from whoosh.middleware.wrappers import MiddlewareWriter, MiddlewareSearcher

# 1. Schema with field-level analyzer
stemmer = get_stemmer("auto", "english")
schema = fields.Schema(
    title=fields.TEXT(stored=True, analyzer=StemmingAnalyzer(stemmer=stemmer)),
    content=fields.TEXT(analyzer=StemmingAnalyzer(stemmer=stemmer)),
)

ix = index.create_in("indexdir", schema)

# 2. Index with middleware (no double-stemming because
#    we don't use StemmingMiddleware when fields already have StemmingAnalyzer)
with ix.writer() as writer:
    writer.add_document(title="Running cats", content="Fast dogs")
    writer.commit()

# 3. Search: QueryParser applies the same analyzer to the query
with ix.searcher() as searcher:
    qp = QueryParser("content", schema)
    q = qp.parse("running cats")
    results = searcher.search(q)
    # "running" is stemmed to "run" by the analyzer
    # "cats" is stemmed to "cat" by the analyzer
    # Matches document with "run" and "cat"
```

### Avoiding double-stemming

```python
# WRONG: double stemming
schema = Schema(
    content=TEXT(analyzer=StemmingAnalyzer(stemmer="auto")),
)
chain = MiddlewareChain([
    StemmingMiddleware(stemmer=get_stemmer("auto").stem),  # Don't do this!
])
# Result: "running" → "run" (analyzer) → "run" (middleware) — harmless but wasteful

# CORRECT: choose ONE level
# Option A: field-level only (recommended for static schemas)
schema = Schema(content=TEXT(analyzer=StemmingAnalyzer(stemmer="auto")))
# No StemmingMiddleware needed

# Option B: middleware-only (for dynamic fields)
schema = Schema(content=TEXT)  # No analyzer
chain = MiddlewareChain([StemmingMiddleware(stemmer=get_stemmer("auto").stem)])
```

### Custom stemmer provider

```python
from whoosh_modern.analysis import register_stemmer, get_stemmer

@register_stemmer("my_stemmer")
class MyStemmer:
    def stem(self, word: str) -> str:
        return word.lower().rstrip("s")

# Use it like any built-in backend
stemmer = get_stemmer("my_stemmer", "english")
analyzer = StemmingAnalyzer(stemmer=stemmer)
```

## See Also

- [Stemming and Stop Words Guide](../core/stemming.md) — Classic Whoosh stemming guide
- [Synonyms & Linguistics Guide](linguistics.md) — Synonym expansion engine
- [Provider Integration Guide](provider-integration.md) — Complete pipeline guide for all providers
- [API: Modern](../api/modern.md) — Full API reference for analysis extensions


## DOCUMENT: Storage Providers

# Storage Providers

Whoosh-NG provides pluggable storage backends through the
`SyncStorageProvider` / `AsyncStorageProvider` contracts. This allows the
index to be persisted on local disk, SQLite, S3, or a hybrid cache + remote
setup without changing the writer or the index.

## Architecture Overview

### Level 1: SnapshotStorage (Simple)

```
Writer → Local FS → Commit → Upload Segment → S3
Reader → Download Segment → Open locally
```

Very simple to maintain. Use `SnapshotStorage` when you want S3 as a simple
backup/restore target without the complexity of a local cache.

### Level 2: CachedObjectStorage (Recommended for Production)

```
+----------+
|  MinIO   |
+----------+
     ^
     |
 Sync |
     v
+-----------+   Cache Layer   +-----------+
| Searcher  |<--------------->| Writer    |
+-----------+                 +-----------+
        |
        v
 Local SSD
```

- Index lives on SSD
- S3 serves as replication
- Segments are pushed after commit
- Restoration possible at any moment

This is what many modern distributed search systems do.

## Available providers

| Provider | Type | Backend | Use Case |
|----------|------|---------|----------|
| `FileStorage` | sync | local filesystem | Single-node, no cloud |
| `AsyncFileStorage` | async | local filesystem | Single-node async |
| `S3Storage` | sync | S3-compatible | Direct S3 access |
| `SnapshotStorage` | sync | S3-compatible | Simple backup/restore |
| `HybridStorage` | sync | local cache + remote | **Production** (alias: `CachedObjectStorage`) |
| `AsyncHybridStorage` | async | local cache + remote | Production async |
| `CoreStorageAdapter` | sync | core `FileStorage` → modern `SyncStorageProvider` | Bridge legacy core backends into the modern pipeline |

All providers are importable from `whoosh_modern.storage`.

## FileStorage

Local filesystem storage. Keys are relative paths under `root`.

```python
from whoosh_modern.storage import FileStorage

storage = FileStorage("indexdir")
storage.write("segment_1.dat", b"data")
assert storage.read("segment_1.dat") == b"data"
assert storage.exists("segment_1.dat") is True
storage.delete("segment_1.dat")
keys = storage.list_keys()
```

## CoreStorageAdapter

Wraps a core ``whoosh.filedb.filestore.FileStorage`` instance behind the modern
``SyncStorageProvider`` interface. This lets you drop an existing core storage
backend into a modern ``HybridStorage`` or ``StorageMiddleware`` chain without
modifying the core class.

```python
from whoosh.filedb.filestore import FileStorage as CoreFileStorage
from whoosh_modern.storage import CoreStorageAdapter

core = CoreFileStorage("indexdir")
adapter = CoreStorageAdapter(core)

adapter.write("segment_1.dat", b"data")
assert adapter.read("segment_1.dat") == b"data"
assert adapter.exists("segment_1.dat") is True
adapter.delete("segment_1.dat")
```

## AsyncFileStorage

Async variant of `FileStorage`. All operations run on a worker thread via
`asyncio.to_thread` so the event loop is never blocked.

```python
import asyncio
from whoosh_modern.storage import AsyncFileStorage

storage = AsyncFileStorage("indexdir")

async def main() -> None:
    await storage.awrite("segment_1.dat", b"data")
    data = await storage.aread("segment_1.dat")
    await storage.adelete("segment_1.dat")

asyncio.run(main())
```

## S3Storage

S3-compatible blob storage. `boto3` is imported lazily, so it is an optional
dependency. A `client` can be injected for testing.

```python
from whoosh_modern.storage import S3Storage

# Default client (requires boto3 installed and configured)
storage = S3Storage(bucket="my-index-bucket", prefix="segments")

# Or inject a client for testing / custom configuration
storage = S3Storage(
    bucket="my-index-bucket",
    prefix="segments",
    client=my_boto3_client,
)

storage.write("segment_1.dat", b"data")
data = storage.read("segment_1.dat")
keys = storage.list_keys()
```

Install the optional dependency:

```bash
pip install whoosh-ng[s3]
```

## SnapshotStorage

Simple S3 snapshot storage. S3 remains the source of truth; on read, the object is
downloaded from S3 and also persisted under `local_path` so subsequent reads of the same
key can be served from the local scratch copy:

- Write: upload segment directly to S3
- Read: download segment from S3, cache it under `local_path`

Use this when you want S3 as a simple backup/restore target without the
complexity of a local cache.

```python
from whoosh_modern.storage import SnapshotStorage

storage = SnapshotStorage(
    local_path="./index",
    bucket="my-index-bucket",
    prefix="snapshots",
)

storage.write("segment_1.dat", b"data")
data = storage.read("segment_1.dat")
```

## HybridStorage / CachedObjectStorage

`HybridStorage` composes a local cache and a remote backend. The remote is
the source of truth; the local cache is a write-through performance layer.

`CachedObjectStorage` is an alias for `HybridStorage` that better conveys
the intent: a local object cache synchronized with S3.

This is the recommended architecture for production deployments with repeated
read patterns.

```python
from whoosh_modern.storage import HybridStorage, S3Storage

remote = S3Storage(bucket="my-index-bucket", prefix="segments")
storage = HybridStorage(local_cache="./cache", remote=remote)

# Write-through: remote is source of truth, cache is updated on success
storage.write("segment_1.dat", b"data")

# First read: cache miss → fetch from S3, write-through into cache
data = storage.read("segment_1.dat")

# Second read: cache hit → served from local disk, zero network
data = storage.read("segment_1.dat")

# Force refresh from remote
storage.invalidate("segment_1.dat")

# Warm cache proactively
storage.prefetch(["segment_2.dat", "segment_3.dat"])
```

### Read path

1. local cache hit → return immediately
2. cache miss → read from remote, write-through into cache, return

### Write path

- `remote.write(key, data)` (source of truth)
- on success → `local_cache.write(key, data)`
- on failure → raise before polluting cache

### Cache eviction

The local cache is bounded by `max_cache_size_mb` (default 1024 MB). When
the limit is reached, the oldest entries are evicted using an LRU policy.

### `list_keys`

`list_keys()` uses the remote as source of truth because the cache is only
partial. Pass `include_cache=True` to return the union of remote and cache
keys.

## AsyncHybridStorage

Async variant of `HybridStorage`. Remote operations are executed on a worker
thread via `asyncio.to_thread` so the event loop is never blocked.

```python
import asyncio
from whoosh_modern.storage import AsyncHybridStorage, S3Storage

remote = S3Storage(bucket="my-index-bucket", prefix="segments")
storage = AsyncHybridStorage(local_cache="./cache", remote=remote)

async def main() -> None:
    await storage.awrite("segment_1.dat", b"data")
    data = await storage.aread("segment_1.dat")
    await storage.adelete("segment_1.dat")
    keys = await storage.alist_keys()

asyncio.run(main())
```

## Using storage with SearchApplication

```python
from whoosh_modern import SearchApplication, SQLSource
from whoosh_modern.storage import HybridStorage, S3Storage

remote = S3Storage(bucket="my-index-bucket", prefix="segments")
storage = HybridStorage(local_cache="./cache", remote=remote)

app = SearchApplication(
    source=SQLSource(query="SELECT * FROM products", connection=engine),
    storage=storage,
)
app.build()
results = app.index.search("laptop")
```

## Performance Benchmarks

Benchmarks were run against a local MinIO instance using a 28.89 MB Whoosh
index (2 segment files). Results are indicative of relative performance
between strategies on S3-compatible storage.

| Strategy | Backup (MB/s) | Restore (MB/s) | Notes |
|----------|---------------|----------------|-------|
| `1_obj_per_segment` | 39.44 | 139.72 | Best restore throughput; simplest |
| `compressed_zstd` | 31.56 | 133.74 | Lower bandwidth, CPU overhead |
| `hybrid_cache_s3` | 44.97 | 133.61 | Best backup; excellent warm-cache reads |
| `1_obj_per_posting_list` | 0.28 | 4.79 | **Avoid**: millions of small objects kill S3 |

### Recommendations

- **Default**: `S3Storage` with 1 object per segment file. It offers the
  best restore throughput and is the simplest to operate.
- **Production with repeated reads**: `HybridStorage(local_cache, S3Storage)`.
  After the first read, subsequent reads are served from local disk at
  ~133 MB/s.
- **Avoid**: 1 object per posting list. S3 is not optimized for millions of
  tiny objects; latency and cost explode.
- **Compression**: ZSTD reduces transfer size by ~20-30% at the cost of CPU.
  Use it when network bandwidth is the bottleneck, not when CPU is.

### Running the benchmarks

```bash
# Start MinIO
docker run -d --name minio-benchmark -p 9000:9000 -p 9001:9001 \
  -e MINIO_ROOT_USER=minioadmin -e MINIO_ROOT_PASSWORD=minioadmin \
  minio/minio:latest server /data --console-address ":9001"

# Run synthetic benchmark
python benchmark/s3_storage_benchmark.py

# Run real Whoosh index benchmark (requires customers CSV)
python benchmark/s3_storage_benchmark_real.py
```

## How Storage Providers Integrate into the Indexing and Search Pipeline

The storage provider participates in two distinct phases: **index creation** (determining where segments live) and **runtime pipeline integration** (via `StorageMiddleware`).

### Full indexing flow with a storage provider

```text
DataSource.stream_batches()
    │
    ▼
SearchApplication.build()
    │
    ├── source.discover_schema() ──► Whoosh Schema
    │
    ├── storage root resolution
    │       │
    │       ├── FileStorageProvider (exposed as `FileStorage`)
    │       │   └── exposes a public `root` ──► whoosh.index.create_in(root, schema)
    │       │
    │       └── Other providers (S3 / Snapshot / Hybrid, no filesystem root)
    │           └── tempfile.mkdtemp() ──► create_in(tmpdir, schema)
    │
    ├── Writer = index.writer()
    │       │
    │       ├── MiddlewareChain.before_index()
    │       │   └── StorageMiddleware.before_index()
    │       │       ├── context.labels["storage_backend"] = provider.__class__.__name__
    │       │       └── context.metadata["storage_provider"] = self
    │       │
    │       ├── for batch in source.stream_batches():
    │       │       for doc in batch:
    │       │           writer.add_document(**doc)
    │       │
    │       └── writer.commit()
    │           │
    │           └── StorageMiddleware.on_commit()
    │               └── provider.write("commits/{name}/{timestamp}", b"1")
    │
    ▼
Index persisted on disk / S3 / hybrid cache
```

### Full search flow with a storage provider

```text
SearchApplication.search(query)
    │
    ├── index.searcher()
    │       │
    │       └── Whoosh core opens segment files from:
    │           ├── local filesystem (FileStorageProvider root)
    │           ├── SQLite DB (SQLiteStorageProvider)
    │           └── S3 / hybrid cache (S3StorageProvider / HybridStorage)
    │
    ├── QueryParser.parse(query) ──► Query object
    │
    └── searcher.search(query)
        │
        └── Whoosh core reads posting lists from segment files
            └── Returns Results (Hits)
```

### StorageMiddleware hooks in detail

`StorageMiddleware` (`whoosh_modern.middleware.storage`) is the integration point
that routes index persistence through any `SyncStorageProvider` without modifying
the writer.

| Hook | When | What it does |
|------|------|--------------|
| `before_index(context)` | Before each document is added | Tags the context with `storage_backend` label and `storage_provider` metadata |
| `on_commit(context)` | After `writer.commit()` | Writes a commit checkpoint marker (`commits/{name}/{timestamp}`) to the provider |

### Example: StorageMiddleware with a custom chain

```python
from whoosh_modern.middleware import (
    StorageMiddleware,
    FileStorageProvider,
    StemmingMiddleware,
)
from whoosh.middleware.chain import MiddlewareChain
from whoosh_modern.analysis import get_stemmer

# Storage provider
storage = FileStorageProvider("/data/index")

# Create middleware chain
chain = MiddlewareChain([
    StorageMiddleware(storage, name="primary"),
    StemmingMiddleware(stemmer=get_stemmer("auto", "english").stem),
])

# Apply to writer
from whoosh.middleware.wrappers import MiddlewareWriter

with MiddlewareWriter(ix.writer(), chain) as writer:
    writer.add_document(title="Hello", content="World")
    # StorageMiddleware.before_index() tags the context
    # StemmingMiddleware stems the fields
    # writer.commit() triggers StorageMiddleware.on_commit()
    writer.commit()
```

### Key insight: StorageProvider vs StorageMiddleware

| Component | Role |
|-----------|------|
| `SyncStorageProvider` / `AsyncStorageProvider` | **Contract** defining `write()`, `read()`, `delete()`, `exists()`, `list_keys()` |
| `FileStorageProvider`, `S3StorageProvider`, `HybridStorage` | **Implementations** of the contract |
| `StorageMiddleware` | **Integration layer** that calls the provider at specific lifecycle hooks (`before_index`, `on_commit`) |
| `SearchApplication` | **Entry point** that delegates to `SearchView.build()`; when the storage is a `FileStorageProvider` (exposed as `FileStorage`) it uses its public `root` to create the Whoosh index directory, otherwise it falls back to a temporary directory |

The provider itself does **not** intercept Whoosh's internal segment reads. Those reads go through Whoosh's built-in `FileStorage` (`whoosh.filedb.filestore`) which reads from the filesystem path given to `create_in()`. The Whoosh-NG storage provider abstraction is designed for:
- Custom segment routing (S3, SQLite, hybrid cache)
- Commit checkpointing via middleware
- Future: segment-level read/write interception

## See Also

- [Provider Integration Guide](provider-integration.md) — Complete pipeline guide for all providers
- [Middleware Guide](middleware-pipeline.md) — Pipeline hooks and provider adapters


## DOCUMENT: Synonyms

# Synonyms

Module: `whoosh_modern.linguistics.synonyms`
Version: 3.0.0

The synonyms engine provides query-time and index-time synonym expansion through a pluggable provider system. It supports static in-memory mappings, YAML/JSON files, SQLite persistence, and large-scale Wiktionary dictionaries.

## Provider Architecture

All synonym providers implement the `SynonymProvider` protocol:

```python
from whoosh_modern.linguistics.synonyms import SynonymProvider

class MyProvider(SynonymProvider):
    def get_synonyms(self, word: str) -> list[str]: ...
    def add_synonym(self, word: str, synonyms: list[str]) -> None: ...
    def remove_synonym(self, word: str, synonym: str) -> None: ...
```

## Built-in Providers

### StaticSynonymProvider

In-memory provider backed by a dictionary:

```python
from whoosh_modern.linguistics.synonyms import StaticSynonymProvider

provider = StaticSynonymProvider({
    "car": ["automobile", "vehicle"],
    "house": ["home", "residence"],
})
print(provider.get_synonyms("car"))  # ['automobile', 'vehicle']
```

### YAMLSynonymProvider

Loads synonyms from a YAML file:

```yaml
# synonyms.yaml
car:
  - automobile
  - vehicle
house:
  - home
  - residence
```

```python
from whoosh_modern.linguistics.synonyms import YAMLSynonymProvider

provider = YAMLSynonymProvider("synonyms.yaml")
print(provider.get_synonyms("car"))  # ['automobile', 'vehicle']
```

### JSONSynonymProvider

Loads synonyms from a JSON file:

```json
{
    "car": ["automobile", "vehicle"],
    "house": ["home", "residence"]
}
```

```python
from whoosh_modern.linguistics.synonyms import JSONSynonymProvider

provider = JSONSynonymProvider("synonyms.json")
print(provider.get_synonyms("car"))
```

### WiktionarySynonymProvider

Loads synonyms from a kaikki.org JSON Lines dictionary file:

```python
from whoosh_modern.linguistics.synonyms import WiktionarySynonymProvider

provider = WiktionarySynonymProvider(
    "src/whoosh_modern/linguistics/dictionaries/wiktionary/fr.json"
)
print(provider.get_synonyms("voiture"))  # ['automobile', 'véhicule']
```

Each line in the dictionary file is a JSON object:

```json
{"word": "voiture", "s": ["automobile", "véhicule"]}
{"word": "ordinateur", "s": ["pc", "machine"]}
```

The provider filters out:
- Words containing spaces (multi-word expressions)
- Entries with non-standard parts of speech
- Empty or missing synonym lists

### SQLiteSynonymStore

Persistent synonym store backed by SQLite:

```python
from whoosh_modern.linguistics.synonyms import SQLiteSynonymStore

store = SQLiteSynonymStore("synonyms.db")
store.add_synonym("car", ["automobile", "vehicle"])
print(store.get_synonyms("car"))  # ['automobile', 'vehicle']
store.close()
```

## SynonymManager

The `SynonymManager` is the high-level interface for managing synonyms:

```python
from whoosh_modern.linguistics.synonyms import SynonymManager

manager = SynonymManager({"car": ["automobile", "vehicle"]})

# CRUD
manager.add_synonyms("house", ["home", "residence"])
print(manager.get_synonyms("house"))  # ['home', 'residence']
manager.remove_synonym("house", "home")

# Import from external sources
manager.import_yaml("synonyms.yaml")       # Requires PyYAML
manager.import_json("synonyms.json")
manager.import_wiktionary("dictionaries/wiktionary/fr.json")

# Export
manager.export_json("output.json")
```

## Updating Wiktionary Dictionaries

Pre-generated dictionaries live in `src/whoosh_modern/linguistics/dictionaries/wiktionary/`:

```
wiktionary/
├── fr.json
├── en.json
├── de.json
├── es.json
├── it.json
├── manifest.json
└── README.md
```

To regenerate them from the latest kaikki.org dump:

```bash
python scripts/update_wiktionary_dictionaries.py --all
```

Or for a single language:

```bash
python scripts/update_wiktionary_dictionaries.py --lang fr
```

The script downloads `kaikki.org-dictionary-all.jsonl`, extracts synonyms by language, filters by allowed POS tags, and writes compact per-language JSON Lines files.

## SynonymExpansionMiddleware

Integrates synonym expansion into the middleware pipeline:

```python
from whoosh_modern.linguistics.synonyms import (
    SynonymManager,
    SynonymExpansionMiddleware,
)

manager = SynonymManager({
    "car": ["automobile", "vehicle"],
    "house": ["home", "residence"],
})
middleware = SynonymExpansionMiddleware(manager)
```

The middleware expands both search queries and indexed documents:

```python
# Query expansion
ctx = MiddlewareContext(operation="search")
ctx.query = "car"
ctx = middleware.before_search(ctx)
# ctx.query == "car automobile vehicle"

# Document expansion
ctx = MiddlewareContext(operation="index")
ctx.document = {"title": "house for sale"}
ctx = middleware.before_index(ctx)
# ctx.document["title"] == "house for sale home residence"
```

## Prebuilt Language Synonyms

`LANG_SYNONYMS` provides starter dictionaries for five languages:

```python
from whoosh_modern.linguistics.synonyms import LANG_SYNONYMS

french_syns = LANG_SYNONYMS["fr"]
print(french_syns["voiture"])  # ['automobile', 'véhicule']

english_syns = LANG_SYNONYMS["en"]
print(english_syns["car"])  # ['automobile', 'vehicle']
```

| Language | Code | Sample Entry                          |
|----------|------|---------------------------------------|
| French   | `fr` | `"voiture": ["automobile", "véhicule"]` |
| English  | `en` | `"car": ["automobile", "vehicle"]`    |
| German   | `de` | `"auto": ["wagen", "fahrzeug"]`       |
| Spanish  | `es` | `"coche": ["automóvil", "vehículo"]`  |
| Italian  | `it` | `"auto": ["automobile", "veicolo"]`   |

## Integration Example

```python
from whoosh_modern.linguistics import (
    LANG_SYNONYMS,
    SynonymExpansionMiddleware,
    SynonymManager,
)
from whoosh.middleware.chain import MiddlewareChain
from whoosh.middleware.wrappers import MiddlewareWriter, MiddlewareSearcher

# 1. Build synonym manager
syn_manager = SynonymManager(LANG_SYNONYMS["en"])
syn_manager.add_synonyms("search", ["query", "find", "lookup"])

# 2. Create middleware
syn_middleware = SynonymExpansionMiddleware(syn_manager)

# 3. Build middleware chain
chain = MiddlewareChain([syn_middleware])

# 4. Use with writer/searcher
with MiddlewareWriter(ix.writer(), chain) as writer:
    writer.add_document(title="How to search in Whoosh")

with MiddlewareSearcher(ix.searcher(), chain) as searcher:
    results = searcher.search("search")
```

## Wiktionary Indexing Integration

`WiktionaryIndexer` can feed synonyms directly into `SynonymManager` and `SearchApplication`.

### SynonymManager.import_wiktionary_index()

Populate a manager from a built Whoosh index:

```python
from whoosh_modern.linguistics.synonyms import SynonymManager
from whoosh_modern.linguistics.wiktionary_indexer import WiktionaryIndexer

indexer = WiktionaryIndexer("indexdir")
# ... build_index() called earlier ...

manager = SynonymManager()
manager.import_wiktionary_index("indexdir", language="fr")
print(manager.get_synonyms("voiture"))
# ['automobile', 'véhicule']
```

### SearchApplication integration

Pass a `WiktionaryIndexer` to `SearchApplication` to expose a pre-populated `synonym_manager`:

```python
from whoosh_modern import SearchApplication
from whoosh_modern.linguistics.wiktionary_indexer import WiktionaryIndexer

indexer = WiktionaryIndexer("indexdir")
app = SearchApplication(wiktionary_indexer=indexer)

# synonym_manager is lazily populated from the index
manager = app.synonym_manager
```

### SynonymExpansionMiddleware wiring

Combine with the middleware to expand queries at search time:

```python
from whoosh_modern.linguistics.synonyms import SynonymExpansionMiddleware

middleware = SynonymExpansionMiddleware(app.synonym_manager)
```

## See Also

- [Linguistics Overview](linguistics.md) — Stemmers, language analyzers, and full pipeline integration
- [Middleware Pipeline](middleware-pipeline.md) — How middleware chains work
- [Stemming Providers](stemming-providers.md) — Language-specific stemmer backends


## DOCUMENT: Vector

# Vector Search

Module: `whoosh_modern.vector`
Version: 2.1.0

Whoosh-NG supports semantic search through vector embeddings. This guide covers setting up and using vector fields.

## Concept

Vector search lets you find documents based on semantic similarity rather than exact keyword matches. You embed documents and queries into a high-dimensional space, then find nearest neighbors.

```
Query embedding  ----\
                      >--- Cosine Similarity ---> Ranked results
Document embedding ---/
```

## Setup

### Define Schema

```python
from whoosh.fields import Schema, TEXT, VectorField

schema = Schema(
    title=TEXT(stored=True),
    content=TEXT,
    embedding=VectorField(dimensions=384)  # e.g., all-MiniLM-L6-v2
)
```

## Providers

Whoosh-NG includes multiple vector backends:

| Provider | Description | Use Case |
|----------|-------------|----------|
| `NumpyProvider` | Pure NumPy, cosine similarity | Small to medium indexes |
| `HNSWProvider` | Hierarchical navigable small world | Large indexes, fast ANN |
| `FaissProvider` | Facebook AI Similarity Search | Very large indexes |
| `QdrantProvider` | Qdrant vector DB | Distributed |

### NumpyProvider (Default)

```python
from whoosh.vector import NumpyProvider

provider = NumpyProvider()
provider.add_vector(doc_id, embedding)
results = provider.search(query_embedding, limit=10)
```

## Indexing with Vectors

### Generate Embeddings

```python
from sentence_transformers import SentenceTransformer

model = SentenceTransformer("all-MiniLM-L6-v2")

embeddings = model.encode([
    "First document content",
    "Second document content"
])
```

### Write Documents

```python
with ix.writer() as writer:
    writer.add_document(
        title="Doc 1",
        content="Python is great",
        embedding=embeddings[0].tolist()
    )
    writer.commit()
```

## Searching with Vectors

### Hybrid Search (Keyword + Vector)

```python
from whoosh.searching import Searcher
from whoosh.vector import VectorProvider

with ix.searcher() as searcher:
    # Semantic search component
    query_embedding = model.encode(["Python tutorial"])[0]
    vector_results = searcher.vector_search(
        "embedding", query_embedding, limit=20
    )

    # Keyword search component
    keyword_query = QueryParser("content", schema).parse("Python")
    keyword_results = searcher.search(keyword_query, limit=20)

    # Combine (e.g., RRF fusion)
    final_results = fuse_results(vector_results, keyword_results)
```

### Pure Vector Search

```python
with ix.searcher() as searcher:
    query_embedding = model.encode(["search query"])[0]
    results = searcher.vector_search(
        "embedding",
        query_embedding,
        limit=10,
        metric="cosine"  # or "euclidean", "dot"
    )
```

## VectorField Options

```python
embedding_field = VectorField(
    dimensions=384,      # Required: embedding dimension
    metric="cosine",     # Similarity metric: cosine, euclidean, dot
    provider="hnsw"      # Provider name from registry
)
```

## Indexing Stream

```python
from whoosh.vector.indexing import VectorIndexer

indexer = VectorIndexer(ix)
indexer.add_document(
    title="Doc",
    content="Content",
    embedding=embedding.tolist()
)
indexer.commit()
```

## Similarity Metrics

| Metric | Description | Range |
|--------|-------------|-------|
| `cosine` | Cosine similarity | [0, 1] (higher is more similar) |
| `euclidean` | Euclidean distance | [0, inf) (lower is more similar) |
| `dot` | Dot product | [-inf, inf] (higher is more similar) |

## Best Practices

1. **Normalize embeddings**: Use cosine similarity with normalized vectors
2. **Choose provider wisely**: Numpy for &lt;100k vectors, HNSW/Faiss for larger
3. **Hybrid search**: Combine vector and keyword search for best results
4. **Cache embeddings**: Pre-compute and store to avoid recomputation
5. **Batch indexing**: Index vectors in batches for efficiency

## Vector Provider Integration in the Pipeline

The vector search system integrates through Whoosh's plugin registry and segment
format. The provider is stored in the index segment and resolved at search time.

### Architecture

```text
┌─────────────────────────────────────────────────────────────────────┐
│  Registration (startup)                                            │
│                                                                     │
│  VectorPlugin.register(PluginManager)                              │
│    └── VectorRegistry.register("numpy", NumpyProvider(), owner)     │
│                                                                     │
│  The provider is now available for any VECTOR field                │
│  that specifies provider="numpy"                                   │
└─────────────────────────────────────────────────────────────────────┘

┌─────────────────────────────────────────────────────────────────────┐
│  Indexation                                                         │
│                                                                     │
│  VECTOR(dimensions=384, provider="numpy")                          │
│       │                                                             │
│       ▼                                                             │
│  PerDocWriter.add_vector_items(fieldname, field, items)            │
│       │                                                             │
│       ▼                                                             │
│  Segment file contains:                                            │
│    - vector bytes (raw)                                            │
│    - provider name ("numpy")                                        │
│    - metric ("cosine")                                              │
│       │                                                             │
│       ▼                                                             │
│  writer.commit() → segments written to disk                        │
└─────────────────────────────────────────────────────────────────────┘

┌─────────────────────────────────────────────────────────────────────┐
│  Search                                                          │
│                                                                     │
│  searcher.vector_search("embedding", query_vec, k=10)              │
│       │                                                             │
│       ▼                                                             │
│  Whoosh core reads segment                                         │
│    └── retrieves provider name ("numpy")                            │
│       │                                                             │
│       ▼                                                             │
│  VectorRegistry.get("numpy")                                        │
│       │                                                             │
│       ▼                                                             │
│  NumpyProvider.search(query_vec, k, filter_ids)                    │
│       │                                                             │
│       ▼                                                             │
│  VectorHit[] sorted by cosine similarity                           │
└─────────────────────────────────────────────────────────────────────┘
```

### Full indexing flow

```python
from whoosh import index, fields
from whoosh_modern.vector.plugin import VectorPlugin
from whoosh.plugins.manager import PluginManager
from whoosh_modern.storage import FileStorage
import numpy as np

# 1. Register vector plugin (startup)
manager = PluginManager()
VectorPlugin().register(manager)

# 2. Define schema with VECTOR field
schema = fields.Schema(
    title=fields.TEXT(stored=True),
    embedding=fields.VECTOR(dimensions=384, provider="numpy", stored=True),
)

# 3. Create index (storage determines where segments live)
ix = index.create_in("indexdir", schema)

# 4. Index documents with vectors
np.random.seed(42)
embeddings = {
    "doc1": np.random.rand(384).astype(np.float32).tolist(),
    "doc2": np.random.rand(384).astype(np.float32).tolist(),
}

with ix.writer() as writer:
    for doc_id, vec in embeddings.items():
        writer.add_document(
            title=f"Document {doc_id}",
            embedding=vec,
        )
    writer.commit()
    # Whoosh core serializes vectors to segment files
    # Provider name "numpy" is stored in the segment
```

### Full search flow

```python
from whoosh.qparser import QueryParser
import numpy as np

with ix.searcher() as searcher:
    # 1. Keyword search
    qp = QueryParser("title", schema)
    keyword_results = searcher.search(qp.parse("Document"))

    # 2. Vector search
    query_vec = np.random.rand(384).astype(np.float32).tolist()
    vector_results = searcher.vector_search(
        "embedding",
        query_vec,
        limit=10,
    )

    # 3. Hybrid: combine both
    # Example: Reciprocal Rank Fusion (RRF)
    def rrf(results_list, k=60):
        scores = {}
        for results in results_list:
            for rank, hit in enumerate(results):
                doc_id = hit.doc_id if hasattr(hit, "doc_id") else hit["doc_id"]
                scores[doc_id] = scores.get(doc_id, 0) + 1 / (k + rank + 1)
        return sorted(scores.items(), key=lambda x: x[1], reverse=True)

    combined = rrf([keyword_results, vector_results])
```

### Standalone usage (no schema)

```python
from whoosh_modern.vector import NumpyProvider

# Create provider directly
provider = NumpyProvider()

# Add vectors
provider.add([
    ("doc1", [0.1, 0.2, 0.3]),
    ("doc2", [0.4, 0.5, 0.6]),
])

# Search
query_vec = [0.1, 0.2, 0.3]
hits = provider.search(query_vec, k=5)

for hit in hits:
    print(f"doc_id={hit.doc_id}, score={hit.score:.4f}")
```

### Provider resolution chain

When `searcher.vector_search()` is called, Whoosh core:

1. Reads the `VECTOR` field configuration from the schema
2. Opens the segment file containing the vector data
3. Extracts the provider name stored in the segment (e.g., `"numpy"`)
4. Looks up the provider in `VectorRegistry`
5. Calls `provider.search(query_vector, k, filter_ids)`
6. Returns `list[VectorHit]`

If the provider is not registered, the search fails with a registry miss. This
is why `VectorPlugin().register(manager)` (or manual registration) is required
at startup.

## See Also

- [Provider Integration Guide](provider-integration.md) — Complete pipeline guide for all providers
- [Middleware Guide](middleware-pipeline.md) — Pipeline hooks and provider adapters


## DOCUMENT (FR): Analysis

> **Note de traduction** : Cette page n'est pas encore traduite en français.
> Le contenu anglais est affiché ci-dessous en attendant la traduction.

<!-- Creez une version francaise de ce fichier et supprimez ce message. -->


# Analysis API

Classes and functions for turning text into indexable "tokens" (usually words).
Analysis is the first step in the indexing pipeline: an analyzer tokenizes text
and applies zero or more filters to the resulting token stream.

## Overview

Three general categories of objects make up the analysis pipeline:

- **Tokenizers** split text into individual tokens (words, n-grams, identifiers).
  Every tokenizer is callable: `tokenizer(text) -> iterator of Token objects`.
- **Filters** transform one token stream into another. Common operations include
  lowercasing, stop-word removal, stemming, and synonym expansion. Every filter
  is callable: `filter(token_generator) -> token_generator`.
- **Analyzers** compose a tokenizer and zero or more filters into a single unit.
  Every analyzer is callable and can be used directly as a field's `analyzer`
  argument.

Tokenizers and filters are combined using the `|` operator:

```python
my_analyzer = RegexTokenizer() | LowercaseFilter() | StopFilter()
```

The first item must be a tokenizer; subsequent items must be filters.

## Composition

### Composable

```python
class whoosh.analysis.Composable
```

Base class for tokenizers and filters, providing `|` composition.

**Attributes:**
- `is_morph (bool)`: Whether this object performs morphological transformation
  (e.g. stemming). Defaults to `False`.

**Methods:**

#### `__or__(self, other)`

Combines this object with `other` using `CompositeAnalyzer`.

```python
analyzer = RegexTokenizer() | LowercaseFilter() | StopFilter()
```

### CompositeAnalyzer

```python
class whoosh.analysis.CompositeAnalyzer
```

Composed analyzer created by chaining a tokenizer and filters with `|`.

**Example:**
```python
from whoosh.analysis import RegexTokenizer, LowercaseFilter, StopFilter

analyzer = RegexTokenizer() | LowercaseFilter() | StopFilter()
tokens = list(analyzer("Hello world, this is a test"))
```

## Token

```python
class whoosh.analysis.Token
```

Represents a single token (usually a word) extracted from source text.
Tokenizers yield the **same** `Token` object repeatedly (for performance), so
consumers must not hold references between iterations.

**Slots:**

| Attribute | Type | Description |
|-----------|------|-------------|
| `text` | `str` | The text of this token |
| `pos` | `int` | Token position (if `positions=True`) |
| `startchar` | `int` | Start character offset (if `chars=True`) |
| `endchar` | `int` | End character offset (if `chars=True`) |
| `original` | `str` | Original text before filters (if `keeporiginal=True`) |
| `positions` | `bool` | Whether position info was requested |
| `chars` | `bool` | Whether character offsets were requested |
| `stopped` | `bool` | Set by `StopFilter` |
| `boost` | `float` | Token boost factor (default `1.0`) |
| `removestops` | `bool` | Whether stop words should be removed |
| `mode` | `str` | `'index'` or `'query'` |
| `boosts` | `dict` | Per-position boost values (if requested) |
| `tokenize` | `bool` | Whether tokenization should proceed |
| `matched` | `bool` | Used during highlighting |
| `fieldname` | `str` | Field name for this token |

**Methods:**

#### `copy()`

Returns a new `Token` with the same attribute values. Use this if you need to
retain a token between iterations.

```python
def remove_duplicates(stream):
    last = None
    for t in stream:
        if last != t.text:
            yield t
        last = t.text
```

## Utility Functions

### entoken

```python
whoosh.analysis.entoken(
    textstream,
    positions=False,
    chars=False,
    start_pos=0,
    start_char=0,
    **kwargs
) -> Iterator[Token]
```

Converts a sequence of strings into a stream of `Token` objects.

### unstopped

```python
whoosh.analysis.unstopped(tokenstream) -> Iterator[Token]
```

Removes tokens where `token.stopped` is `True`.

## Analyzers

### Analyzer (Base)

```python
class whoosh.analysis.Analyzer
```

Abstract base class for all analyzers. Subclasses implement `__call__`.

### CompositeAnalyzer

Created automatically when you use `|` to compose tokenizers and filters.

### Predefined Analyzers

#### IDAnalyzer

```python
whoosh.analysis.IDAnalyzer(lowercase=False) -> Analyzer
```

Yields the entire input as a single token. Deprecated; use `IDTokenizer` directly.

- `lowercase (bool)`: If True, add a `LowercaseFilter`.

#### KeywordAnalyzer

```python
whoosh.analysis.KeywordAnalyzer(
    lowercase=False,
    commas=False
) -> Analyzer
```

Splits on whitespace or commas. Suitable for field values that are lists of
keywords.

- `lowercase (bool)`: Lowercase each token.
- `commas (bool)`: Split on commas instead of whitespace.

**Example:**
```python
from whoosh.analysis import KeywordAnalyzer

an = KeywordAnalyzer(lowercase=True, commas=True)
list(an("Hello, WORLD, test"))
# => ["hello", "world", "test"]
```

#### RegexAnalyzer

```python
whoosh.analysis.RegexAnalyzer(
    expression=r"\w+(\.?\w+)*",
    gaps=False
) -> Analyzer
```

Deprecated; use `RegexTokenizer` directly.

#### SimpleAnalyzer

```python
whoosh.analysis.SimpleAnalyzer(
    expression=default_pattern,
    gaps=False
) -> Analyzer
```

Composes `RegexTokenizer` with `LowercaseFilter`.

- `expression`: Regex pattern for tokens.
- `gaps`: If True, split on the expression instead of matching it.

**Example:**
```python
an = SimpleAnalyzer()
list(an("Hello there, this is a TEST"))
# => ["hello", "there", "this", "is", "a", "test"]
```

#### StandardAnalyzer

```python
whoosh.analysis.StandardAnalyzer(
    expression=default_pattern,
    stoplist=STOP_WORDS,
    minsize=2,
    maxsize=None,
    gaps=False
) -> Analyzer
```

Composes `RegexTokenizer`, `LowercaseFilter`, and optional `StopFilter`.

- `expression`: Regex pattern for tokens.
- `stoplist`: Words to remove (set to `None` to disable).
- `minsize`: Minimum token length (default `2`).
- `maxsize`: Maximum token length (default `None`, no limit).
- `gaps`: If True, split on the expression instead of matching it.

**Example:**
```python
an = StandardAnalyzer()
list(an("Testing is testing and testing"))
# => ["testing", "testing", "testing"]
```

#### StemmingAnalyzer

```python
whoosh.analysis.StemmingAnalyzer(
    expression=default_pattern,
    stoplist=STOP_WORDS,
    minsize=2,
    maxsize=None,
    gaps=False,
    stemfn=stem,
    ignore=None,
    cachesize=50000
) -> Analyzer
```

Composes `RegexTokenizer`, `LowercaseFilter`, optional `StopFilter`, and
`StemFilter`.

- `expression`: Regex pattern for tokens.
- `stoplist`: Words to remove (set to `None` to disable).
- `minsize`: Minimum token length (default `2`).
- `maxsize`: Maximum token length.
- `gaps`: If True, split on the expression instead of matching it.
- `stemfn`: Stemming function (default: Porter stemmer for English).
- `ignore`: Words to not stem (set).
- `cachesize`: Stem cache size (default `50000`). Use `-1` for unbounded,
  `None` for no cache.

**Example:**
```python
an = StemmingAnalyzer()
list(an("Testing is testing and testing"))
# => ["test", "test", "test"]
```

#### FancyAnalyzer

```python
whoosh.analysis.FancyAnalyzer(
    expression=r"\s+",
    stoplist=STOP_WORDS,
    minsize=2,
    gaps=True,
    splitwords=True,
    splitnums=True,
    mergewords=False,
    mergenums=False
) -> Analyzer
```

Composes `RegexTokenizer`, `IntraWordFilter`, `LowercaseFilter`, and `StopFilter`.
Splits on whitespace and breaks compound words into subwords.

**Example:**
```python
an = FancyAnalyzer()
list(an("Should I call getInt or get_real?"))
# => ["should", "call", "get", "int", "get", "real"]
```

#### LanguageAnalyzer

```python
whoosh.analysis.LanguageAnalyzer(
    lang,
    expression=default_pattern,
    gaps=False,
    cachesize=50000
) -> Analyzer
```

Configures a language-specific analyzer with `LowercaseFilter`, `StopFilter`,
and `StemFilter`.

- `lang`: Language code (e.g., `"en"`, `"es"`, `"fr"`).
- `expression`: Regex pattern for tokens.
- `gaps`: If True, split on the expression instead of matching it.
- `cachesize`: Stem cache size.

Available languages: `ar`, `da`, `nl`, `en`, `fi`, `fr`, `de`, `hu`, `it`,
`no`, `pt`, `ro`, `ru`, `es`, `sv`, `tr`.

See `whoosh.lang` for `has_stemmer()` and `has_stopwords()` helper functions.

## Tokenizers

All tokenizers inherit from `Tokenizer`.

### Tokenizer

```python
class whoosh.analysis.Tokenizer
```

Base class for tokenizers. Each tokenizer is callable and yields `Token`
objects.

### RegexTokenizer

```python
class whoosh.analysis.RegexTokenizer(
    expression=default_pattern,
    gaps=False
)
```

Uses a regular expression to extract tokens from text. Each match of the
expression equals one token; group 0 (the entire match) is used as the text.

- `expression`: Compiled regex or pattern string.
- `gaps`: If True, split on the expression rather than matching it.

**Example:**
```python
from whoosh.analysis import RegexTokenizer

rext = RegexTokenizer()
list(rext("hi there 3.141 big-time under_score"))
# => ["hi", "there", "3.141", "big", "time", "under_score"]
```

### IDTokenizer

```python
class whoosh.analysis.IDTokenizer
```

Yields the entire input string as a single token. Used for indexed but
untokenized fields (e.g., document paths).

### CharsetTokenizer

```python
class whoosh.analysis.CharsetTokenizer(charmap)
```

Tokenizes and translates text according to a character mapping dictionary.
Characters that map to `None` are treated as token break characters.

- `charmap`: Mapping from integer character codes to unicode characters
  (as used by `unicode.translate()`).

### PathTokenizer

```python
class whoosh.analysis.PathTokenizer(expression="[^/]+")
```

Tokenizes path strings into hierarchical prefixes. Given `"/a/b/c"`, yields
`["/a", "/a/b", "/a/b/c"]`.

### NgramTokenizer

```python
class whoosh.analysis.NgramTokenizer(minsize, maxsize=None)
```

Splits input text into N-grams instead of words. Unlike `RegexTokenizer`, this
tokenizer does not use a regex, so grams may include whitespace and punctuation.

- `minsize`: Minimum N-gram size.
- `maxsize`: Maximum N-gram size (defaults to `minsize`).

**Example:**
```python
from whoosh.analysis import NgramTokenizer

ngt = NgramTokenizer(4)
list(ngt("hi there"))
# => ["hi t", "i th", " the", "ther", "here"]
```

### CachedRegexTokenizer

```python
class whoosh.analysis.CachedRegexTokenizer(
    expression=default_pattern,
    gaps=False,
    maxsize=8192
)
```

A `RegexTokenizer` wrapper that caches tokenization results for repeated
strings, trading memory for speed.

- `expression`: Regex pattern.
- `gaps`: If True, split on the expression.
- `maxsize`: Maximum cache size (LRU eviction when exceeded).

### SpaceSeparatedTokenizer

```python
whoosh.analysis.SpaceSeparatedTokenizer() -> RegexTokenizer
```

Returns a `RegexTokenizer` that splits on whitespace.

### CommaSeparatedTokenizer

```python
whoosh.analysis.CommaSeparatedTokenizer() -> CompositeAnalyzer
```

Returns a composed analyzer that splits on commas and strips whitespace.

## Filters

All filters inherit from `Filter`.

### Filter

```python
class whoosh.analysis.Filter
```

Base class for filters. Subclasses implement `__call__(self, tokens)` which
takes a token generator and returns a token generator.

- `is_morph (bool)`: Set to `True` for morphological filters (e.g., stemming).
  This allows the filter to be bypassed during query analysis if desired.

### STOP_WORDS

```python
whoosh.analysis.STOP_WORDS
```

A frozenset of common English stop words: `"a"`, `"an"`, `"and"`, `"the"`, etc.
Used as the default stoplist for `StopFilter` and `StandardAnalyzer`.

### url_pattern

```python
whoosh.analysis.url_pattern
```

A compiled regex useful for URL filtering.

### LowercaseFilter

```python
class whoosh.analysis.LowercaseFilter
```

Lowercases token text using `unicode.lower()`.

**Example:**
```python
rext = RegexTokenizer() | LowercaseFilter()
list(rext("This is a TEST"))
# => ["this", "is", "a", "test"]
```

### StopFilter

```python
class whoosh.analysis.StopFilter(
    stoplist=STOP_WORDS,
    minsize=2,
    maxsize=None,
    renumber=True,
    lang=None
)
```

Marks and optionally removes stop words from the token stream.

- `stoplist`: Set of words to filter out (defaults to `STOP_WORDS`).
- `minsize`: Minimum token length; shorter tokens are removed (default `2`).
- `maxsize`: Maximum token length; longer tokens are removed (default `None`).
- `renumber`: Renumber positions to account for removed tokens (default `True`).
- `lang`: If set, loads stop words for the given language code.

**Example:**
```python
from whoosh.analysis import RegexTokenizer, StopFilter

stopper = RegexTokenizer() | StopFilter()
list(stopper("this is a test"))
# => ["test"]
```

### StripFilter

```python
class whoosh.analysis.StripFilter
```

Calls `unicode.strip()` on each token's text.

### CharsetFilter

```python
class whoosh.analysis.CharsetFilter(charmap)
```

Translates token text using `unicode.translate()` with the given character map.
Useful for case folding and accent folding.

- `charmap`: Dictionary mapping character ordinals to unicode characters.

**Example:**
```python
from whoosh.support.charset import accent_map

rext = RegexTokenizer() | CharsetFilter(accent_map)
list(rext("café"))
# => ["cafe"]
```

### DelimitedAttributeFilter

```python
class whoosh.analysis.DelimitedAttributeFilter(
    delimiter="^",
    attribute="boost",
    default=1.0,
    type=float
)
```

Looks for delimiter characters in token text and extracts data after the
delimiter into a named token attribute.

- `delimiter`: Separator character (default `"^"`).
- `attribute`: Attribute name on the token (default `"boost"`).
- `default`: Default value if no delimiter is found (default `1.0`).
- `type`: Type to cast the extracted value (default `float`).

**Example:**
```python
from whoosh.analysis import RegexTokenizer, DelimitedAttributeFilter

daf = DelimitedAttributeFilter()
an = RegexTokenizer(r"\S+") | daf
for t in an(u"image 3.14^2 render"):
    print(t.text, t.boost)
# image 1.0
# 3.14 2.0
# render 1.0
```

### SubstitutionFilter

```python
class whoosh.analysis.SubstitutionFilter(pattern, replacement)
```

Performs regex substitution on token text using `re.sub()`.

- `pattern`: Pattern string or compiled regex.
- `replacement`: Replacement text.

**Example:**
```python
from whoosh.analysis import RegexTokenizer, SubstitutionFilter

# Remove hyphens
ana = RegexTokenizer(r"\S+") | SubstitutionFilter("-", "")
```

### MultiFilter

```python
class whoosh.analysis.MultiFilter(**kwargs)
```

Selects between two or more sub-filters based on the `mode` attribute of the
token stream. Useful for using different filters during indexing vs. querying.

- Keyword arguments map mode names to filter instances.

**Example:**
```python
from whoosh.analysis import MultiFilter, IntraWordFilter

iwf_index = IntraWordFilter(mergewords=True, mergenums=True)
iwf_query = IntraWordFilter(mergewords=False, mergenums=False)
mf = MultiFilter(index=iwf_index, query=iwf_query)
```

### TeeFilter

```python
class whoosh.analysis.TeeFilter(*filters)
```

Interleaves the results of two or more filter chains. Requires at least two
filters. Note: this filter is slow because it creates token copies.

**Example:**
```python
# Lowercase in one branch, reverse in another
f1 = LowercaseFilter()
f2 = ReverseTextFilter()
ana = RegexTokenizer(r"\S+") | TeeFilter(f1, f2)
```

### ReverseTextFilter

```python
class whoosh.analysis.ReverseTextFilter
```

Reverses the text of each token.

**Example:**
```python
an = RegexTokenizer() | ReverseTextFilter()
list(an("hello there"))
# => ["olleh", "ereht"]
```

### PassFilter

```python
class whoosh.analysis.PassFilter
```

Identity filter; passes tokens through unchanged.

### LoggingFilter

```python
class whoosh.analysis.LoggingFilter(logger=None)
```

Prints debug log entries for every token that passes through.

- `logger`: Logger instance (defaults to `whoosh.analysis` logger).

## Intraword Filters

### IntraWordFilter

```python
class whoosh.analysis.IntraWordFilter(
    delims="-_'\"()!@#$%^&*[]{}<>\\|;:,./?`~+=",
    splitwords=True,
    splitnums=True,
    mergewords=False,
    mergenums=False
)
```

Splits words into subwords and performs optional merging. Based on
WordDelimiterFilter in Solr.

- `delims`: String of delimiter characters.
- `splitwords`: Split at case transitions (e.g., `PowerShot` → `Power`, `Shot`).
- `splitnums`: Split at letter-number transitions (e.g., `SD500` → `SD`, `500`).
- `mergewords`: Merge consecutive alphabetic subwords.
- `mergenums`: Merge consecutive numeric subwords.

### CompoundWordFilter

```python
class whoosh.analysis.CompoundWordFilter(wordset, keep_compound=True)
```

Breaks compound tokens into their constituent parts if they match words in the
given wordset. Useful for agglutinative languages and trademarks.

- `wordset`: A set (or any `__contains__` object) of known words.
- `keep_compound`: If True, keep the original compound token in the stream.

### BiWordFilter

```python
class whoosh.analysis.BiWordFilter(sep="-")
```

Merges adjacent tokens into bigram tokens. Useful for pseudo-phrase searching.

- `sep`: Separator string for bigrams.

### ShingleFilter

```python
class whoosh.analysis.ShingleFilter(size=2, sep="-")
```

Merges N adjacent tokens into multi-word tokens (shingles).

- `size`: Number of tokens to combine.
- `sep`: Separator string.

**Note:** For `size=2`, `BiWordFilter` is faster.

## Morphological Filters

### StemFilter

```python
class whoosh.analysis.StemFilter(
    stemfn=stem,
    lang=None,
    ignore=None,
    cachesize=50000
)
```

Stems tokens using the Porter stemming algorithm (or a language-specific
stemmer if `lang` is specified).

- `stemfn`: Stemming function (default: Porter stemmer).
- `lang`: Language code to override `stemfn` with a Snowball stemmer.
- `ignore`: Set of words to not stem (defaults to stemming all words).
- `cachesize`: Cache size for stemmed words. Use `-1` for unbounded,
  `None` for no cache.

**Example:**
```python
from whoosh.analysis import RegexTokenizer, StemFilter

stemmer = RegexTokenizer() | StemFilter()
list(stemmer("fundamentally willows"))
# => ["fundament", "willow"]
```

### PyStemmerFilter

```python
class whoosh.analysis.PyStemmerFilter(
    lang="english",
    ignore=None,
    cachesize=10000
)
```

Subclass of `StemFilter` that uses the third-party `py-stemmer` library.
Requires the py-stemmer package to be installed.

**Methods:**
- `algorithms()`: Returns available stemming algorithms from py-stemmer.

### DoubleMetaphoneFilter

```python
class whoosh.analysis.DoubleMetaphoneFilter(
    primary_boost=1.0,
    secondary_boost=0.5,
    combine=False
)
```

Encodes tokens using Lawrence Philips's Double Metaphone algorithm. Useful
for phonetic matching of names and places.

- `primary_boost`: Boost factor for the primary code token.
- `secondary_boost`: Boost factor for the secondary code token.
- `combine`: If True, keep the original token alongside the encoded tokens.

## N-gram Filters and Analyzers

### NgramFilter

```python
class whoosh.analysis.NgramFilter(minsize, maxsize=None, at=None)
```

Splits token text into N-grams of varying sizes.

- `minsize`: Minimum N-gram size.
- `maxsize`: Maximum N-gram size (defaults to `minsize`).
- `at`: `'start'` for prefix grams, `'end'` for suffix grams, or `None`
  for all position grams.

### NgramAnalyzer

```python
whoosh.analysis.NgramAnalyzer(minsize, maxsize=None) -> Analyzer
```

Composes `NgramTokenizer` with `LowercaseFilter`.

### NgramWordAnalyzer

```python
whoosh.analysis.NgramWordAnalyzer(
    minsize,
    maxsize=None,
    tokenizer=None,
    at=None
) -> Analyzer
```

Composes `RegexTokenizer`, `LowercaseFilter`, and `NgramFilter`. Use this
when you want sub-word n-grams (without whitespace) rather than raw
character n-grams.


## DOCUMENT (FR): Automata

> **Note de traduction** : Cette page n'est pas encore traduite en français.
> Le contenu anglais est affiché ci-dessous en attendant la traduction.

<!-- Creez une version francaise de ce fichier et supprimez ce message. -->


# Automata API

Module for constructing and manipulating finite state automata (FSAs),
including NFAs, DFAs, finite state transducers (FSTs), Levenshtein
automata, and regular expression automata. Used internally for spelling
correction, fuzzy term queries, and term dictionary operations.

The automata module is a refactored package with submodules. All classes
and functions are importable directly from `whoosh.automata`.

## Module Functions

### `parse_glob`

```python
whoosh.automata.parse_glob(pattern, _glob_multi="*", _glob_single="?", _glob_range1="[", _glob_range2="]") -> NFA
```

Parses a glob-style pattern string and returns an NFA that matches strings
matching the pattern.

**Parameters:**
- `pattern`: Glob pattern string (`*` matches any sequence, `?` matches any
  single character).
- `_glob_multi`, `_glob_single`: Override the wildcard characters.
- `_glob_range1`, `_glob_range2`: Override the range syntax brackets.

### `glob_automaton`

```python
whoosh.automata.glob_automaton(pattern) -> NFA
```

Convenience function that parses a glob pattern and returns an NFA.

## FSA (Finite State Automaton) Classes

### `FSA`

```python
class whoosh.automata.FSA(initial)
```

Base class for finite state automata.

**Constructor:**
- `initial`: The initial state.

**Attributes:**
- `initial`: Initial state.
- `transitions`: Dict mapping source states to dicts mapping labels to
  target states.
- `final_states`: Set of accepting (final) states.

**Methods:**
- `__eq__(other)`: Compares initial state, final states, and transitions.
- `all_states()`: Returns a set of all states reachable in the automaton.
- `all_labels()`: Returns a set of all transition labels.
- `get_labels(src)`: Yields all labels leaving state `src`.
- `generate_all(state=None, sofar="")`: Yields all strings accepted by the
  automaton.
- `move(state, label)`: Returns the state reached by following `label` from
  `state`, or `None`.
- `moves(state, labels)`: Yields `(label, next_state)` pairs.
- `next(state)`: Yields target states reachable from `state` via any label.
- `is_final(state)`: Returns `True` if `state` is a final state.
- `start()`: Returns the initial state.
- `has_path_to(target)`: Returns `True` if there is a path to `target`.

### `Marker`

```python
class whoosh.automata.Marker(name)
```

Marker object used as a special transition label in NFAs (e.g., `ANY`,
`EPSILON`).

### `EPSILON`

```python
whoosh.automata.EPSILON = Marker("EPSILON")
```

Special marker representing an epsilon transition (no input consumed).

### `ANY`

```python
whoosh.automata.ANY = Marker("ANY")
```

Special marker representing a transition that matches any input character.

### `NFA`

```python
class whoosh.automata.NFA(initial)
```

Nondeterministic Finite Automaton. Extends `FSA` with epsilon transitions
and NFA-specific construction methods.

**Methods:**
- `add_transition(src, label, dst)`: Adds a transition from `src` to `dst`
  consuming `label`.
- `add_final_state(state, final=True)`: Marks `state` as a final/accepting
  state.
- `epsilon_closure(state)`: Returns the set of states reachable from `state`
  via epsilon transitions.
- `to_dfa()`: Converts this NFA to an equivalent DFA and returns it.

### `DFA`

```python
class whoosh.automata.DFA(initial)
```

Deterministic Finite Automaton. Extends `FSA` with DFA-specific operations.

**Methods:**
- `next_valid_string(string)`: Finds the lexicographically smallest string
  accepted by the DFA that is greater than or equal to `string`.
- `to_dfa()`: Returns self (already a DFA).

### `renumber_dfa`

```python
whoosh.automata.renumber_dfa(dfa, base=0) -> DFA
```

Renumerates the states of a DFA to integers starting at `base`.

### `u_to_utf8`

```python
whoosh.automata.u_to_utf8(dfa, base=0) -> DFA
```

Converts a Unicode DFA to a UTF-8 DFA.

### `find_all_matches`

```python
whoosh.automata.find_all_matches(dfa, lookup_func, first=unull)
```

Yields all strings accepted by the DFA, using `lookup_func` to determine
which strings exist in the dictionary.

**Parameters:**
- `dfa`: A deterministic finite automaton.
- `lookup_func`: Function called with each candidate string; returns the
  string if found in the dictionary.
- `first`: First string to start matching from (default `chr(0)`).

### `reverse_nfa`

```python
whoosh.automata.reverse_nfa(n) -> NFA
```

Returns the reverse of an NFA (reversed transitions, swapped initial
and final states).

### `product`

```python
whoosh.automata.product(dfa1, op, dfa2) -> DFA
```

Computes the product of two DFAs using a binary operation.

**Parameters:**
- `dfa1`, `dfa2`: Input DFAs.
- `op`: A function `(set1, set2) -> set` computing the output final states
  from the two input final state sets.

### `intersection`

```python
whoosh.automata.intersection(dfa1, dfa2) -> DFA
```

Returns the intersection of two DFAs.

### `union`

```python
whoosh.automata.union(dfa1, dfa2) -> DFA
```

Returns the union of two DFAs.

### `epsilon_nfa`

```python
whoosh.automata.epsilon_nfa() -> NFA
```

Returns an NFA that accepts only the empty string.

### `dot_nfa`

```python
whoosh.automata.dot_nfa() -> NFA
```

Returns an NFA that accepts any single character.

### `basic_nfa`

```python
whoosh.automata.basic_nfa(label) -> NFA
```

Returns an NFA that accepts exactly the string `label`.

### `charset_nfa`

```python
whoosh.automata.charset_nfa(labels) -> NFA
```

Returns an NFA that accepts any single character in `labels`.

### `string_nfa`

```python
whoosh.automata.string_nfa(string) -> NFA
```

Returns an NFA that accepts exactly `string`.

### `choice_nfa`

```python
whoosh.automata.choice_nfa(n1, n2) -> NFA
```

Returns an NFA that accepts strings accepted by either `n1` or `n2`.

### `concat_nfa`

```python
whoosh.automata.concat_nfa(n1, n2) -> NFA
```

Returns an NFA that accepts the concatenation of `n1` and `n2`.

### `star_nfa`

```python
whoosh.automata.star_nfa(n) -> NFA
```

Returns an NFA that accepts zero or more repetitions of `n`.

### `plus_nfa`

```python
whoosh.automata.plus_nfa(n) -> NFA
```

Returns an NFA that accepts one or more repetitions of `n`.

### `optional_nfa`

```python
whoosh.automata.optional_nfa(n) -> NFA
```

Returns an NFA that accepts zero or one occurrence of `n`.

### `strings_dfa`

```python
whoosh.automata.strings_dfa(strings) -> DFA
```

Constructs a minimal DFA that accepts exactly the given strings.

### `add_suffix`

```python
whoosh.automata.add_suffix(dfa, nodes, last, downto, seen)
```

Internal function for adding suffixes to a trie during DFA construction.

## Levenshtein Automata

### `levenshtein_automaton`

```python
whoosh.automata.levenshtein_automaton(term, k, prefix=0) -> NFA
```

Constructs an NFA that matches all strings within edit distance `k` of
`term`. This is the core function for fuzzy term queries and spelling
suggestions.

**Parameters:**
- `term`: The reference string to compute edit distance from.
- `k`: Maximum edit distance (number of insertions, deletions, or
  substitutions).
- `prefix`: If positive, require matched strings to share this length of
  prefix with `term` (speeds up matching significantly).

**Returns:** An NFA that can be converted to a DFA via `.to_dfa()`.

```python
from whoosh.automata import levenshtein_automaton

nfa = levenshtein_automaton("hello", k=1, prefix=0)
dfa = nfa.to_dfa()
```

## RegEx

### `parse`

```python
whoosh.automata.parse(pattern) -> NFA
```

Parses a regular expression pattern string and returns an NFA.

**Parameters:**
- `pattern`: A regex pattern string (Python `re`-style syntax).

### `RegexBuilder`

```python
class whoosh.automata.RegexBuilder(pattern)
```

Helper class for building NFAs from regex patterns.

## FST (Finite State Transducer) Classes

### `Values`

```python
class whoosh.automata.Values
```

Abstract base class for value types stored in FST arcs.

### `IntValues`

```python
class whoosh.automata.IntValues
```

Stores integer values in FST arcs.

### `SequenceValues`

```python
class whoosh.automata.SequenceValues
```

Base class for value types that store sequences of values.

### `BytesValues`

```python
class whoosh.automata.BytesValues
```

Stores byte string values in FST arcs.

### `ArrayValues`

```python
class whoosh.automata.ArrayValues
```

Stores arrays of values in FST arcs.

### `IntListValues`

```python
class whoosh.automata.IntListValues
```

Stores lists of integers in FST arcs.

### `Node`

```python
class whoosh.automata.Node
```

Base class for nodes in an FST.

### `ComboNode`

```python
class whoosh.automata.ComboNode
```

Base class for nodes that combine multiple sub-nodes (intersection, union).

### `UnionNode`

```python
class whoosh.automata.UnionNode
```

A node that represents the union of multiple sub-nodes.

### `IntersectionNode`

```python
class whoosh.automata.IntersectionNode
```

A node that represents the intersection of multiple sub-nodes.

### `BaseCursor`

```python
class whoosh.automata.BaseCursor
```

Base class for cursors that iterate over FST contents.

### `Cursor`

```python
class whoosh.automata.Cursor
```

Concrete cursor for iterating over an FST, supporting `next()`, `find()`,
`text()`, and other navigation methods.

### `UncompiledNode`

```python
class whoosh.automata.UncompiledNode
```

Represents an FST node that has not yet been compiled into a binary
representation. Used during FST construction.

### `Arc`

```python
class whoosh.automata.Arc
```

Represents a single arc in an FST, with a label, target node, and associated
value.

### `GraphWriter`

```python
class whoosh.automata.GraphWriter
```

Writes an FST to a binary file on disk or to an in-memory buffer.

### `BaseGraphReader`

```python
class whoosh.automata.BaseGraphReader
```

Base class for reading FSTs from disk.

### `GraphReader`

```python
class whoosh.automata.GraphReader
```

Concrete reader for FSTs stored on disk. Supports `find()`, `next()`, and
`text()` for navigating the graph.

### `to_labels`

```python
whoosh.automata.to_labels(key)
```

Converts a key (string, int, etc.) into a list of FST arc labels.

### `within`

```python
whoosh.automata.within(graph, text, k=1, prefix=0, address=None)
```

Uses a pre-built FST and a Levenshtein automaton to find all keys in the
graph within edit distance `k` of `text`.

**Parameters:**
- `graph`: A `GraphReader` instance.
- `text`: The search term.
- `k`: Maximum edit distance.
- `prefix`: Required shared prefix length.
- `address`: Optional starting address in the graph.

### `dump_graph`

```python
whoosh.automata.dump_graph(graph, address=None, tab=0, out=None)
```

Debug utility that prints the structure of an FST to stdout or a file.

### `FileVersionError`

```python
class whoosh.automata.FileVersionError
```

Raised when reading an FST file with an incompatible version.

### `InactiveCursor`

```python
class whoosh.automata.InactiveCursor
```

Raised when operating on a cursor that is not at a valid position.


## DOCUMENT (FR): Backends

# API Backends

Architecture de stockage pliable via backends.

## FileBackend (défaut)

```python
class whoosh.backends.file.FileBackend
```

Backend par défaut stockant les segments comme fichiers sur disque.

### Options

| Paramètre | Description |
|-----------|-------------|
| `storage` | Instance de storage (FileStorage par défaut) |
| `limitmb` | Taille maximum des segments (MiB) |

**Exemple:**
```python
from whoosh import index

ix = index.create_in("indexdir", schema)
# Utilise FileBackend implicitement
```

## SQLiteBackend

```python
class whoosh.backends.sqlite.SQLiteBackend
```

Stocke l'index entier dans une base de données SQLite.

### Options

| Paramètre | Description |
|-----------|-------------|
| `storage` | `SQLiteStorage(path)` |
| `writethrough` | Écriture synchrone |

**Exemple:**
```python
from whoosh.backends.sqlite import SQLiteStorage, SQLiteBackend

storage = SQLiteStorage("mon_index.db")
backend = SQLiteBackend(storage=storage)

ix = backend.create_index(schema)
```

## MemoryBackend

```python
class whoosh.backends.memory.MemoryBackend
```

Backend en mémoire (tests uniquement, données perdues au redémarrage).

## Classes Storage

### FileStorage

```python
class whoosh.store.FileStorage
```

Gère les fichiers sur disque.

### SQLiteStorage

```python
class whoosh.store.SQLiteStorage(db_path)
```

Gère le stockage SQLite.

## ProviderRegistry

```python
class whoosh.registry.ProviderRegistry
```

Registre pour les providers de stockage:

```python
from whoosh.registry import ProviderRegistry

ProviderRegistry.register("sqlite", SQLiteBackend(), "mon_app")
```


## DOCUMENT (FR): Classify

> **Note de traduction** : Cette page n'est pas encore traduite en français.
> Le contenu anglais est affiché ci-dessous en attendant la traduction.

<!-- Creez une version francaise de ce fichier et supprimez ce message. -->


# Classify API

Classes and functions for classifying and extracting information from
documents. This module provides query expansion models, similarity
functions (shingling, simhash), and clustering algorithms.

## Expansion Models

### `ExpansionModel`

```python
class whoosh.classify.ExpansionModel(doc_count, field_length)
```

Abstract base class for query expansion models. Subclass to implement custom
expansion scoring.

**Constructor:**
- `doc_count`: Total number of documents in the collection.
- `field_length`: Total length of the field across all documents.

**Computed Attributes:**
- `N`: Document count.
- `collection_total`: Total field length.
- `mean_length`: Average field length (`collection_total / N`).

**Methods:**
- `normalizer(maxweight, top_total)`: Returns a normalization factor.
- `score(weight_in_top, weight_in_collection, top_total)`: Returns the
  expansion score for a term.

### `Bo1Model`

```python
class whoosh.classify.Bo1Model(doc_count, field_length)
```

Bayesian One-Poisson expansion model. One of the standard query expansion
models.

### `Bo2Model`

```python
class whoosh.classify.Bo2Model(doc_count, field_length)
```

Bayesian Two-Poisson expansion model. Another standard query expansion model.

### `KLModel`

```python
class whoosh.classify.KLModel(doc_count, field_length)
```

Kullback-Leibler divergence-based expansion model.

## Expander

### `Expander`

```python
class whoosh.classify.Expander(
    ixreader,
    fieldname,
    model=Bo1Model
)
```

Uses an `ExpansionModel` to expand the set of query terms based on the top N
result documents.

**Constructor:**
- `ixreader`: An `IndexReader` object.
- `fieldname`: The name of the field to expand terms from.
- `model`: An `ExpansionModel` class or instance. Defaults to `Bo1Model`.

**Methods:**

#### `add(vector)`

Adds forward-index information about one of the "top N" documents.

- `vector`: A series of `(text, weight)` tuples, such as is returned by
  `Reader.vector_as("weight", docnum, fieldname)`.

#### `add_document(docnum)`

Adds a document's term vector to the expander. If the field has a term vector,
uses it; otherwise falls back to stored field text.

#### `add_text(string)`

Adds a text string by indexing it with the field's analyzer.

#### `expanded_terms(number, normalize=True)`

Returns the N most important terms in the vectors added so far, ranked by
the expansion model's score.

- `number`: Number of terms to return.
- `normalize`: Whether to normalize weights.
- Returns: List of `(term, weight)` tuples, sorted by weight descending.

```python
from whoosh.classify import Expander, Bo1Model

expander = Expander(ix.reader(), "content")
for docnum in results.ids()[:10]:
    expander.add_document(docnum)

for word, weight in expander.expanded_terms(5):
    print(word, weight)
```

## Similarity Functions

### `shingles`

```python
whoosh.classify.shingles(input, size=2) -> iterable
```

Generates `(shingle, frequency)` pairs from a string by sliding a window of
the given size over the input.

**Parameters:**
- `input`: The input string.
- `size`: The shingle size (default `2`).

```python
from whoosh.classify import shingles

for shingle, freq in shingles("hello world", size=2):
    print(shingle, freq)
```

### `simhash`

```python
whoosh.classify.simhash(features, hashbits=32) -> int
```

Computes a simhash (perceptual hash) from a sequence of weighted features.
Simhashes that are similar produce similar hash values, allowing fast
near-duplicate detection via Hamming distance.

**Parameters:**
- `features`: Iterable of `(feature, weight)` tuples.
- `hashbits`: Number of bits in the hash (default `32`).
- Returns: An integer hash value.

```python
from whoosh.classify import shingles, simhash

h1 = simhash(shingles(text1))
h2 = simhash(shingles(text2))
from whoosh.classify import hamming_distance
dist = hamming_distance(h1, h2)
```

### `hamming_distance`

```python
whoosh.classify.hamming_distance(first_hash, other_hash, hashbits=32) -> int
```

Computes the Hamming distance between two hash values. A small distance
indicates high similarity.

**Parameters:**
- `first_hash`: First hash integer.
- `other_hash`: Second hash integer.
- `hashbits`: Number of bits in the hashes (default `32`).

## Clustering

### `kmeans`

```python
whoosh.classify.kmeans(
    data,
    k,
    t=0.0001,
    distfun=None,
    maxiter=50,
    centers=None
) -> (labels, centroids)
```

One-dimensional K-means clustering. Assigns each data point to the nearest
of `k` centroids and returns cluster labels and final centroids.

**Parameters:**
- `data`: List of data points (numeric values).
- `k`: Number of clusters.
- `t`: Tolerance; stops if centroid changes are below this value.
- `distfun`: Optional distance function (unused if `None`).
- `maxiter`: Maximum iterations (default `50`).
- `centers`: Optional list of initial centroids. If `None`, selects `k`
  random points from `data`.

**Returns:** A tuple `(labels, centroids)` where `labels` is a list of
cluster assignments per data point and `centroids` is the list of final
centroid positions.

### `two_pass_variance`

```python
whoosh.classify.two_pass_variance(data) -> float
```

Computes the sample variance of a data list using the two-pass algorithm
(first pass computes the mean, second pass accumulates squared deviations).

### `weighted_incremental_variance`

```python
whoosh.classify.weighted_incremental_variance(data_weight_pairs) -> float
```

Computes the weighted variance incrementally from a sequence of
`(value, weight)` pairs.

### `swin`

```python
whoosh.classify.swin(data, size) -> list
```

Sliding window clustering that groups data points where the range (max - min)
within a window of `size` is below a threshold. Uses variance for ranking.

**Parameters:**
- `data`: Sorted list of data points.
- `size`: Maximum window range (max - min) for clustering.

**Returns:** A list of `(left, right, count, variance)` tuples representing
clusters, sorted by count descending then by variance ascending.


## DOCUMENT (FR): Codecs

> **Note de traduction** : Cette page n'est pas encore traduite en français.
> Le contenu anglais est affiché ci-dessous en attendant la traduction.

<!-- Creez une version francaise de ce fichier et supprimez ce message. -->


# Codecs API

Classes and interfaces for how Whoosh writes and reads the inverted index,
postings, and per-document values. The codecs module is a refactored package
exposing the same public API as the former monolithic module.

## Module Functions

### `default_codec`

```python
whoosh.codec.default_codec(*args, **kwargs) -> Codec
```

Returns the default codec used by the index. Currently returns a
`W3Codec` instance.

```python
from whoosh.codec import default_codec
codec = default_codec()
```

## Exceptions

### `OutOfOrderError`

```python
whoosh.codec.OutOfOrderError
```

Raised when documents are added to a field out of order. Fields must
receive documents in ascending docnum order.

## Base Classes

### `Codec`

```python
class whoosh.codec.Codec
```

Abstract base class for index codecs. Subclasses implement methods for
writing and reading the index format.

**Class Attributes:**
- `length_stats (bool)`: If `True`, the codec stores per-document field
  length statistics. Default `True`.

**Methods:**

#### `per_document_writer(storage, segment)`

Abstract. Returns a `PerDocumentWriter` for writing per-document values
(columns, term vectors) to the given segment.

#### `field_writer(storage, segment)`

Abstract. Returns a `FieldWriter` for writing postings to the given segment.

#### `postings_writer(dbfile, byteids=False)`

Abstract. Returns a `PostingsWriter` for writing posting lists to `dbfile`.

#### `postings_reader(dbfile, terminfo, format_, term=None, scorer=None)`

Abstract. Returns a `Matcher` for reading postings from `dbfile`.

#### `automata(storage, segment)`

Returns an `Automata` instance for spelling correction using automata-based
edit distance. Default returns a base `Automata()` object.

#### `terms_reader(storage, segment)`

Abstract. Returns a `TermsReader` for reading the term dictionary and
postings of the given segment.

#### `per_document_reader(storage, segment)`

Abstract. Returns a `PerDocumentReader` for reading per-document values
from the given segment.

#### `new_segment(storage, indexname)`

Abstract. Creates and returns a new `Segment` object for the given storage
and index name.

### `WrappingCodec`

```python
class whoosh.codec.WrappingCodec(child)
```

A `Codec` that delegates all operations to a child codec. Useful for
creating codec wrappers that modify or intercept specific operations.

**Constructor:**
- `child`: The underlying `Codec` instance to wrap.

All methods delegate to the child codec:
`per_document_writer()`, `field_writer()`, `postings_writer()`,
`postings_reader()`, `automata()`, `terms_reader()`, `per_document_reader()`,
`new_segment()`.

## Writer Classes

### `PerDocumentWriter`

```python
class whoosh.codec.PerDocumentWriter
```

Abstract base class for writing per-document values (columns, term vectors).

**Methods:**

#### `start_doc(docnum)`

Abstract. Called when starting to write a new document.

#### `add_field(fieldname, fieldobj, value, length)`

Abstract. Adds a field value to the current document.

#### `add_column_value(fieldname, columnobj, value)`

Abstract. Adds a column value. Raises `NotImplementedError` if the codec
doesn't support columns.

#### `add_vector_items(fieldname, fieldobj, items)`

Abstract. Adds term vector items.

#### `add_vector_matcher(fieldname, fieldobj, vmatcher)`

Convenience method that reads items from a `Matcher` and calls
`add_vector_items()`.

#### `finish_doc()`

Called when finishing a document. Default does nothing.

#### `close()`

Called when done writing. Default does nothing.

### `FieldWriter`

```python
class whoosh.codec.FieldWriter
```

Abstract base class for writing postings (inverted index) data.

**Methods:**

#### `add_postings(schema, lengths, items)`

Translates a generator of `(fieldname, btext, docnum, weight, vbytes)`
tuples into calls to `start_field()`, `start_term()`, `add()`,
`finish_term()`, and `finish_field()`.

**Parameters:**
- `schema`: The `Schema` object.
- `lengths`: Optional `FieldLengthTable` for document field lengths.
- `items`: Iterable of posting tuples.

#### `start_field(fieldname, fieldobj)`

Abstract. Called when starting a new field.

#### `start_term(text)`

Abstract. Called when starting a new term within a field.

#### `add(docnum, weight, vbytes, length=None)`

Abstract. Adds a posting to the current term.

#### `add_spell_word(fieldname, text)`

Called to add a word to the spelling index. Default does nothing.

#### `finish_term()`

Abstract. Called when finishing a term.

#### `finish_field()`

Called when finishing a field. Default does nothing.

#### `close()`

Called when done writing. Default does nothing.

### `PostingsWriter`

```python
class whoosh.codec.PostingsWriter
```

Abstract base class for writing posting lists (the inverted index).

**Methods:**

#### `start_postings(format_, terminfo)`

Abstract. Starts writing postings for a new term.

#### `add_posting(id_, weight, vbytes, length=None)`

Abstract. Adds a posting to the current term.

#### `finish_postings(allow_compact=True)`

Called when finished writing postings. Default does nothing.

#### `written()`

Abstract. Returns `True` if this writer has already written to disk.

## Reader Classes

### `FieldCursor`

```python
class whoosh.codec.FieldCursor
```

Abstract base class for iterating over terms in a field.

**Methods:**
- `first()`: Move to the first term.
- `find(string)`: Find a term matching or closest to `string`.
- `next()`: Move to the next term.
- `term()`: Returns the current term's text.

### `EmptyCursor`

```python
class whoosh.codec.EmptyCursor
```

A `FieldCursor` representing an empty field. All methods return `None` or
`False`.

### `TermsReader`

```python
class whoosh.codec.TermsReader
```

Abstract base class for reading the term dictionary and postings of a
segment.

**Methods:**
- `__contains__(term)`: Returns `True` if the term exists.
- `cursor(fieldname, fieldobj)`: Returns a `FieldCursor`.
- `terms()`: Yields `(fieldname, text)` tuples for all terms.
- `terms_from(fieldname, prefix)`: Yields terms from `fieldname` starting
  with `prefix`.
- `items()`: Yields `((fieldname, text), TermInfo)` tuples.
- `items_from(fieldname, prefix)`: Like `items()` but filtered by prefix.
- `term_info(fieldname, text)`: Returns a `TermInfo` for the term.
- `frequency(fieldname, text)`: Returns the total frequency.
- `doc_frequency(fieldname, text)`: Returns the document frequency.
- `matcher(fieldname, text, format_, scorer=None)`: Returns a `Matcher`.
- `indexed_field_names()`: Yields names of indexed fields.
- `close()`: Close the reader.

### `PerDocumentReader`

```python
class whoosh.codec.PerDocumentReader
```

Abstract base class for reading per-document values (columns, term vectors,
stored fields).

**Methods:**
- `close()`: Close the reader.
- `doc_count()`: Returns number of non-deleted documents.
- `doc_count_all()`: Returns total document count (including deleted).
- `has_deletions()`: Returns `True` if any documents are deleted.
- `is_deleted(docnum)`: Returns `True` if docnum is deleted.
- `deleted_docs()`: Yields docnums of deleted documents.
- `all_doc_ids()`: Yields docnums of all non-deleted documents.
- `supports_columns()`: Returns `True` if column storage is supported.
- `has_column(fieldname)`: Returns `True` if field has a column.
- `list_columns()`: Yields names of available columns.
- `column_reader(fieldname, column)`: Returns a column reader.
- `doc_field_length(docnum, fieldname)`: Returns field length for docnum.
- `field_length(fieldname)`: Returns total field length.
- `min_field_length(fieldname)`: Returns minimum field length.
- `max_field_length(fieldname)`: Returns maximum field length.
- `has_vector(docnum, fieldname)`: Returns `True` if docnum has a vector.
- `vector(docnum, fieldname, format_)`: Returns a `Matcher` for the vector.
- `stored_fields(docnum)`: Returns dict of stored field values.
- `all_stored_field()`: Yields stored fields for all documents.

### `MultiPerDocumentReader`

```python
class whoosh.codec.MultiPerDocumentReader(readers, offset=0)
```

Combines multiple `PerDocumentReader` instances into one for multi-segment
indices.

**Constructor:**
- `readers`: List of `PerDocumentReader` instances.
- `offset`: Base document offset (usually `0`).

## Automata

### `Automata`

```python
class whoosh.codec.Automata
```

Provides static methods for automata-based term matching, used by the
spelling corrector.

**Static Methods:**

#### `levenshtein_dfa(uterm, maxdist, prefix=0)`

Returns a deterministic finite automaton (DFA) that matches all edit-distance
variants of `uterm` within `maxdist` edits, optionally requiring a minimum
shared prefix of length `prefix`.

#### `find_matches(dfa, cur)`

Given a DFA and a `FieldCursor`, yields all matching terms.

**Methods:**

#### `terms_within(fieldcur, uterm, maxdist, prefix=0)`

Returns an iterator of matching terms within the given edit distance of
`uterm`.

## Segment

### `Segment`

```python
class whoosh.codec.Segment
```

Represents a segment of the index. Instances are pickled into the TOC file
to describe on-disk files.

**Class Attributes:**
- `COMPOUND_EXT = ".seg"`: Extension for compound segment files.

**Instance Attributes:**
- `indexname`: Base name of the segment.
- `segid`: Random unique ID string.
- `compound (bool)`: Whether this segment uses compound file format.

**Methods:**
- `make_filename(ext)`: Returns `f"{segment_id()}{ext}"`.
- `list_files(storage)`: Lists all files belonging to this segment.
- `create_file(storage, ext, **kwargs)`: Creates a new file for this segment.
- `open_file(storage, ext, **kwargs)`: Opens a file for this segment.
- `create_compound_file(storage)`: Combines all segment files into a
  compound `.seg` file.
- `open_compound_file(storage)`: Opens the compound segment file.
- `doc_count_all()`: Abstract. Returns total document count.
- `doc_count()`: Returns non-deleted document count.
- `set_doc_count(doccount)`: Sets the document count.
- `has_deletions()`: Returns `True` if any documents are deleted.
- `deleted_count()`: Abstract. Returns number of deleted documents.
- `deleted_docs()`: Abstract. Yields docnums of deleted documents.
- `delete_document(docnum, delete=True)`: Abstract. Deletes/undeletes a
  document.
- `is_deleted(docnum)`: Abstract. Returns `True` if docnum is deleted.
- `should_assemble()`: Returns `True` by default. Override to control
  compound file behavior.
- `validate(storage)`: Checks on-disk integrity of this segment.
- `segment_id()`: Returns the unique segment identifier string.
- `is_compound()`: Returns `True` if this segment uses compound file format.

### `WrappingSegment`

```python
class whoosh.codec.WrappingSegment(child)
```

A `Segment` that delegates all operations to a child segment.

**Constructor:**
- `child`: The underlying `Segment` instance to wrap.

## W3 Codec (Default)

The `W3` codec ("Whoosh 3") is the default index format, storing postings in
compressed blocks for efficient reading and skipping.

### `W3Codec`

```python
class whoosh.codec.whoosh3.W3Codec(blocklimit=128, compression=3, inlinelimit=1)
```

The default codec. Uses compressed blocks and term inlining for efficient
storage and fast lookups.

**Constructor:**
- `blocklimit`: Number of postings per block (default `128`).
- `compression`: zlib compression level (default `3`, `0` = no compression).
- `inlinelimit`: Maximum number of postings to inline directly in the term
  info (default `1`).

**File Extensions:**
- `.trm`: Term dictionary
- `.pst`: Postings
- `.vps`: Vector postings
- `.col`: Per-document value columns

### `W3PerDocWriter`

Writer for per-document values using the W3 format. Handles columns,
stored fields, term vectors, and field lengths.

### `W3FieldWriter`

Writer for the inverted term index using the W3 format. Uses a
`OrderedHashWriter` for the term dictionary and posts to a postings file.

### `W3LeafMatcher`

```python
class whoosh.codec.whoosh3.W3LeafMatcher(postfile, startoffset, length, format_, term=None, byteids=None, scorer=None)
```

Reads on-disk postings from the postings file and presents the
`Matcher` interface. Supports block-level skipping and lazy block loading.

**Optimization methods:**
- `block_min_id()`: Returns the first doc ID in the current block.
- `block_max_id()`: Returns the last doc ID in the current block.
- `block_min_length()`: Returns the minimum field length in the current block.
- `block_max_length()`: Returns the maximum field length in the current block.
- `block_max_weight()`: Returns the maximum weight in the current block.
- `skip_to_quality(minquality)`: Skips blocks exceeding a quality threshold.

### `W3TermsReader`

Reader for the term dictionary using the W3 format. Uses an
`OrderedHashReader` for fast lookups.

### `W3TermInfo`

```python
class whoosh.codec.whoosh3.W3TermInfo
```

Stores term statistics and posting location information. Supports inlining
small posting sets directly in the term dictionary for fast lookups.

**Flags:**
- `_FLAG_OFFSET` (0): Postings stored at an offset in the postings file.
- `_FLAG_INLINE_PICKLE` (1): Postings inlined as a pickled tuple.
- `_FLAG_INLINE_COMPACT` (2): Single posting compactly inlined.
- `_FLAG_INLINE_COMPACT_SHORT` (3): Multiple postings compactly inlined.

**Methods:**
- `add_block(block)`: Merges block statistics into this term info.
- `set_extent(offset, length)`: Sets offset and length of postings in file.
- `extent()`: Returns `(offset, length)`.
- `set_inlined(ids, weights, values)`: Sets inlined posting data.
- `set_compact_inline(id_, weight, value)`: Sets single inlined posting.
- `set_compact_short_inline(ids, weights, values)`: Sets multiple compact
  inlined postings.
- `is_inlined()`: Returns `True` if postings are inlined.
- `inlined_postings()`: Returns `(ids, weights, values)` tuples for inlined
  postings.
- `to_bytes()` / `from_bytes()`: Serialize/deserialize.

### `W3Segment`

```python
class whoosh.codec.whoosh3.W3Segment(codec, indexname, doccount=0, segid=None, deleted=None)
```

Segment class for the W3 codec. Stores a reference to the codec, document
count, and deleted document set.

## Plain Text Codec (Debugging)

### `PlainTextCodec`

```python
class whoosh.codec.plaintext.PlainTextCodec
```

A codec that stores the index as human-readable plain text. Intended for
debugging and manual inspection, not for production use.

**Class Attributes:**
- `length_stats = False`

**File extensions:**
- `.dcs`: Document (stored fields, columns, vectors)
- `.trm`: Term dictionary (plain text)

### `PlainPerDocWriter`

Plain text writer for per-document values.

### `PlainPerDocReader`

Plain text reader for per-document values.

### `PlainFieldWriter`

Plain text writer for the inverted index.

### `PlainTermsReader`

Plain text reader for the term dictionary.

### `PlainSegment`

```python
class whoosh.codec.plaintext.PlainSegment(indexname)
```

Segment class for the plain text codec. Does not support compound files
(`should_assume()` returns `False`).

## Memory Codec

### `MemoryCodec`

```python
class whoosh.codec.memory.MemoryCodec
```

An in-memory-only codec for testing. Stores all data in Python objects
rather than on disk.

**Class Attributes:**
- `storage`: A `RamStorage` instance.
- `segment`: A `MemSegment` instance.

**Methods:**
- `writer(schema)`: Returns a `MemWriter`.
- `reader(schema)`: Returns a `SegmentReader`.

### `MemWriter`

```python
class whoosh.codec.memory.MemWriter
```

A `SegmentWriter` subclass that commits immediately without merging.

### `MemPerDocWriter`

In-memory writer for per-document values.

### `MemPerDocReader`

In-memory reader for per-document values.

### `MemFieldWriter`

In-memory writer for the inverted index.

### `MemTermsReader`

In-memory reader for the term dictionary.

### `MemSegment`

```python
class whoosh.codec.memory.MemSegment(codec, indexname)
```

In-memory segment storing all data in Python dictionaries (inverted index,
stored fields, lengths, vectors, term infos). Uses a `Lock` for thread-safe
access.


## DOCUMENT (FR): Collectors

> **Note de traduction** : Cette page n'est pas encore traduite en français.
> Le contenu anglais est affiché ci-dessous en attendant la traduction.

<!-- Creez une version francaise de ce fichier et supprimez ce message. -->


# Collectors API

Classes and functions for gathering search results. Collectors are used
internally by `Searcher.search()` to collect matching documents and build
`Results` objects. The collectors module is a refactored package exposing the
same public API as the former monolithic module.

## Overview

A `Collector` iterates over matching documents in an index, collects
information about them, and produces a `Results` object. The base `Collector`
class defines the interface; specialized subclasses implement different
collection strategies (top-N, unlimited, sorting, filtering, faceting, etc.).

## Core Classes

### `Collector`

```python
class whoosh.collectors.Collector
```

Abstract base class for all collectors. Subclasses must implement `collect()`
and `results()`.

**Methods:**

#### `prepare(top_searcher, q, context)`

Called before a search begins. Sets up `self.top_searcher`, `self.q`,
`self.context`, `self.starttime`, and `self.docset`.

#### `run()`

Iterates over sub-searchers, calling `set_subsearcher()` and
`collect_matches()` for each, then calls `finish()`.

#### `set_subsearcher(subsearcher, offset)`

Called when moving to a new sub-searcher. Sets `self.subsearcher`,
`self.offset`, and `self.matcher`.

#### `collect(sub_docnum)`

Called for every matched document. Must add the document to results and
return a sort key. Subclasses must implement this.

- `sub_docnum`: Segment-relative document number. Add `self.offset` to get
  the top-level document number.

#### `sort_key(sub_docnum)`

Returns a sort key for the current match without the side effect of adding
the document to results. Subclasses must implement this.

#### `collect_matches()`

Calls `matches()` and then `collect()` for each matched document.

#### `matches()`

Yields segment-relative document numbers for matches in the current
sub-searcher.

#### `count()`

Returns the total number of matching documents.

#### `all_ids()`

Returns a sequence of docnums matched in this collector.

#### `computes_count()`

Returns `True` if the collector naturally computes the exact count of
matching documents.

#### `finish()`

Called after the search completes. Sets `self.runtime`.

#### `remove(global_docnum)`

Removes a document from the collector using its global docnum.

#### `results()`

Returns a `Results` object. Subclasses must implement this.

### `ilen`

```python
whoosh.collectors.ilen(iterator) -> int
```

Counts the number of items in an iterator without loading it all into memory.

## Scored Collectors

### `ScoredCollector`

```python
class whoosh.collectors.ScoredCollector(replace=10)
```

Base class for collectors that sort by document score.

**Constructor:**
- `replace`: Number of matches between attempts to replace the matcher with
  a more efficient version.

### `TopCollector`

```python
class whoosh.collectors.TopCollector(
    limit=10,
    usequality=True,
    **kwargs
)
```

A collector that returns only the top N scored results.

**Constructor:**
- `limit`: Maximum number of results to return.
- `usequality`: Whether to use block-quality optimizations for faster
  search. Can be set to `False` for debugging.

**Notes:**
- When `usequality=True`, `computes_count()` returns `False` and
  `all_ids()` requires re-searching.
- Uses a min-heap to efficiently track the top N documents.

### `UnlimitedCollector`

```python
class whoosh.collectors.UnlimitedCollector(reverse=False)
```

A collector that returns **all** scored results. Sorts by score (descending
by default).

**Constructor:**
- `reverse`: If `True`, sort results in ascending order (lowest scores first).

### `UnsortedCollector`

```python
class whoosh.collectors.UnsortedCollector
```

A collector that returns results in document order (no sorting). Used when
the search weighting is `None`.

## Wrapping Collectors

### `WrappingCollector`

```python
class whoosh.collectors.WrappingCollector(child)
```

Base class for collectors that wrap other collectors. Delegates most
operations to the child collector while adding additional behavior.

**Constructor:**
- `child`: The collector to wrap.

**Methods** (all delegated to child):
`top_searcher`, `context`, `prepare`, `set_subsearcher`, `all_ids`,
`count`, `collect_matches`, `sort_key`, `collect`, `remove`, `matches`,
`finish`, `results()`

### `SortingCollector`

```python
class whoosh.collectors.SortingCollector(
    sortedby,
    limit=10,
    reverse=False
)
```

A collector that returns results sorted by a `FacetType` object.

**Constructor:**
- `sortedby`: A `FacetType` or field name to sort by.
- `limit`: Maximum number of results (0 for no limit).
- `reverse`: If `True`, reverse the overall sort order.

### `FilterCollector`

```python
class whoosh.collectors.FilterCollector(
    child,
    allow=None,
    restrict=None
)
```

A collector that allows and/or restricts certain document numbers in
results.

A document is discarded if:
- `allow` is set and the docnum is not in the allowed set, or
- `restrict` is set and the docnum is in the restricted set.

**Constructor:**
- `child`: The collector to wrap.
- `allow`: A query, `Results` object, or set-like of allowed docnums.
  `None` means everything is allowed.
- `restrict`: A query, `Results` object, or set-like of disallowed docnums.
  `None` means nothing is disallowed.

**Attributes:**
- `filtered_count`: Number of documents filtered out.

### `FacetCollector`

```python
class whoosh.collectors.FacetCollector(child, groupedby, maptype=None)
```

A collector that creates groups of documents based on facet objects. Used
when `groupedby` is specified in `Searcher.search()`.

**Constructor:**
- `child`: The collector to wrap.
- `groupedby`: A field name, `FacetType`, dict, or `Facets` object.
- `maptype`: Default `FacetMap` class for facets that don't specify one.

**Attributes:**
- `facetmaps`: Dictionary of facet name to `FacetMap` objects.

### `CollapseCollector`

```python
class whoosh.collectors.CollapseCollector(
    child,
    keyfacet,
    limit=1,
    order=None
)
```

A collector that eliminates all but the top N results sharing the same facet
key. Useful for "dedup" or grouped result views.

**Constructor:**
- `child`: The collector to wrap.
- `keyfacet`: A `FacetType` to collapse on. All but the top N documents
  sharing a key are eliminated.
- `limit`: Maximum documents to keep per key (default `1`).
- `order`: Optional `FacetType` to determine which documents are "top" within
  each group. Defaults to the results order (e.g., highest score).

**Attributes:**
- `collapsed_counts`: Dictionary mapping keys to the number of documents
  eliminated.

### `TimeLimitCollector`

```python
class whoosh.collectors.TimeLimitCollector(
    child,
    timelimit,
    greedy=False,
    use_alarm=True
)
```

A collector that raises a `TimeLimit` exception if the search exceeds a
time limit. Partial results are still available via `results()`.

**Constructor:**
- `child`: The collector to wrap.
- `timelimit`: Maximum search time in seconds.
- `greedy`: If `True`, finish adding the current hit before raising.
- `use_alarm`: If `True` (default), use `signal.SIGALRM` on Unix for
  immediate interruption. On Windows, time is only checked between
  documents.

```python
from whoosh.searching import TimeLimit

uc = collectors.UnlimitedCollector()
tlc = TimeLimitCollector(uc, timelimit=5.8)
try:
    searcher.search_with_collector(myquery, tlc)
except TimeLimit:
    print("Search timed out!")
# Still get partial results:
print(tlc.results())
```

### `TermsCollector`

```python
class whoosh.collectors.TermsCollector(child, settype=set)
```

A collector that records which terms appeared in which matched documents.
Used when `terms=True` in `Searcher.search()`.

**Constructor:**
- `child`: The collector to wrap.
- `settype`: Set type to use for docnum collections (default `set`).

**Attributes:**
- `termdocs`: Dict mapping `(fieldname, text)` tuples to arrays of docnums.
- `docterms`: Dict mapping docnums to lists of `(fieldname, text)` tuples.

## Exceptions

### `TimeLimit`

```python
from whoosh.searching import TimeLimit
```

Raised by `TimeLimitCollector` when the search exceeds the time limit.
Partial results are still available from the collector.


## DOCUMENT (FR): Columns

> **Note de traduction** : Cette page n'est pas encore traduite en français.
> Le contenu anglais est affiché ci-dessous en attendant la traduction.

<!-- Creez une version francaise de ce fichier et supprimez ce message. -->


# Columns API

Classes for storing per-document values (column-oriented storage) used for
fast sorting, faceting, and filtering. Columns are the mechanism by which
Whoosh stores field values alongside the inverted index, in a column-oriented
layout for efficient range access.

The default column type for most fields is `VarBytesColumn`, although numeric
and date fields use `NumericColumn`. Expert users may use other column types
that may be faster or more storage-efficient based on the field contents.

A `Column` object stores configuration information and provides two important
methods: `writer()` to return a `ColumnWriter` and `reader()` to return a
`ColumnReader`.

## Module Functions

### `bytes_column`

```python
whoosh.columns.bytes_column
```

A default `VarBytesColumn` instance used as the column type for string fields.

### `numeric_column`

```python
whoosh.columns.numeric_column
```

A default `NumericColumn` instance used as the column type for numeric fields.

## Base Classes

### `Column`

```python
class whoosh.columns.Column
```

Base class for all column types.

**Class Attributes:**
- `reversible (bool)`: Whether values can be reversed for descending sort.
  Default `False`.

**Methods:**
- `writer(dbfile)`: Returns a `ColumnWriter` for this column type.
- `reader(dbfile, basepos, length, doccount)`: Returns a `ColumnReader` for
  this column type.
- `default_value(reverse=False)`: Returns the default value for documents
  without a column value at index time.
- `stores_lists()`: Returns `True` if the column stores a list of values per
  document instead of a single value.

### `ColumnWriter`

```python
class whoosh.columns.ColumnWriter(dbfile)
```

Base class for writing column values to disk.

**Constructor:**
- `dbfile`: The `StructFile` to write to.

**Methods:**
- `fill(docnum)`: Fills any gap in docnums up to `docnum` with default values.
- `add(docnum, value)`: Adds a value for the given docnum.
- `finish(docnum)`: Called when done writing. Default does nothing.

### `ColumnReader`

```python
class whoosh.columns.ColumnReader(dbfile, basepos, length, doccount)
```

Base class for reading column values from disk.

**Constructor:**
- `dbfile`: The `StructFile` to read from.
- `basepos`: The offset within the file at which the column starts.
- `length`: The length in bytes the column occupies in the file.
- `doccount`: The number of rows (documents) in the column.

**Methods:**
- `__getitem__(docnum)`: Returns the value for the given docnum.
- `sort_key(docnum)`: Returns the value for sorting (defaults to
  `__getitem__`).
- `__iter__()`: Yields values for all documents.
- `load()`: Returns a list of all values.
- `set_reverse()`: Prepares the reader for reverse iteration.

## Concrete Column Types

### `VarBytesColumn`

```python
class whoosh.columns.VarBytesColumn(
    allow_offsets=True,
    write_offsets_cutoff=2**15
)
```

Stores variable-length byte strings. The default value for documents without
a value is `b''` (empty bytes).

**Constructor:**
- `allow_offsets`: Whether to write offsets for faster lookup when there are
  many rows. Default `True`.
- `write_offsets_cutoff`: Write offsets when there are more than this many
  rows (default `2**15`).

### `FixedBytesColumn`

```python
class whoosh.columns.FixedBytesColumn(blocksize, default=emptybytes)
```

Stores fixed-length byte strings, saving space by not storing the length of
each value.

**Constructor:**
- `blocksize`: Fixed size of each value in bytes.
- `default`: Default value for documents without a value.

### `RefBytesColumn`

```python
class whoosh.columns.RefBytesColumn(
    cachesize=1000,
    stable=True,
    default=emptybytes
)
```

Stores references to unique values rather than the values themselves, saving
space when the field has few unique values. Uses a `DocIdSet` to track which
documents contain each value.

**Constructor:**
- `cachesize`: Size of the LRU cache for value lookups (default `1000`).
- `stable`: Whether to use a stable sort of references (default `True`).
- `default`: Default value for missing documents.

### `NumericColumn`

```python
class whoosh.columns.NumericColumn(
    typecode,
    default=None,
    nullable=False
)
```

Stores numbers (int, float, datetime) encoded as binary values. Extends
`FixedBytesColumn`.

**Constructor:**
- `typecode`: A `struct` typecode string (e.g., `"I"` for unsigned int,
  `"q"` for long, `"d"` for float).
- `default`: Default numeric value (None for the type's zero value).
- `nullable`: Whether `None` values are allowed.

### `BitColumn`

```python
class whoosh.columns.BitColumn
```

Stores boolean values as a bitmap. Each value is either `True` (1) or
`False` (0). Uses a `BitSet` internally.

### `CompressedBytesColumn`

```python
class whoosh.columns.CompressedBytesColumn(default=emptybytes)
```

Wraps a `VarBytesColumn` with zlib compression for the value bytes.

### `CompressedBlockColumn`

```python
class whoosh.columns.CompressedBlockColumn
```

Stores values with block-level zlib compression. More efficient for large
columns.

### `StructColumn`

```python
class whoosh.columns.StructColumn(struct, name)
```

Wraps a `FixedBytesColumn` to store structured binary data (e.g., tuples
encoded with `struct`).

**Constructor:**
- `struct`: A `struct.Struct` object defining the format.
- `name`: Field name for error messages.

### `EmptyColumnReader`

```python
class whoosh.columns.EmptyColumnReader(default, doccount)
```

A `ColumnReader` that returns a constant default value for every document.
Used when a field has no column.

### `MultiColumnReader`

```python
class whoosh.columns.MultiColumnReader(readers)
```

Combines multiple `ColumnReader` instances into one for multi-segment indices.

**Constructor:**
- `readers`: List of `ColumnReader` instances (one per segment).

### `TranslatingColumnReader`

```python
class whoosh.columns.TranslatingColumnReader(child, translator)
```

Wraps a `ColumnReader` to apply a translation function to the values.

**Constructor:**
- `child`: The underlying `ColumnReader`.
- `translator`: Function that maps sort keys to human-readable values.

### `WrappedColumn`

```python
class whoosh.columns.WrappedColumn(child)
```

Base class for column wrappers that adapt another column type.

### `WrappedColumnWriter`

```python
class whoosh.columns.WrappedColumnWriter(child)
```

Base class for column writer wrappers.

### `WrappedColumnReader`

```python
class whoosh.columns.WrappedColumnReader(child)
```

Base class for column reader wrappers.

### `ClampedNumericColumn`

```python
class whoosh.columns.ClampedNumericColumn(child, clampfn)
```

Wraps a `NumericColumn` to clamp values to a valid range before sorting.

**Constructor:**
- `child`: The wrapped `NumericColumn`.
- `clampfn`: Function that clamps a value to the valid range.

### `PickleColumn`

```python
class whoosh.columns.PickleColumn(child, ...)
```

Wraps another column to store pickled Python objects.

### `ListColumn`

```python
class whoosh.columns.ListColumn(child)
```

Base class for columns that store multiple values per document.

### `ListColumnReader`

```python
class whoosh.columns.ListColumnReader(child)
```

Reader for list-valued columns.

### `VarBytesListColumn`

```python
class whoosh.columns.VarBytesListColumn
```

A `ListColumn` variant of `VarBytesColumn` that stores lists of byte strings.

### `FixedBytesListColumn`

```python
class whoosh.columns.FixedBytesListColumn(blocksize)
```

A `ListColumn` variant of `FixedBytesColumn` that stores lists of fixed-size
byte strings.


## DOCUMENT (FR): Core

# API Core

Gestion des indexes via les fonctions et classes du module `whoosh.index`.

## Fonctions

### create_in

```python
def create_in(dirname, schema, indexname="MAIN", create=True, **kwargs) -> FileIndex
```

Crée un nouvel index dans le répertoire donné.

**Args:**
- `dirname (str)`: Chemin du répertoire.
- `schema (Schema)`: Objet Schema définissant les champs.
- `indexname (str)`: Nom de l'index.
- `create (bool)`: Si True, crée même si existe (efface l'existant).

**Retourne:**
- `FileIndex`: Objet index.

**Exemple:**
```python
from whoosh.index import create_in
index = create_in("indexdir", schema)
```

### open_dir

```python
def open_dir(dirname, indexname="MAIN", readonly=False, **kwargs) -> FileIndex
```

Ouvre un index existant.

**Exemple:**
```python
index = open_dir("indexdir")
```

### exists_in

```python
def exists_in(dirname, indexname="MAIN", **kwargs) -> bool
```

Vérifie si un index valide existe dans le répertoire.

## Classes

### Index

Classe de base pour les objets index.

#### Méthodes principales

| Méthode | Description |
|---------|-------------|
| `writer(**kwargs)` | Retourne un IndexWriter |
| `searcher(**kwargs)` | Retourne un Searcher |
| `reader()` | Retourne un IndexReader |
| `commit()` | Commit via un writer temporaire |
| `optimize()` | Fusionne tous les segments |
| `add_field()` | Ajoute un champ au schéma |
| `remove_field()` | Supprime un champ du schéma |
| `doc_count()` | Nombre de documents |
| `doc_count_all()` | Nombre total (y compris supprimés) |

## Exceptions

### LockError

Levée quand l'index est verrouillé par un autre writer.

```python
from whoosh.index import LockError

try:
    writer = ix.writer(timeout=5.0)
except LockError:
    print("Index verrouillé, réessayez plus tard")
```

### IndexMissingError

Levée quand l'index n'existe pas.


## DOCUMENT (FR): Events

# API Events

Système d'événements pour un couplage lâche entre les composants.

## EventBus

```python
class whoosh.event_bus.EventBus
```

Registre central des événements et subscribers.

### Méthodes

| Méthode | Description |
|---------|-------------|
| `bus.publish(event)` | Publie un événement |
| `bus.subscribe(fn)` | Abonne un handler |
| `bus.unsubscribe(fn)` | Désabonne un handler |
| `bus.clear()` | Supprime tous les abonnés |

**Exemple:**
```python
from whoosh.event_bus import EventBus

bus = EventBus()

@bus.subscribe
def on_index(event: DocumentIndexed):
    print(f"Indexé: {event.docnum}")

# Publier
bus.publish(DocumentIndexed(docnum=42))
```

## Événements intégrés

### DocumentIndexed

```python
class DocumentIndexed
    docnum: int           # Numéro de document
    schema: Schema        # Schéma utilisé
    timestamp: datetime   # Horodatage
    metadata: dict        # Métadonnées
```

### SearchExecuted

```python
class SearchExecuted
    query: str            # Requête originale
    result_count: int     # Nombre de résultats
    duration_ms: float    # Durée en ms
    user: str | None      # Utilisateur (si auth)
```

### IndexOptimized

```python
class IndexOptimized
    segments_before: int
    segments_after: int
    size_bytes: int
```

## Utilisation avec FastAPI

```python
from fastapi import FastAPI
from whoosh.event_bus import EventBus, DocumentIndexed

app = FastAPI()
bus = EventBus()

@app.on_event("startup")
def startup():
    bus.subscribe(on_index)

@app.post("/documents")
def add_document(doc: dict):
    # Indexation...
    bus.publish(DocumentIndexed(docnum=doc["id"]))
```

## Gestion d'erreurs

```python
@bus.subscribe
def on_error(event: SearchExecuted):
    if event.result_count == 0:
        logger.warning(f"Recherche vide: {event.query}")
```


## DOCUMENT (FR): Fields

# API Champs

Définissez la structure de votre index avec les types de champs.

## Schema

```python
class whoosh.fields.Schema
```

Définit les champs disponibles dans l'index.

### Méthodes

#### `add()`

```python
schema.add(fieldname, fieldtype, glob=False, **kwargs)
```

Ajoute un champ. Si `glob=True`, le nom est traité comme un pattern glob.

#### `remove()`

```python
schema.remove(fieldname, **kwargs)
```

Supprime un champ.

#### `items()`

```python
for name, field in schema.items():
    print(name, field)
```

Retourne les paires (nom, objet champ).

## Types de champs

### TEXT

```python
TEXT(
    stored=False,
    unique=False,
    phrase=True,
    analyzer=None,
    field_boost=1.0
)
```

Texte libre avec tokenisation et recherche de phrase optionnelle.

**Exemple:**
```python
titre = TEXT(stored=True)
corps = TEXT(analyzer=StemmingAnalyzer(), phrase=False)
```

### ID

```python
ID(stored=False, unique=False, field_boost=1.0)
```

Identifiant non tokenisé. Stocke la valeur entière comme terme unique.

**Exemple:**
```python
chemin = ID(stored=True, unique=True)
slug = ID(stored=True)
```

### KEYWORD

```python
KEYWORD(
    stored=False,
    lowercase=False,
    commas=False,
    scorable=False,
    field_boost=1.0
)
```

Mots-clés séparés par espace ou virgule.

**Exemple:**
```python
tags = KEYWORD(lowercase=True, commas=True, stored=True)
```

### STORED

```python
STORED(stored=True)
```

Champ stocké uniquement, non indexé ni searchable.

### NUMERIC

```python
NUMERIC(numtype=int, stored=False, unique=False, field_boost=1.0)
```

Champ numérique (entier ou flottant).

### DATETIME

```python
DATETIME(stored=False, unique=False, field_boost=1.0)
```

Champ date/heure.

### BOOLEAN

```python
BOOLEAN(stored=False, unique=False, field_boost=1.0)
```

Champ booléen. Searchable avec `oui`, `non`, `vrai`, `faux`, `1`, `0`, `t`, `f`.

### VectorField

```python
VectorField(
    dimensions: int,
    metric: str = "cosine",
    provider: str = "numpy",
    stored: bool = False
)
```

Champ pour embeddings vectoriels.

**Exemple:**
```python
embedding = VectorField(dimensions=384, metric="cosine", stored=True)
```

## SchemaBuilder

API fluent pour construire des schémas :

```python
from whoosh.fields import SchemaBuilder, TEXT, ID, NUMERIC

schema = (
    SchemaBuilder()
    .field("titre", TEXT(stored=True))
    .field("chemin", ID(stored=True, unique=True))
    .field("contenu", TEXT)
    .field("note", NUMERIC(float, stored=True))
    .build()
)
```

## Attributs de FieldType

| Attribut | Type | Description |
|----------|------|-------------|
| `format` | `Format` | Définit l'indexation |
| `vector` | `Format` | Format vectoriel optionnel |
| `scorable` | `bool` | Stocke la longueur pour BM25F |
| `stored` | `bool` | Stocke la valeur |
| `unique` | `bool` | Identifie les documents de façon unique |


## DOCUMENT (FR): Filedb Storage

> **Note de traduction** : Cette page n'est pas encore traduite en français.
> Le contenu anglais est affiché ci-dessous en attendant la traduction.

<!-- Creez une version francaise de ce fichier et supprimez ce message. -->


# File DB / Storage API

Classes for storing and retrieving index data on disk or in memory. The
`Storage` class is the main entry point for persisting an index.

## Storage Classes

### `Storage`

```python
class whoosh.filedb.filestore.Storage(path=None)
```

Abstract base class for storage backends. A `Storage` manages a filesystem-
or memory-based location where index files can be created, read, and
manipulated.

**Constructor:**
- `path`: Optional path string. Subclasses may use this to set the storage
  location.

**Methods:**

#### `create_file(name, **kwargs)`

Creates and returns a file object for writing.

#### `open_file(name, **kwargs)`

Opens and returns a file object for reading.

#### `list()`

Returns a list of all filenames in this storage.

#### `exists(name)`

Returns `True` if a file/named item exists in the storage.

#### `file_exists(name)`

Alias for `exists()`.

#### `file_length(name)`

Returns the length of file `name` in bytes.

#### `rename(src, dst)`

Renames a file from `src` to `dst`.

#### `delete_file(name)`

Deletes file `name` from storage.

#### `destroy()`

Deletes all files and the storage itself.

#### `temp_storage()`

Creates and returns a temporary isolated `Storage` for scratch space.

#### `supports_mmap`

Returns `True` if this storage supports memory-mapped file access.

**Properties:**
- `schema`: The `Schema` for this storage (if it holds an index).
- `lock`: The lock object used for this storage.

### `FileStorage`

```python
class whoosh.filedb.filestore.FileStorage(
    path,
    cachesize_limit=40,
    supports_mmap=None,
    **kwargs
)
```

A `Storage` subclass that uses the operating system's filesystem.

**Constructor:**
- `path`: A `Path` (or string path) to the directory where files are stored.
- `cachesize_limit`: Maximum number of open file handles to cache.
- `supports_mmap`: If `None`, auto-detected; otherwise force enable/disable.

**Methods:** All `Storage` methods plus:
- `create_index(schema, indexname="index", ...)`: Creates and returns a new
  `Index` object.
- `open_index(indexname="index", ...)`: Opens an existing `Index`.
- `lock(name)`: Returns a lock object for the given lock name.

### `RamStorage`

```python
class whoosh.filedb.filestore.RamStorage(cachesize_limit=10)
```

A `Storage` subclass that keeps all files in memory as bytes. Useful for
testing and small indexes.

**Constructor:**
- `cachesize_limit`: Maximum number of files to cache as decoded objects.

**Methods:** All `Storage` methods plus:
- `create_index(schema, ...)`: Creates an in-memory `Index`.
- `save_to_file(filename, ...)`: Saves the entire storage to a file.
- `load_from_file(filename, ...)`: Loads storage contents from a file.

### `OverlayStorage`

```python
class whoosh.filedb.filestore.OverlayStorage(base, overlay)
```

A `Storage` wrapper that presents two storage layers: a base and an overlay.
Files in the overlay take precedence over the base.

**Constructor:**
- `base`: The base `Storage` (e.g., read-only original).
- `overlay`: The overlay `Storage` (e.g., writable copy).

## Storage Exceptions

### `StorageError`

```python
class whoosh.filedb.filestore.StorageError
```

Base exception for storage-related errors.

### `ReadOnlyError`

```python
class whoosh.filedb.filestore.ReadOnlyError(StorageError)
```

Raised when attempting to write to a read-only storage.

## File Tables

### `HashWriter`

```python
class whoosh.filedb.filetables.HashWriter(dbfile, keycoder=None, keydecoder=None, data_encoder=None, data_decoder=None, **kwargs)
```

Writes key-value pairs to a file, with optional indexing by key.

**Constructor:**
- `dbfile`: The `StructFile` to write to.
- `keycoder`: Function to encode keys for storage.
- `keydecoder`: Function to decode keys from storage.
- `data_encoder`: Function to encode values.
- `data_decoder`: Function to decode values.

### `HashReader`

```python
class whoosh.filedb.filetables.HashReader(dbfile, length, keycoder=None, keydecoder=None, data_decoder=None, **kwargs)
```

Reads key-value pairs from a file written by `HashWriter`.

**Constructor:**
- `dbfile`: The `StructFile` to read from.
- `length`: Length of the data section.
- `keycoder`/`keydecoder`/`data_decoder`: Same as `HashWriter`.

**Methods:**
- `__getitem__(key)`: Returns the value for `key`.
- `keys()`: Yields all keys.
- `values()`: Yields all values.
- `items()`: Yields `(key, value)` pairs.
- `keys_from(prefixbytes)`: Yields keys starting at `prefixbytes`.
- `items_from(prefixbytes)`: Yields `(key, value)` pairs starting at prefix.
- `closest_key_pos(key)`: Returns the position of the closest matching key.
- `range_for_key(key)`: Returns `(startpos, endpos)` for a key range.

### `OrderedHashWriter`

```python
class whoosh.filedb.filetables.OrderedHashWriter(HashWriter)
```

A `HashWriter` that maintains keys in sorted order.

### `OrderedHashReader`

```python
class whoosh.filedb.filetables.OrderedHashReader(HashReader)`

A `HashReader` for reading data written by `OrderedHashWriter`. Preserves
key ordering for efficient prefix iteration.

### `FieldedOrderedHashWriter`

```python
class whoosh.filedb.filetables.FieldedOrderedHashWriter(HashWriter)
```

An `OrderedHashWriter` that stores an extra "fieldmap" in the extras dict,
mapping field names to numeric IDs.

### `FieldedOrderedHashReader`

```python
class whoosh.filedb.filetables.FieldedOrderedHashReader(HashReader)
```

Reader for data written by `FieldedOrderedHashWriter`.

## Struct File

### `StructFile`

```python
class whoosh.filedb.structfile.StructFile(name, source, cachesize_limit=40)
```

Wraps a file object and adds methods for reading/writing packed binary
values, arrays, varints, and pickle objects.

**Methods include:**
- `read_int()`, `write_int(n)`: Read/write a 4-byte signed integer.
- `read_long()`, `write_long(n)`: Read/write a 8-byte signed integer.
- `read_uint()`, `write_uint(n)`: Read/write unsigned int.
- `read_ulong()`, `write_ulong(n)`: Read/write unsigned long.
- `read_float()`, `write_float(n)`: Read/write a float.
- `read_ushort()`, `write_ushort(n)`: Read/write unsigned short.
- `read_byte()`, `write_byte(b)`: Read/write a single byte.
- `write_array(arr)`: Write an array of values.
- `get_array(offset, typecode, length)`: Read an array from offset.
- `write_pickle(obj)`: Pickle and write an object.
- `read_pickle()`: Read and unpickle an object.
- `get(offset, length)`: Read `length` bytes from `offset`.
- `get_int()`, `get_uint()`, `get_long()`, `get_float()`, `get_byte()`:
  Read a single value from the given offset.

### `BufferFile`

```python
class whoosh.filedb.structfile.BufferFile
```

A `StructFile` that wraps an in-memory byte buffer.

### `ChecksumFile`

```python
class whoosh.filedb.structfile.ChecksumFile(dbfile)
```

A `StructFile` wrapper that computes a checksum as data is written, for
integrity verification.

## Compound Storage

### `CompoundStorage`

```python
class whoosh.filedb.compound.CompoundStorage(dbfile, use_mmap=True)
```

Treats a single file as a container for multiple sub-files. Used for compound
segment files.

**Methods:**
- `create_file(name)`: Create a sub-file within the compound file.
- `open_file(name)`: Open a sub-file for reading.
- `list()`: List all sub-file names.
- `close()`: Close the compound storage.

### `SubFile`

```python
class whoosh.filedb.compound.SubFile
```

A file-like object representing a sub-file within a `CompoundStorage`.

### `CompoundWriter`

```python
class whoosh.filedb.compound.CompoundWriter(storage)
```

Writes a compound file by assembling multiple files from a storage.

**Methods:**
- `create_file(name)`: Reserve a filename in the compound file.
- `save_as_files(dest_storage, fn_generator)`: Assemble the compound file
  from source files into the destination storage.

## Storage Utility Functions

### `copy_storage`

```python
whoosh.filedb.filestore.copy_storage(sourcestore, deststore)
```

Copies all files from one storage to another.

### `copy_to_ram`

```python
whoosh.filedb.filestore.copy_to_ram(storage)
```

Reads all files from a storage into a `RamStorage` and returns it.


## DOCUMENT (FR): Formats

> **Note de traduction** : Cette page n'est pas encore traduite en français.
> Le contenu anglais est affiché ci-dessous en attendant la traduction.

<!-- Creez une version francaise de ce fichier et supprimez ce message. -->


# Formats API

Classes that control how posting information (frequencies, positions,
character offsets, and weights) is encoded and stored for each field in the
index. The `Format` object is a factory and encoder/decoder for the
value strings stored alongside each posting.

## Module Functions

### `tokens`

```python
whoosh.formats.tokens(value, analyzer, kwargs)
```

Takes a text `value` and an `analyzer`, runs the analyzer on the value, and
returns the resulting token generator (wrapped with `unstopped()` to ignore
`STOP` tokens). Used internally by `Format.word_values()`.

## Format Classes

All format classes accept a `field_boost` parameter (default `1.0`) that
scales the score of all queries matching terms in that field.

### `Format`

```python
class whoosh.formats.Format(field_boost=1.0, **options)
```

Abstract base class for all posting formats. Format objects are
field-level objects: one is created per `Field` and shared across all
postings for that field.

**Attributes:**
- `posting_size (int)`: Fixed byte size of encoded postings, or `None`/`-1`
  if variable-size.
- `textual (bool)`: Whether this format expects string tokens (vs. bytes).
  Default `True`.

**Methods:**

#### `word_values(value, analyzer, **kwargs)`

Abstract. Takes a text value, runs it through the analyzer, and yields
`(tokentext, frequency, weight, valuestring)` tuples.

#### `encode(value)`

Abstract. Encodes raw posting data into the value string bytes.

#### `decode_frequency(valuestring)`

Abstract. Decodes the frequency (term count in document) from the value
string.

#### `decode_weight(valuestring)`

Abstract. Decodes the weight (total boost contribution) from the value string.

#### `combine(valuestrings)`

Abstract. Combines multiple value strings (from overlapping segments) into
a single value string.

#### `supports(name)`

Returns `True` if this format supports interpreting its postings as `name`
(e.g., `"frequency"`, `"positions"`, `"characters"`, `"position_boosts"`,
`"character_boosts"`). Equivalent to `hasattr(self, "decode_" + name)`.

#### `decoder(name)`

Returns the `decode_<name>` method for the given attribute name.

#### `decode_as(astype, valuestring)`

Calls the appropriate `decode_<astype>` method on `valuestring` and returns
the result.

#### `fixed_value_size()`

Returns `self.posting_size` if positive, otherwise `None`.

#### `__eq__(other)`

Returns `True` if `other` is the same class with equal `__dict__`.

### `Existence`

```python
class whoosh.formats.Existence(field_boost=1.0, **options)
```

Indexes only whether a term occurred in a document—not its frequency or
positions. Useful for non-scorable fields like paths.

- `posting_size = 0`
- Supports: `frequency` (always 1), `weight` (always `field_boost`)
- `encode()` returns empty bytes

### `Frequency`

```python
class whoosh.formats.Frequency(field_boost=1.0, boost_as_freq=False, **options)
```

Stores term frequency information (term count per document) for each posting.

- `posting_size = _INT_SIZE` (4 bytes)
- Supports: `frequency`, `weight`
- `encode()` encodes the count as a packed unsigned int
- `boost_as_freq`: If `True`, boosts are interpreted as frequency boosts

```python
from whoosh.formats import Frequency
fmt = Frequency(field_boost=1.0)
```

### `Positions`

```python
class whoosh.formats.Positions(field_boost=1.0, **options)
```

Stores position information (term offsets within the document) in each
posting, enabling phrase queries and "near" queries.

- Supports: `frequency`, `weight`, `positions`, `position_boosts`
- `encode(poslist)` encodes positions using variable-length delta encoding
- Positions are stored as delta-encoded variable-length integers

```python
from whoosh.formats import Positions
fmt = Positions()
```

### `Characters`

```python
class whoosh.formats.Characters(field_boost=1.0, **options)
```

Extends `Positions` to also store character start and end offsets for each
term occurrence, enabling character-precise highlighting.

- Supports: `frequency`, `weight`, `positions`, `position_boosts`,
  `characters`
- `encode()` encodes (position, startchar, endchar) triples with delta
  encoding

### `PositionBoosts`

```python
class whoosh.formats.PositionBoosts(field_boost=1.0, **options)
```

Extends `Positions` to store per-position boost values in addition to
positions.

- Supports: `frequency`, `weight`, `positions`, `position_boosts`
- `encode()` encodes `(position, boost)` pairs

### `CharacterBoosts`

```python
class whoosh.formats.CharacterBoosts(field_boost=1.0, **options)
```

Extends `Characters` to store per-position boost values along with
character offsets.

- Supports: `frequency`, `weight`, `positions`, `position_boosts`,
  `characters`, `character_boosts`
- `encode()` encodes `(position, startchar, endchar, boost)` tuples


## DOCUMENT (FR): Highlight

> **Note de traduction** : Cette page n'est pas encore traduite en français.
> Le contenu anglais est affiché ci-dessous en attendant la traduction.

<!-- Creez une version francaise de ce fichier et supprimez ce message. -->


# Highlight API

Classes and functions for highlighting matches in search result fragments.
The highlight module is a refactored package exposing the same public API as
the former monolithic module.

## Overview

The highlighting system has four components:

- **Fragmenters** split text into fragments.
- **Fragment Scorers** score fragments to determine which to display.
- **Formatters** render fragments as output (HTML, plain text, etc.).
- **Highlighter** ties these together and is used by `Searcher.highlights()`.

## Module-level Functions

### `highlight`

```python
whoosh.highlight.highlight(
    text: str,
    terms: list[str],
    analyzer,
    fragmenter,
    formatter,
    top: int = 3,
    scorer=None,
    minscore: int = 1,
    order=SCORE,
    mode: str = "query"
) -> str
```

Highlights the matched terms in `text` and returns a formatted string.

- `text`: The text to highlight.
- `terms`: A list of matched terms (strings).
- `analyzer`: The analyzer for the field.
- `fragmenter`: A `Fragmenter` instance or class.
- `formatter`: A `Formatter` instance or class.
- `top`: Maximum number of fragments to return.
- `scorer`: Optional fragment scorer (defaults to `BasicFragmentScorer`).
- `minscore`: Minimum score for a fragment to be included.
- `order`: Sort order for fragments (`FIRST`, `SCORE`, `LONGER`, `SHORTER`).
- `mode`: Analysis mode, typically `"query"` or `"index"`.

### `mkfrag`

```python
whoosh.highlight.mkfrag(
    text: str,
    tokens,
    startchar=None,
    endchar=None,
    charsbefore: int = 0,
    charsafter: int = 0
) -> Fragment
```

Returns a `Fragment` object based on `Token` objects in `tokens`.

### `get_text`

```python
whoosh.highlight.get_text(
    original: str,
    token,
    replace: bool
) -> str
```

Returns the text to use for a match when formatting. If `replace` is `False`,
returns the original text between `token.startchar` and `token.endchar`. If
`True`, returns `token.text`.

### `set_matched_filter`

```python
whoosh.highlight.set_matched_filter(
    tokens,
    termset: frozenset
) -> Iterator[Token]
```

Marks tokens as matched if their `text` attribute is in `termset`. Used for
phrase-agnostic highlighting.

### `set_matched_filter_phrases`

```python
whoosh.highlight.set_matched_filter_phrases(
    tokens,
    text: str,
    terms,
    phrases
) -> Iterator[Token]
```

Marks tokens as matched using phrase-aware logic. Highlights only tokens that
are part of matched phrases.

### `top_fragments`

```python
whoosh.highlight.top_fragments(
    fragments,
    count: int,
    scorer,
    order,
    minscore: int = 1
) -> list[Fragment]
```

Returns the best `count` fragments sorted by `order`, filtered by `minscore`.

## Constants

### `DEFAULT_CHARLIMIT`

```python
whoosh.highlight.DEFAULT_CHARLIMIT = 2**15
```

Default character limit for fragments.

### Sort Order Constants

```python
whoosh.highlight.FIRST   # Sort passages from earlier in the document first
whoosh.highlight.SCORE   # Sort higher scored passages first
whoosh.highlight.LONGER  # Sort longer passages first
whoosh.highlight.SHORTER # Sort shorter passages first
```

## Formatters

### `Formatter`

```python
class whoosh.highlight.Formatter
```

Base class for formatters. Subclasses implement `format_token()` to define
how matched tokens are rendered.

**Methods:**

- `format_token(text, token, replace=False)`: Returns formatted text for a
  matched token.
- `format_fragment(fragment, replace=False)`: Returns formatted text for a
  `Fragment`.
- `format(fragments, replace=False)`: Returns formatted text for a list of
  fragments, joined by `between`.

**Attributes:**
- `between`: String inserted between formatted fragments (default `"..."`).

### `NullFormatter`

```python
class whoosh.highlight.NullFormatter(Formatter)
```

A formatter that does not modify the string. Returns fragments unformatted.

### `UppercaseFormatter`

```python
class whoosh.highlight.UppercaseFormatter(between="...")
```

Formats matched terms in uppercase.

### `HtmlFormatter`

```python
class whoosh.highlight.HtmlFormatter(
    tagname="strong",
    between="...",
    classname="match",
    termclass="term",
    maxclasses=5,
    attrquote='"'
)
```

Wraps matched terms in HTML tags with CSS class names. Two classes are
applied to each match: `classname` (same for all matches) and `termclass`
(different for each term, e.g. `term0`, `term1`).

- `tagname`: The HTML tag to wrap matches (default `"strong"`).
- `between`: Text inserted between fragments.
- `classname`: CSS class applied to all matched term tags.
- `termclass`: CSS class prefix for per-term classes.
- `maxclasses`: Maximum number of distinct per-term class numbers.
- `attrquote`: Quote character for attribute values.

**Methods:**
- `clean()`: Clears the internal term-to-classname mapping dictionary.

### `GenshiFormatter`

```python
class whoosh.highlight.GenshiFormatter(qname="strong", between="...")
```

Formats matched terms as Genshi event streams (requires the Genshi library).

## Fragmenters

### `Fragmenter`

```python
class whoosh.highlight.Fragmenter
```

Base class for fragmenters. Subclasses implement `fragment_tokens()` and/or
`fragment_matches()`.

**Methods:**
- `must_retokenize()`: Returns `True` if this fragmenter needs to re-tokenize
  the text (calls `fragment_tokens` with all tokens). Returns `False` if it can
  work from matched token positions alone (calls `fragment_matches`).

### `WholeFragmenter`

```python
class whoosh.highlight.WholeFragmenter(charlimit=DEFAULT_CHARLIMIT)
```

Does not fragment text. Returns the entire text as one fragment. Useful for
highlighting short fields.

```python
results.fragmenter = WholeFragmenter()
```

### `SentenceFragmenter`

```python
class whoosh.highlight.SentenceFragmenter(
    maxchars: int = 200,
    sentencechars=".!?",
    charlimit=DEFAULT_CHARLIMIT
)
```

Breaks text at sentence-ending punctuation (`.`, `!`, `?`).

- `maxchars`: Maximum characters per fragment.
- `sentencechars`: Characters that indicate sentence boundaries.
- `charlimit`: Maximum character position to process.

**Note:** Should be used with an analyzer that does not remove stop words.

### `ContextFragmenter`

```python
class whoosh.highlight.ContextFragmenter(
    maxchars: int = 200,
    surround: int = 20,
    charlimit=DEFAULT_CHARLIMIT
)
```

The default fragmenter. Finds matched terms and includes `surround` characters
of context before and after each match.

- `maxchars`: Maximum characters per fragment.
- `surround`: Number of context characters to include around matches.
- `charlimit`: Maximum character position to process.

### `PinpointFragmenter`

```python
class whoosh.highlight.PinpointFragmenter(
    maxchars: int = 200,
    surround: int = 20,
    autotrim: bool = False,
    charlimit=DEFAULT_CHARLIMIT
)
```

A non-retokenizing fragmenter that builds fragments from character positions of
matched terms. Faster than `ContextFragmenter` because it doesn't need to
re-tokenize text.

- `maxchars`: Maximum characters per fragment.
- `surround`: Number of context characters around matches.
- `autotrim`: If `True`, trims fragments to the nearest spaces.
- `charlimit`: Maximum character position to process.

### `NullFragmeter`

Alias for `WholeFragmenter`.

### `Fragment`

```python
class whoosh.highlight.Fragment(
    text: str,
    matches,
    startchar: int = 0,
    endchar: int = -1
)
```

Represents a fragment (excerpt) from a hit document. Stores the start and end
character offsets and the list of matched term objects.

**Attributes:**
- `text`: The original source text.
- `matches`: List of objects with `startchar` and `endchar` attributes.
- `startchar`: Start index of the fragment.
- `endchar`: End index of the fragment.
- `matched_terms`: Set of text values of matched terms.

**Methods:**
- `overlaps(fragment)`: Returns `True` if this fragment overlaps the given one.
- `overlapped_length(fragment)`: Returns the combined length of overlapping
  fragments.

### `FragmentScorer`

```python
class whoosh.highlight.FragmentScorer
```

Base class for fragment scoring objects. Subclasses implement `__call__()`
to score a `Fragment`.

### `BasicFragmentScorer`

```python
class whoosh.highlight.BasicFragmentScorer
```

Scores fragments by summing the boosts of matched terms, then multiplying by
the number of distinct matched terms (favors diversity).

## Highlighter

### `Highlighter`

```python
class whoosh.highlight.Highlighter(
    fragmenter=None,
    scorer=None,
    formatter=None,
    always_retokenize: bool = False,
    order=SCORE
)
```

Main highlighter object used by `Searcher.highlights()`.

- `fragmenter`: Fragmenter instance (defaults to `ContextFragmenter`).
- `scorer`: Fragment scorer (defaults to `BasicFragmentScorer`).
- `formatter`: Formatter instance (defaults to `HtmlFormatter(tagname="b")`).
- `always_retokenize`: If `True`, always re-tokenize text instead of using
  character offsets from postings.
- `order`: Sort order for fragments.

**Methods:**
- `highlight_hit(hitobj, fieldname, top=3, minscore=1, strict_phrase=False)`:
  Returns the highlighted string for a single hit in a given field.
- `can_load_chars(results, fieldname)`: Returns `True` if the field supports
  "pinpoint" highlighting using stored character offsets.


## DOCUMENT (FR): Idsets

> **Note de traduction** : Cette page n'est pas encore traduite en français.
> Le contenu anglais est affiché ci-dessous en attendant la traduction.

<!-- Creez une version francaise de ce fichier et supprimez ce message. -->


# Idsets API

Specialized set implementations for storing sorted lists of positive
integers (document IDs). These are more memory-efficient than the built-in
`set` for certain use cases, though they are slower for most operations since
they are pure Python.

## Overview

The `DocIdSet` class is the abstract base class. Concrete implementations
include `BitSet`, `OnDiskBitSet`, `SortedIntSet`, `RoaringIdSet`, and
`MultiIdSet`. The `AutoIdSet` function selects the best implementation
based on the contents.

## Module Functions

### `autoset`

```python
whoosh.idsets.autoset
```

A factory that creates an appropriate `DocIdSet` subclass based on the
contents of a given iterable. If all integers in the set are below 10,000,
returns a `BitSet`; otherwise returns a `SortedIntSet`.

## `DocIdSet`

```python
class whoosh.idsets.DocIdSet
```

Abstract base class for set implementations specialized toward storing sorted
lists of positive integers.

**Inheritance:** Inherits from `set`-like interface.

**Methods:**
- `__eq__(other)`: Compares two `DocIdSet` instances by iterating.
- `__len__()`: Returns the number of elements. Override in subclasses.
- `__iter__()`: Yields elements in sorted order. Override in subclasses.
- `__contains__(i)`: Returns `True` if `i` is in the set.
- `__or__(other)`: Returns `self.union(other)`.
- `__and__(other)`: Returns `self.intersection(other)`.
- `__sub__(other)`: Returns `self.difference(other)`.
- `copy()`: Returns a copy of this set.
- `add(n)`: Adds `n` to the set.
- `discard(n)`: Removes `n` from the set (no error if absent).
- `update(other)`: Adds all elements from `other`.
- `intersection_update(other)`: Removes elements not in `other`.
- `difference_update(other)`: Removes all elements in `other`.
- `invert_update(size)`: In-place inversion over the range `[0, size)`.
- `intersection(other)`: Returns a new set with elements in both.
- `union(other)`: Returns a new set with elements from both.
- `difference(other)`: Returns a new set with elements in self but not other.
- `invert(size)`: Returns a new set that is the inversion over `[0, size)`.
- `isdisjoint(other)`: Returns `True` if no elements are shared.
- `before(i)`: Returns the previous integer in the set before `i`, or `None`.
- `after(i)`: Returns the next integer in the set after `i`, or `None`.
- `first()`: Returns the first (lowest) integer.
- `last()`: Returns the last (highest) integer.

## `BaseBitSet`

```python
class whoosh.idsets.BaseBitSet(DocIdSet)
```

Base class for bitmap-backed `DocIdSet` implementations. Uses a bytes-based
bitmap where each bit represents membership of an integer.

**Abstract Methods to Override:**
- `byte_count()`: Returns the number of bytes in the bitmap.
- `_get_byte(i)`: Returns the byte at index `i`.
- `_iter_bytes()`: Yields all bytes in the bitmap.

**Inherited Methods:** All `DocIdSet` methods with efficient bitmap
implementations of `__len__`, `__iter__`, `__contains__`, `first`, and
`last`.

## `OnDiskBitSet`

```python
class whoosh.idsets.OnDiskBitSet(file, doc_count)
```

A `BaseBitSet` that reads the bitmap from a file on disk, using `mmap` for
memory efficiency.

**Constructor:**
- `file`: A file-like object (opened in binary mode) containing the bitmap.
- `doc_count`: Total number of documents (bits) represented.

```python
from whoosh.idsets import OnDiskBitSet

with open("deletions.dat", "rb") as f:
    bs = OnDiskBitSet(f, doc_count=10000)
    if 42 in bs:
        print("Document 42 is deleted")
```

## `BitSet`

```python
class whoosh.idsets.BitSet
```

A `BaseBitSet` that stores the bitmap in memory as a `bytearray`. Fast for
membership tests and set operations on small ranges of integers.

**Constructor:**
- Optional initial iterable of integers.

```python
from whoosh.idsets import BitSet

bs = BitSet([0, 5, 10, 15])
print(5 in bs)  # True
print(bs.first())  # 0
print(len(bs))   # 4
```

**Methods:**
- `from_blob(data)`: Create a `BitSet` from raw bytes.
- `tostring()`: Returns the bitmap as a `bytes` string.
- `set_reverse()`: Prepares the set for reverse iteration.

## `SortedIntSet`

```python
class whoosh.idsets.SortedIntSet
```

A `DocIdSet` that stores integers as a sorted list of Python `int` objects.
More memory-efficient than `BitSet` for sparse sets but slower for membership
tests.

**Constructor:**
- Optional initial iterable of integers.

```python
from whoosh.idsets import SortedIntSet

sis = SortedIntSet([100, 500, 999])
print(500 in sis)  # True
print(sis.after(200))  # 500
```

## `ReverseIdSet`

```python
class whoosh.idsets.ReverseIdSet(child)
```

Wraps another `DocIdSet` to reverse the interpretation of integers. Instead
of representing membership directly, the set represents the *complement* of
the inner set. Useful for representing deleted documents.

**Constructor:**
- `child`: The `DocIdSet` to reverse.

**Example:** If `child` represents documents `{3, 7, 9}`, then
`ReverseIdSet(child)` represents all documents *except* `{3, 7, 9}`.

## `RoaringIdSet`

```python
class whoosh.idsets.RoaringIdSet
```

A `DocIdSet` that partitions integers into 16-bit buckets and uses `BitSet`
within each bucket. More memory-efficient than a single flat `BitSet` for
large, sparse sets of integers.

**Constructor:**
- Optional initial iterable of integers.

**Methods:**
- `from_bytes(data)`: Deserialize from bytes.
- `to_bytes()`: Serialize to bytes.
- `to_bytes_list()`: Returns a list of `(bucket, bytes)` pairs.

## `MultiIdSet`

```python
class whoosh.idsets.MultiIdSet(readers, offsets=None)
```

Combines multiple `DocIdSet` instances into one, handling document ID offsets
automatically. Used for combining deletions across multiple segments.

**Constructor:**
- `readers`: List of `DocIdSet` instances (one per segment).
- `offsets`: Optional list of base docnum offsets for each reader. If
  omitted, offsets are computed automatically.

**Methods:**
- `__contains__(i)`: Checks the appropriate sub-set based on offsets.
- `__iter__()`: Iterates over all integers in all sub-sets.
- `__len__()`: Returns the total count across all sub-sets.


## DOCUMENT (FR): Lang

> **Note de traduction** : Cette page n'est pas encore traduite en français.
> Le contenu anglais est affiché ci-dessous en attendant la traduction.

<!-- Creez une version francaise de ce fichier et supprimez ce message. -->


# Language Support API

Language detection helpers, stemmer selection, stop-word lists, and
language-specific modules (Snowball stemmers, ISRI stemmer, Soundex,
Double Metaphone, etc.).

## Module Overview

The `whoosh.lang` package provides functions for detecting and selecting
language-specific resources (stemmers, stop words) and submodules containing
stemmers for various languages.

## Supported Languages

```python
whoosh.lang.languages = ("ar", "da", "nl", "en", "fi", "fr", "de", "hu",
                         "it", "no", "pt", "ro", "ru", "es", "sv", "tr")
```

Two-letter ISO 639-1 language codes for which stemmers or stop-word lists
are available.

## Language Aliases

```python
whoosh.lang.aliases = { ... }
```

A dictionary mapping alternate language identifiers to their canonical
two-letter codes. Includes ISO 639-3 three-letter codes, English names,
and native-language names.

## Exceptions

### `NoStemmer`

```python
class whoosh.lang.NoStemmer
```

Raised by `stemmer_for_language()` when no stemmer is available for the
given language.

### `NoStopWords`

```python
class whoosh.lang.NoStopWords
```

Raised by `stopwords_for_language()` when no stop-word list is available for
the given language.

## Language Functions

### `two_letter_code`

```python
whoosh.lang.two_letter_code(name) -> str or None
```

Converts a language identifier to its canonical two-letter code. Accepts
two-letter codes, ISO 639-3 codes, English names, and native-language names.

```python
from whoosh.lang import two_letter_code

code = two_letter_code("french")   # 'fr'
code = two_letter_code("deutsch")  # 'de'
code = two_letter_code("español")  # 'es'
```

### `has_stemmer`

```python
whoosh.lang.has_stemmer(lang) -> bool
```

Returns `True` if a stemmer is available for the given language.

### `has_stopwords`

```python
whoosh.lang.has_stopwords(lang) -> bool
```

Returns `True` if a stop-word list is available for the given language.

### `stemmer_for_language`

```python
whoosh.lang.stemmer_for_language(lang) -> callable
```

Returns a stemmer function for the given language. Raises `NoStemmer` if
no stemmer is available.

**Supported languages and stemmers:**
- `"en"` / `"en_porter"`: Original Porter stemmer (`whoosh.lang.porter`)
- `"ar"`: ISRI Arabic stemmer (`whoosh.lang.isri`)
- `"da"`: Danish Snowball stemmer
- `"nl"`: Dutch Snowball stemmer
- `"en"`: English Snowball stemmer
- `"fi"`: Finnish Snowball stemmer
- `"fr"`: French Snowball stemmer
- `"de"`: German Snowball stemmer
- `"hu"`: Hungarian Snowball stemmer
- `"it"`: Italian Snowball stemmer
- `"no"`: Norwegian Snowball stemmer
- `"pt"`: Portuguese Snowball stemmer
- `"ro"`: (no stemmer currently)
- `"ru"`: Russian Snowball stemmer
- `"es"`: Spanish Snowball stemmer
- `"sv"`: Swedish Snowball stemmer
- `"tr"`: (no stemmer currently)

```python
from whoosh.lang import stemmer_for_language

stem = stemmer_for_language("en")
print(stem("running"))  # 'run'
```

### `stopwords_for_language`

```python
whoosh.lang.stopwords_for_language(lang) -> list
```

Returns the stop-word list for the given language. Raises `NoStopWords` if
no stop-word list is available.

```python
from whoosh.lang import stopwords_for_language

stops = stopwords_for_language("en")
```

## Snowball Stemmers

The `whoosh.lang.snowball` subpackage contains stemmers implementing the
Snowball stemming algorithms for various languages.

### Available Stemmers

| Module | Class | Language |
|--------|-------|----------|
| `snowball.english` | `EnglishStemmer` | English |
| `snowball.dutch` | `DutchStemmer` | Dutch |
| `snowball.finnish` | `FinnishStemmer` | Finnish |
| `snowball.french` | `FrenchStemmer` | French |
| `snowball.german` | `GermanStemmer` | German |
| `snowball.hungarian` | `HungarianStemmer` | Hungarian |
| `snowball.italian` | `ItalianStemmer` | Italian |
| `snowball.norwegian` | `NorwegianStemmer` | Norwegian |
| `snowball.portugese` | `PortugueseStemmer` | Portuguese |
| `snowball.russian` | `RussianStemmer` | Russian |
| `snowball.romanian` | `RomanianStemmer` | Romanian |
| `snowball.spanish` | `SpanishStemmer` | Spanish |
| `snowball.swedish` | `SwedishStemmer` | Swedish |
| `snowball.danish` | `DanishStemmer` | Danish |

### Base Classes

```python
class whoosh.lang.snowball.bases._ScandinavianStemmer
class whoosh.lang.snowball.bases._StandardStemmer
```

Internal base classes for Snowball stemmers. User code should use the
language-specific stemmer classes directly.

### `classes`

```python
whoosh.lang.snowball.classes = {"da": DanishStemmer, "nl": DutchStemmer, ...}
```

Dictionary mapping two-letter language codes to Snowball stemmer classes.

## Porter Stemmer

### `whoosh.lang.porter`

The original Porter stemming algorithm, faster but less accurate than
Snowball English stemmer.

#### `stem`

```python
whoosh.lang.porter.stem(w) -> str
```

Stems a single English word using the Porter algorithm.

## ISRI Stemmer

### `whoosh.lang.isri.ISRIStemmer`

```python
class whoosh.lang.isri.ISRIStemmer
```

Arabic stemmer based on the Information Science Research Institute (ISRI)
algorithm. Does not use a root dictionary.

#### `stem`

```python
def ISRIStemmer.stem(word) -> str
```

Stems an Arabic word.

## Double Metaphone

### `whoosh.lang.dmetaphone.double_metaphone`

```python
whoosh.lang.dmetaphone.double_metaphone(text) -> tuple
```

Returns a tuple of `(primary, secondary)` metaphone codes for the given
text, using the Double Metaphone algorithm.

## Soundex

### `whoosh.lang.phonetic`

Soundex implementations for phonetic matching.

#### `soundex_en`

```python
whoosh.lang.phonetic.soundex_en(word) -> str
```

English Soundex encoding.

#### `soundex_esp`

```python
whoosh.lang.phonetic.soundex_esp(word) -> str
```

Spanish Soundex encoding.

#### `soundex_ar`

```python
whoosh.lang.phonetic.soundex_ar(word) -> str
```

Arabic Soundex encoding.

## WordNet Thesaurus

### `whoosh.lang.wordnet.Thesaurus`

```python
class whoosh.lang.wordnet.Thesaurus
```

Provides synonym expansion based on WordNet-style data.

**Methods:**
- `synonyms(word)`: Returns the set of synonyms for `word`.
- `__contains__(word)`: Returns `True` if `word` is in the thesaurus.

### Functions

```python
whoosh.lang.wordnet.parse_file(f) -> dict
whoosh.lang.wordnet.make_index(storage, indexname, word2nums, num2words)
whoosh.lang.wordnet.synonyms(word2nums, num2words, word) -> set
```

## Lovins Stemmer

### `whoosh.lang.lovins`

A suffix-stripping stemmer by Lovins. Functions include:
- `stem(word)`: Main stemming function.
- `remove_ending(word)`: Removes suffixes.
- `fix_ending(word)`: Fixes the word ending after stemming.

## Paice-Husk Stemmer

### `whoosh.lang.paicehusk.PaiceHuskStemmer`

```python
class whoosh.lang.paicehusk.PaiceHuskStemmer(rules)
```

A rule-based stemmer using Paice-Husk rules.

#### `stem`

```python
def PaiceHuskStemmer.stem(word) -> str
```

Stems a word using the Paice-Husk algorithm.

**Usage note:** The module also exposes a pre-configured stemmer:
```python
whoosh.lang.paicehusk.stem = PaiceHuskStemmer(defaultrules).stem
```


## DOCUMENT (FR): Matching

> **Note de traduction** : Cette page n'est pas encore traduite en français.
> Le contenu anglais est affiché ci-dessous en attendant la traduction.

<!-- Creez une version francaise de ce fichier et supprimez ce message. -->


# Matching API

Classes and functions for iterating over and combining result sets during
searching. The matching module is a refactored package exposing the same
public API as the former monolithic module.

## Overview

When you search an index, Whoosh creates `Matcher` objects representing the
postings (document IDs and scores) produced by query objects. Matchers can
be combined (e.g., union, intersection) to build compound queries. The
matching module provides the core `Matcher` class hierarchy, utility
functions, and concrete implementations for various query types.

## Core Matcher Classes

### `Matcher`

```python
class whoosh.matching.Matcher
```

Abstract base class for all matchers. Concrete subclasses implement
`__init__()` and the `_set()` and `_maybe_values()` methods.

**Methods:**

#### `init = property(is_active)`

Property that returns whether the matcher is "active" (at top of segment
postings, not exhausted).

#### `init(view, docnum, score)`

Called when the matcher is initialized.

#### `set(matcher)`

Replaces this matcher with another one.

#### `copy()`

Returns a copy of this matcher.

#### `all_ids()`

Returns a list of docnums matched by this matcher.

#### `matches(matcher)`

Returns `True` if any of the current matches in `self` also match in
`matcher`.

#### `skip_to(docid)`

Advances the matcher to the first match at or after `docid`.

#### `skip_to_intersect(matcher)`

Moves this matcher to the earliest matching docnum that is also matched in
`matcher`.

#### `next()`

Advances the matcher to the next match.

#### `next_in_segment()`

Advances to the next match in the current segment.

#### `next_segment(matcher)`

Advances to the next segment in the context of `matcher`.

#### `is_active(in_segment=False)`

Returns `True` if this matcher has more matches to process.

#### `all_matching_segments()`

Generates `(segment_num, matcher)` pairs for all matching segments.

#### `doc()`

Returns the current document number of this matcher. May advance to next
document if not already on one.

#### `docnum()`

Returns the current docnum (segment-relative) of the matcher.

#### `score()`

Returns the current match's score.

#### `value()`

Returns the current match's value (e.g., the decoded stored value of the
term).

#### `supports()`

Returns `True` if `value()` is supported.

#### `value_matches()`

Returns the value at the current match.

#### `all_values()`

Returns a list of all values in this matcher.

#### `supports_lee()`

Returns `True` if the matcher uses lazy evaluation.

#### `lee`

Returns the current "lazy evaluation extension" value (for term vectors).

#### `spans()`

If the postings include positions, returns a list of `Position` objects for
the current match.

#### `spans()`

Returns the spans (positions) of the match in the current document.

#### `next_type()`

Returns the type of the next match.

#### `copy()`

Returns a shallow copy of this matcher.

### `Child`

```python
class whoosh.matching.Child
```

Mixin class for matchers that wrap other matchers.

### `FilterMixin`

```python
class whoosh.matching.FilterMixin
```

Mixin for matchers used as filters (boolean scoring, no relevance).

### `Custom`

```python
class whoosh.matching.Custom
```

Mixin for matchers that return a custom score from `score()` rather than 1.

### `Constant`

```python
class whoosh.matching.Constant
```

Mixin for matchers whose score is always the same value.

### `Coord`

```python
class whoosh.matching.Coord
```

Mixin for matchers that compute coordination factor (for phrase and other
queries that benefit from it).

## Concrete Matcher Classes

### `ListUnion`

```python
class whoosh.matching.ListUnion(matcher, items, maptype=None)
```

Base class for matchers that combine multiple matchers with a list of keys.

#### `filter`

```python
class whoosh.matching.filter
```

Decorator for creating filter matchers (boolean matchers with no relevance).

### `Union`

```python
class whoosh.matching.Union(matcher, items)
```

Base class for the `OR` operator.

### `Intersection`

```python
class whoosh.matching.Intersection(matcher, items)
```

The `AND` operator. A document matches only if it appears in all the child
matchers.

#### `IntersectionFilter`

```python
class whoosh.matching.IntersectionFilter(matcher, items)
```

A filter (no scoring) version of intersection.

### `And`

```python
class whoosh.matching.And(matcher, items)
```

Alias for `Intersection`.

### `Or`

```python
class whoosh.matching.Or(matcher, items)
```

Alias for `Union`.

### `Not`

```python
class whoosh.matching.Not(matcher, a, b)
```

The `NOT` operator. Matches all documents in `a` that are not in `b`.

### `Require`

```python
class whoosh.matching.Require(matcher, a, b)
```

Matches documents in `a` only if they also appear in `b`, but does not add
`b`'s score.

### `AndNot`

```python
class whoosh.matching.AndNot(matcher, a, b)
```

Matches documents in `a` that are not in `b`.

#### `AndMaybe`

```python
class whoosh.matching.AndMaybe(matcher, a, b)
```

Matches documents in `a`, adding `b`'s score if present.

### `BinaryUnion`

```python
class whoosh.matching.BinaryUnion(items)
```

Efficient intersection of exactly two matchers.

#### `BinaryUnion2`

```python
class whoosh.matching.BinaryUnion2
```

Optimized binary union for two items.

### `TreeMatcher`

```python
class whoosh.matching.TreeMatcher
```

A matcher that wraps a `Tree` object for combining results.

### `NestedParent`

```python
class whoosh.matching.NestedParent(parent, child, bools=False)
```

Matches parent documents that have at least one child document matched by
the child matcher. Used for nested document queries.

### `NestedChildren`

```python
class who which.matching.NestedChildren(parentmatch, child)
```

Matches child documents for a given parent document.

### `LengthMatcher`

```python
class whoosh.matching.LengthMatcher(child, q, polarity=False)
```

Matches documents based on field length (used by `Every` query).

### `Filter`

```python
class whoosh.matching.Filter(matcher)
```

Converts any matcher into a filter (no scoring).

### `AlwaysFilter`

```python
class whoosh.matching.AlwaysFilter
```

A filter that matches all documents.

### `NeverFilter`

```python
class whoosh.matching.NeverFilter
```

A filter that matches no documents.

### `PseudoMatcher`

```python
class whoosh.matching.PseudoMatcher
```

Base class for pseudo-matchers used in span queries.

## Matching Utilities

### `current_spans`

```python
whoosh.matching.current_spans(matcher) -> list
```

Returns a list of `Span` objects for the current match in `matcher`, or an
empty list if the matcher doesn't support positions.

### `disjunction_score`

```python
whoosh.matching.disjunction_score(matcher) -> float
```

Returns the sum of `matcher.score()` and the scores of all child matchers of
type `Union`.

### `intersection_score`

```python
whoosh.matching.intersection_score(matcher) -> float
```

Returns the sum of `matcher.score()` and all child matchers of type
`Intersection`.

### `child_count`

```python
whoosh.matching.child_count(matcher) -> int
```

Returns the number of child matchers in `matcher`.

### `has_quality`

```python
whoosh.matching.has_quality(matcher) -> bool
```

Returns `True` if `matcher` has a `query` attribute (i.e., is a
`QueryMatcher`-derived object, or a combination of such matchers).

### `has_untranslated`

```python
whoosh.matching.has_untranslated(matcher) -> bool
```

Returns `True` if the matcher has an `untranslated` attribute (set by
certain wrapper matchers like `TimeLimited`).

### `wrap`

```python
whoosh.matching.wrap(matcher)
```

Returns `matcher` if it has a `.copy()` method, otherwise wraps it in an
`AutoMatcher`.

### `wrap2`

```python
whoosh.matching.wrap2(a, b, m)
```

Returns either a `BinaryUnion2` or an `AutoMatcher` depending on whether `a`
and `b` are list-compatible.

### `unified`

```python
whoosh.matching.unified(matcher)
```

Returns `matcher` if it has an `untranslated` attribute, otherwise returns
`None`.

### `deletion`

```python
whoosh.matching.deletion(matcher)
```

If `matcher` has a `parent` attribute, returns the parent, otherwise returns
`None`.

### `AutoMatcher`

```python
class whoosh.matching.AutoMatcher(m, **kwargs)
```

A general-purpose matcher that wraps arbitrary objects and adds default
behavior for scoring, docnums, and other features. Created by `wrap()`.

### `MatchingTimeLimit`

```python
class whoosh.matching.MatchingTimeLimit
```

A lightweight exception raised when a query matcher exceeds a time limit.

### `TimeLimited`

```python
class whoosh.matching.TimeLimited(child, maxsteps=100, timeout=None, currenttime=None)
```

Wrapper that wraps a `Matcher` to enforce a time limit. Raises
`MatchingTimeLimit` if the time limit is exceeded.

**Parameters:**
- `child`: The matcher to wrap.
- `maxsteps`: Check time every N documents (default `100`).
- `timeout`: Maximum time in seconds (default `None`, no limit).
- `currenttime`: Optional function to use for getting the current time.

### `TermMatcher`

```python
class whoosh.matching.TermMatcher(postings, text, qname, scorer=None, boost=1.0)
```

Matches documents containing a specific term.

**Constructor:**
- `postings`: A `Postings` object from the index reader.
- `text`: The term text.
- `qname`: The query name for this term.
- `scorer`: Optional `Scorer` object.
- `boost`: Boost factor for this term's score.

### `MultiScorer`

```python
class whoosh.matching.MultiScorer(numgroups, start_i=0)
```

A `Scorer` that combines the scores from multiple scorers into one, weighted
across groups of segments.

### `RangeMatcher`

```python
class whoosh.matching.RangeMatcher(start_matcher, end_matcher, query)
```

Matches documents within a range of term values.

### `RegexMatcher`

```python
class whoosh.matching.RegexMatcher(regex, qname, boost=1.0)
```

Matches documents whose terms match a compiled regex.

### `SpanMatcher`

```python
class whoosh.matching.SpanMatcher(matcher, order=0, end=0)
```

Matches spans (positions) within documents.

### `SpanOverlap`

```python
class whoosh.matching.SpanOverlap(l, r)
```

Matches overlapping spans from two matchers.

### `SpanNear`

```python
class whoosh.matching.SpanNear(l, r, slop=1, ordered=True)
```

Matches spans that are near each other within a document.

### `SpanCondition`

```python
class whoosh.matching.SpanCondition(l, r)
```

Matches a condition on spans.

### `SpanBefore`

```python
class whoosh.matching.SpanBefore(l, r, end=0)
```

Matches spans before a given position.

### `SpanAfter`

```python
class whoosh.matching.SpanAfter(l, r, end=0)
```

Matches spans after a given position.

### `SpanOutside`

```python
class whoosh.matching.SpanOutside(l, r, end=0)
```

Matches spans outside a given range.

### `SpanFirst`

```python
class whoosh.matching.SpanFirst(l, start=0, end=1)
```

Matches spans at the beginning of a document.

### `SpanNot`

```python
class whoosh.matching.SpanNot(l, r)
```

Matches spans in `l` that are not in `r`.

### `SpanOr`

```python
class whoosh.matching.SpanOr(items)
```

Logical OR for span matchers.

### `SpanAnd`

```python
class whoosh.matching.SpanAnd(l, r)
```

Logical AND for span matchers.


## DOCUMENT (FR): Middleware

# API Middleware

Pipeline de middleware pour les opérations d'indexation et de recherche.

## Classes principales

### Middleware

```python
class whoosh.middleware.base.Middleware
```

Classe de base pour tous les middlewares.

#### Méthodes

| Hook | Signature | Appelé quand |
|------|-----------|--------------|
| startup | (context) -> context | Initialisation writer/searcher |
| shutdown | (context) -> context | Nettoyage writer/searcher |
| before_index | (context) -> context | Avant l'ajout d'un document |
| after_index | (context) -> context | Après l'ajout d'un document |
| before_delete | (context) -> context | Avant la suppression |
| after_delete | (context) -> context | Après la suppression |
| before_search | (context) -> context | Avant la recherche |
| after_search | (context) -> context | Après les résultats |
| on_error | (context, exc) -> None | Sur exception |
| on_commit | (context) -> None | Après le commit |

### MiddlewareContext

```python
class whoosh.middleware.context.MiddlewareContext(
    operation: str,
    metadata: dict | None = None
)
```

Attributs:

| Attribut | Type | Description |
|----------|------|-------------|
| `operation` | str | `"index"`, `"search"`, `"delete"`, `"commit"` |
| `query` | Any | Requête (pour `search`) |
| `results` | Any | Résultats de la recherche |
| `document` | dict | Document à indexer (pour `index`) |
| `docnum` | int | Numéro du document (pour `delete`) |
| `metadata` | dict | Données par requête (request_id, trace_id) |

### MiddlewareChain

```python
class whoosh.middleware.base.MiddlewareChain(middlewares)
```

Orchestre l'exécution des middlewares dans l'ordre.

#### Méthodes

| Méthode | Description |
|---------|-------------|
| `chain.add(middleware)` | Ajoute un middleware |
| `chain.run_before(hook, context)` | Exécute les hooks before |
| `chain.run_after(hook, context)` | Exécute les hooks after |
| `chain.run_before_all(hook, context)` | Exécute tous les hooks before |
| `chain.run_after_all(hook, context)` | Exécute tous les hooks after |

### SettingGuard

```python
class whoosh.middleware.base.SettingGuard(
    field: str | None = None,
    default: bool = False
)
```

Vérifie et réinitialise les settings de middleware.

### Skip

```python
class whoosh.middleware.base.Skip(metadata)
```

Exception pour sauter une opération tout en la commitant.

## Intégration

### apply_middleware_to_writer

```python
def apply_middleware_to_writer(
    writer: IndexWriter,
    middlewares: list[Middleware]
) -> IndexWriter
```

Retourne un writer enveloppé par les middlewares.

### apply_middleware_to_searcher

```python
def apply_middleware_to_searcher(
    searcher: Searcher,
    middlewares: list[Middleware]
) -> Searcher
```

Retourne un searcher enveloppé par les middlewares.

## Exceptions

```python
class MiddlewareError(Exception)
class StopOperation(Exception)
class SkipOperation(Skip)
```


## DOCUMENT (FR): Modern

# Fournisseurs de stockage

Whoosh-NG fournit des backends de stockage pluggables via les contrats
`SyncStorageProvider` / `AsyncStorageProvider`. Cela permet de persister
l'index sur disque local, SQLite, S3, ou une configuration hybride cache +
distant sans modifier le writer ni l'index.

## Vue d'ensemble de l'architecture

### Niveau 1 : SnapshotStorage (Simple)

```
Writer → FS local → Commit → Upload Segment → S3
Reader → Download Segment → Open local
```

Très simple à maintenir. Utilisez `SnapshotStorage` quand vous voulez S3
comme cible de sauvegarde/restauration simple sans la complexité d'un cache
local.

### Niveau 2 : CachedObjectStorage (Recommandé pour la production)

```
+----------+
|  MinIO   |
+----------+
     ^
     |
 Sync |
     v
+-----------+   Couche Cache   +-----------+
| Searcher  |<--------------->| Writer    |
+-----------+                 +-----------+
        |
        v
 Local SSD
```

- L'index vit sur SSD
- S3 sert de réplication
- Les segments sont poussés après commit
- Restauration possible à tout moment

C'est exactement ce que font beaucoup de systèmes de recherche distribués
modernes.

## Fournisseurs disponibles

| Fournisseur | Type | Backend | Cas d'usage |
|-------------|------|---------|-------------|
| `FileStorage` | sync | système de fichiers local | Single-node, pas de cloud |
| `AsyncFileStorage` | async | système de fichiers local | Single-node async |
| `S3Storage` | sync | compatible S3 | Accès S3 direct |
| `SnapshotStorage` | sync | compatible S3 | Sauvegarde/restauration simple |
| `HybridStorage` | sync | cache local + distant | **Production** (alias : `CachedObjectStorage`) |
| `AsyncHybridStorage` | async | cache local + distant | Production async |

Tous les fournisseurs sont importables depuis `whoosh_modern.storage`.

## FileStorage

Stockage local sur système de fichiers. Les clés sont des chemins relatifs
sous ``root``.

```python
from whoosh_modern.storage import FileStorage

storage = FileStorage("indexdir")
storage.write("segment_1.dat", b"data")
assert storage.read("segment_1.dat") == b"data"
assert storage.exists("segment_1.dat") is True
storage.delete("segment_1.dat")
keys = storage.list_keys()
```

## AsyncFileStorage

Variante async de ``FileStorage``. Toutes les opérations s'exécutent dans
un thread de travail via ``asyncio.to_thread``.

```python
import asyncio
from whoosh_modern.storage import AsyncFileStorage

storage = AsyncFileStorage("indexdir")

async def main() -> None:
    await storage.awrite("segment_1.dat", b"data")
    data = await storage.aread("segment_1.dat")
    await storage.adelete("segment_1.dat")

asyncio.run(main())
```

## S3Storage

Stockage blobs compatible S3. ``boto3`` est requis uniquement lorsque ce
fournisseur est utilisé ; il est importé paresseusement pour que le reste
de Whoosh-NG n'en dépende pas. Un ``client`` peut être injecté pour les tests.

```python
from whoosh_modern.storage import S3Storage

storage = S3Storage(bucket="mon-bucket", prefix="segments")
storage.write("segment_1.dat", b"data")
data = storage.read("segment_1.dat")
keys = storage.list_keys()
```

Installer la dépendance optionnelle :

```bash
pip install whoosh-ng[s3]
```

## SnapshotStorage

Stockage snapshot S3 simple sans cache local. C'est la stratégie la plus
simple :

- Écriture : upload du segment directement vers S3
- Lecture : download du segment depuis S3 vers un fichier temporaire local

Utilisez ceci quand vous voulez S3 comme cible de sauvegarde/restauration
simple sans la complexité d'un cache local.

```python
from whoosh_modern.storage import SnapshotStorage

storage = SnapshotStorage(
    local_path="./index",
    bucket="mon-bucket",
    prefix="snapshots",
)

storage.write("segment_1.dat", b"data")
data = storage.read("segment_1.dat")
```

## HybridStorage / CachedObjectStorage

`HybridStorage` compose un cache local avec un backend distant. Le distant
est la source de vérité ; le cache local est une couche de performance
write-through.

`CachedObjectStorage` est un alias de `HybridStorage` qui exprime mieux
l'intention : un cache d'objets local synchronisé avec S3.

C'est l'architecture recommandée pour les déploiements production avec des
motifs de lecture répétés.

```python
from whoosh_modern.storage import HybridStorage, S3Storage

distant = S3Storage(bucket="mon-bucket", prefix="segments")
stockage = HybridStorage(local_cache="./cache", remote=distant)

# Write-through : le distant est la source de vérité, le cache est mis à jour
stockage.write("segment_1.dat", b"data")

# Première lecture : miss cache → fetch depuis S3, write-through dans le cache
data = stockage.read("segment_1.dat")

# Deuxième lecture : hit cache → servi depuis le disque local, zéro réseau
data = stockage.read("segment_1.dat")

# Forcer le rafraîchissement depuis le distant
stockage.invalidate("segment_1.dat")

# Pré-chauffer le cache
stockage.prefetch(["segment_2.dat", "segment_3.dat"])
```

### Chemin de lecture

1. hit cache local → retour immédiat
2. miss → lecture depuis le distant, write-through dans le cache, retour

### Chemin d'écriture

- ``distant.write(key, data)`` (source de vérité)
- en cas de succès → ``local_cache.write(key, data)``
- en cas d'échec → lever l'erreur avant de polluer le cache

### Éviction du cache

Le cache local est limité par `max_cache_size_mb` (défaut 1024 Mo). Quand
la limite est atteinte, les entrées les plus anciennes sont évincées selon
une politique LRU.

### `list_keys`

`list_keys()` utilise le distant comme source de vérité car le cache n'est
que partiel. Passez `include_cache=True` pour retourner l'union des clés
distant et cache.

## AsyncHybridStorage

Variante async de ``HybridStorage``. Les opérations distantes s'exécutent
dans un thread de travail via ``asyncio.to_thread`` pour ne jamais bloquer
la boucle d'événements.

```python
import asyncio
from whoosh_modern.storage import AsyncHybridStorage, S3Storage

distant = S3Storage(bucket="mon-bucket", prefix="segments")
stockage = AsyncHybridStorage(local_cache="./cache", remote=distant)

async def main() -> None:
    await stockage.awrite("segment_1.dat", b"data")
    data = await stockage.aread("segment_1.dat")
    await stockage.adelete("segment_1.dat")
    cles = await stockage.alist_keys()

asyncio.run(main())
```

## Utilisation avec SearchApplication

```python
from whoosh_modern import SearchApplication, SQLSource
from whoosh_modern.storage import HybridStorage, S3Storage

distant = S3Storage(bucket="mon-bucket", prefix="segments")
stockage = HybridStorage(local_cache="./cache", remote=distant)

app = SearchApplication(
    source=SQLSource(query="SELECT * FROM produits", connection=engine),
    storage=stockage,
)
app.build()
resultats = app.index.search("laptop")
```

## Benchmarks de performance

Les benchmarks ont été exécutés contre une instance MinIO locale en utilisant
un index Whoosh de 28,89 Mo (2 fichiers de segment). Les résultats indiquent
les performances relatives entre les stratégies sur un stockage compatible S3.

| Stratégie | Sauvegarde (Mo/s) | Restauration (Mo/s) | Notes |
|-----------|-------------------|---------------------|-------|
| `1_obj_per_segment` | 39.44 | 139.72 | Meilleur débit de restauration ; le plus simple |
| `compressed_zstd` | 31.56 | 133.74 | Bande passante réduite, overhead CPU |
| `hybrid_cache_s3` | 44.97 | 133.61 | Meilleure sauvegarde ; lectures cache chaud excellentes |
| `1_obj_per_posting_list` | 0.28 | 4.79 | **À éviter** : millions de petits objets tuent S3 |

### Recommandations

- **Par défaut** : `S3Storage` avec 1 objet par fichier de segment. Il offre
  le meilleur débit de restauration et est le plus simple à exploiter.
- **Production avec lectures répétées** : `HybridStorage(cache_local, S3Storage)`.
  Après le premier accès, les lectures suivantes sont servies depuis le disque
  local à ~133 Mo/s.
- **À éviter** : 1 objet par posting list. S3 n'est pas optimisé pour des
  millions de petits objets ; la latence et le coût explosent.
- **Compression** : ZSTD réduit la taille des transferts de ~20-30% au prix
  d'un overhead CPU. À utiliser quand la bande passante réseau est le
  goulot, pas quand le CPU l'est.

### Exécution des benchmarks

```bash
# Démarrer MinIO
docker run -d --name minio-benchmark -p 9000:9000 -p 9001:9001 \
  -e MINIO_ROOT_USER=minioadmin -e MINIO_ROOT_PASSWORD=minioadmin \
  minio/minio:latest server /data --console-address ":9001"

# Lancer le benchmark synthétique
python benchmark/s3_storage_benchmark.py

# Lancer le benchmark avec un vrai index Whoosh (nécessite le CSV customers)
python benchmark/s3_storage_benchmark_real.py
```


## DOCUMENT (FR): Overview

# Vue d'ensemble API

Cette section fournit une référence complète de l'API publique de Whoosh-NG.

## Modules

| Module | Description |
|--------|-------------|
| `whoosh.index` | Gestion des indexes |
| `whoosh.fields` | Types de champs et schéma |
| `whoosh.writing` | Writers et politiques de fusion |
| `whoosh.searching` | Searcher, Results, collectors |
| `whoosh.query` | Classes de requêtes |
| `whoosh.qparser` | Analyseur de requêtes |
| `whoosh.analysis` | Tokenizers, filtres, analyseurs |
| `whoosh.highlight` | Surbrillance des résultats |
| `whoosh.spelling` | Correction orthographique |
| `whoosh.sorting` | Facettes et tri |
| `whoosh.event_bus` | Système d'événements |
| `whoosh.hooks` | Système de hooks |
| `whoosh.middleware` | Pipeline de middleware |
| `whoosh.plugins` | Système de plugins et registres |
| `whoosh.backends` | Backends de stockage |
| `whoosh.vector` | Providers de recherche vectorielle |
| `whoosh_modern.autocomplete` | Providers d'autocomplétion |
| `whoosh_fastapi` | Intégration FastAPI |

## Référence rapide

### Cycle de vie d'un index

```python
from whoosh.index import create_in, open_dir, exists_in

ix = create_in("indexdir", schema)
ix = open_dir("indexdir")
exists = exists_in("indexdir")
```

### Écriture

```python
with ix.writer() as writer:
    writer.add_document(champ1=val1, champ2=val2)
```

### Lecture

```python
from whoosh.qparser import QueryParser

with ix.searcher() as searcher:
    qp = QueryParser("content", ix.schema)
    q = qp.parse("requête")
    results = searcher.search(q)
```

### Schéma

```python
from whoosh.fields import Schema, TEXT, ID, NUMERIC

schema = Schema(
    titre=TEXT(stored=True),
    chemin=ID(stored=True, unique=True),
    compte=NUMERIC(int, stored=True)
)
```


## DOCUMENT (FR): Plugins

# API Plugins

Système de plugins, registres de providers et discovery.

## PluginManager

```python
class whoosh.plugins.manager.PluginManager
```

Point d'entrée unique pour gérer tous les plugins.

### Méthodes

| Méthode | Description |
|---------|-------------|
| `PluginManager.load_plugins()` | Auto-découvre et charge les entry points |
| `PluginManager.register(name, plugin)` | Enregistre un plugin |
| `PluginManager.enable(name)` | Active un plugin |
| `PluginManager.disable(name)` | Désactive un plugin |
| `PluginManager.get(name)` | Retourne le plugin par nom |
| `PluginManager.list_plugins()` | Liste des plugins enregistrés |

**Exemple:**
```python
from whoosh.plugins.manager import PluginManager

# Programmatique
PluginManager.register("mon_plugin", MonPlugin())
PluginManager.enable("mon_plugin")

# Entry points (dans pyproject.toml)
# [project.entry-points."whoosh_ng.plugins"]
# mon_plugin = "mon_package.plugin:MonPlugin"
PluginManager.load_plugins()
```

## BasePlugin

```python
class whoosh.plugins.base.BasePlugin
```

Classe de base pour tous les plugins.

### Méthodes à implémenter

| Méthode | Appelée quand |
|---------|---------------|
| `setup(registry)` | Plugin activé |
| `teardown(registry)` | Plugin désactivé |
| `on_startup()` | Démarrage application |
| `on_shutdown()` | Arrêt application |

### Attributs

| Attribut | Description |
|----------|-------------|
| `name` | Nom unique |
| `version` | Version du plugin |
| `dependencies` | Liste des noms de plugins requis |

## Registre

```python
class whoosh.registry.Registry
```

Registre global pour les providers de tous types.

### Méthodes

| Méthode | Description |
|---------|-------------|
| `registry.register(name, provider, category)` | Enregistre un provider |
| `registry.get(name, category)` | Récupère un provider |
| `registry.unregister(name, category)` | Supprime un provider |
| `registry.list_providers(category)` | Liste tous les providers d'une catégorie |

**Catégories courantes:**
- `"vector_provider"`
- `"autocomplete_provider"`
- `"storage_provider"`
- `"middleware"`

## VectorRegistry

```python
from whoosh.registry import VectorRegistry

VectorRegistry.register("numpy", NumpyProvider(), "mon_app")
provider = VectorRegistry.get("numpy", "mon_app")
```

## Exception

```python
class PluginNotFoundError(Exception)
```


## DOCUMENT (FR): Query

# API Requêtes

Construisez des requêtes complexes avec l'API de Whoosh-NG.

## Classes principales

### Query

Classe de base pour toutes les requêtes.

```python
class whoosh.query.Query
```

#### Méthodes

| Méthode | Description |
|---------|-------------|
| `q.all(methodname, *args)` | Applique une méthode à tous les termes |
| `q.normalize()` | Normalise la représentation textuelle |
| `q.replace(fieldname, old, new)` | Remplace un terme |
| `q.exclude(term)` | Exclut un terme spécifique |
| `q.fieldname` | Champ principal de la requête |
| `q.children()` | Requêtes enfants (pour opérateurs) |

## Requêtes booléennes

### And

```python
And(require, boost=1.0)
# Tous doivent matcher
```

### Or

```python
Or(require, boost=1.0)
# Un seul doit matcher
```

### Not

```python
Not(require, exclude=None)
```

### Requête Term

```python
Term(fieldname, text, boost=1.0)
```

## Plages

### NumericRange

```python
NumericRange(fieldname, start, end, startexcl=False, endexcl=False)
```

### DateRange

```python
DateRange(fieldname, start, end, startexcl=False, endexcl=False)
```

## Phrase et proximité

### Phrase

```python
Phrase(fieldname, words, slop=1, boost=1.0)
```

### Distance / Near

### Prefix

```python
Prefix(fieldname, text, boost=1.0)
```

## Combinateurs avancés

### Every

```python
Every(fieldname, boost=1.0)
```

### Null

```python
NullQuery
```

## Opérateurs de requête globaux

| Requête | Description |
|---------|-------------|
| `Term` | Égalité exacte (pas d'analyse) |
| `Variations` | Variantes lexicales |
| `FuzzyTerm` | Recherche floue |
| `Wildcard` | Jokers (lent sur grands corpus) |
| `Regex` | Expression régulière |

## Construction manuelle

```python
from whoosh.query import (
    Term, And, Or, Not, Phrase, NumericRange, DateRange
)

q = And([
    Term("status", "published"),
    NumericRange("date", 2020, 2025),
    Or([Term("tags", "python"), Term("tags", "recherche")]),
    Phrase("content", ["tutoriel", "whoosh"])
])
```


## DOCUMENT (FR): Reading

> **Note de traduction** : Cette page n'est pas encore traduite en français.
> Le contenu anglais est affiché ci-dessous en attendant la traduction.

<!-- Creez une version francaise de ce fichier et supprimez ce message. -->


# Reading API

Classes and functions for reading from an index. The reading module is a
refactored package exposing the same public API as the former monolithic
module.

## Overview

The reading module provides classes for accessing documents, terms, and
postings in an index. The main entry points are `IndexReader` objects obtained
from a searcher. These readers allow you to enumerate terms, access stored
fields, iterate postings, and get term frequencies.

## Core Classes

### `IndexReader`

```python
class whoosh.reading.IndexReader
```

Abstract base class for reading index data. Concrete subclasses include
`SegmentReader` and `MultiReader` (which wraps multiple segment readers).

### `MultiReader`

```python
class whoosh.reading.MultiReader(readers, base=None)
```

Combines multiple `IndexReader` instances into one. All docnums are treated
as relative to the combined index.

**Constructor:**
- `readers`: A list of `IndexReader` instances.
- `base`: Optional list of cumulative document count offsets for each reader.

**Methods:**

#### `doc_frequency(fieldname, text)`

Returns the total number of documents that have the given term in the given
field across all sub-readers.

#### `documents()`

Yields dictionaries of stored fields for each document in the index.

#### `stored_fields(docnum)`

Returns a dictionary of stored field values for the given document number
(index-wide docnum).

```python
r = my_index.reader()
print(r.stored_fields(20))
```

#### `all_stored_fields()`

Yields a `(docnum, stored_fields)` tuple for each document in the index.

#### `terms(fieldname)`

Yields `(fieldname, text)` tuples for every term in the given field.

#### `terms_from(segmentreader,fieldnameprefix)`

Low-level method for multi-reader.

#### `has_termvector(docnum, fieldname)`

Returns `True` if the document has a term vector for the given field.

#### `term_vector(docnum, fieldname)`

Returns a `TermVector` for the given document and field.

#### `is_deleted(docnum)`

Returns `True` if the given document (index-wide docnum) is deleted.

#### `all_doc_ids()`

Returns a sorted array of non-deleted document IDs.

#### `min_spam(fieldname)`

Returns the minimum spam value for the given field.

#### `set_spam(fieldname)`

Returns the set spam value for the given field.

#### `has_exact_length(docnum)`

Returns `True` if the exact length is known for `docnum`.

#### `doc_field_length(docnum, fieldname=None, default=1)`

Returns the length of the given field in the given document.

```python
r = my_index.reader()
length = r.doc_field_length(20, "content")
```

#### `max_field_length(fieldname)`

Returns the maximum length of the given field across all documents.

#### `iter_fieldname`

Low-level method for multi-reader.

#### `lexicon(fieldname)`

Returns an array of all unique terms in the given field, sorted.

#### `expanded_lexicon(fieldname)`

Low-level method that yields terms without the overhead of building an array.

#### `term_info(fieldname, text)`

Returns a `TermInfo` object for the given term, or `None` if the term does
not appear in the index.

#### `terminfos(fieldname)`

Yields `(text, TermInfo)` pairs for the given field.

#### `postings(fieldname, text, stype=None)`

Returns a `Matcher` for the postings list of the given term.

```python
r = my_index.reader()
m = r.postings("content", "whoosh")
for docnum, score in m:
    print("doc %d has term" % docnum)
```

#### `_all_postings(fieldname)`

Low-level. Yields `(text, matcher)` pairs for all terms in a field.

#### `_posting_fragments()`

Low-level.

#### `has_vector(docnum, fieldname)`

Returns `True` if the given field has a term vector in the given document.

#### `vectors(docnum)`

Yields `(fieldname, TermVector)` pairs for all term vectors in the document.

#### `all_items(fieldname)`

Yields `(term, weight, docfreq)` tuples for every term in the given field.

#### `frequency(fieldname, text)`

Returns the total frequency of the term across all documents.

#### `idf(term)`

Returns an iterator of `(docnum, idf)` pairs for the given term.

#### `spelling`

Returns a `SpellingAnalyzer` for the given field.

#### `doc_term(slicenum, fieldname, word)`

Returns `(df, weight)` for `word` in `fieldname` in segment `slicenum`.

#### `doc_diff(slicenum, fieldname, text, num)`

Returns `(df, weight)` for `word` in `fieldname` in segment `slicenum`.

### `SegmentReader`

```python
class whoosh.reading.SegmentReader(segment, schema, storage, base=True)
```

Reader for a single segment of the index.

**Constructor:**
- `segment`: The `Segment` object.
- `schema`: The `Schema` object.
- `storage`: The `Storage` instance.
- `base`: Base document number offset (usually `True`, meaning compute it).

### `MultiID3Reader`

```python
class whoosh.reading.MultiID3Reader(readers, base)
```

Combines multiple readers that have ID3 codec.

### `TermInfo`

```python
class whoosh.reading.TermInfo(
    df=0,
    weight=0,
    minlength=0,
    maxlen=0,
    maxnum=0,
    numdocs=0,
    scorable=True
)
```

Information about a term in the index.

**Attributes:**
- `df`: Document frequency (number of documents containing the term).
- `weight`: Total term frequency across all documents.
- `minlength`: Minimum document length where the term appears.
- `maxlength`: Maximum document length where the term appears. This is `0`
  if lengths are not stored.
- `maxnum`: Maximum number of occurrences per document.
- `numdocs`: Number of documents where the term has a non-zero contribution
  to the score.
- `scorable`: Whether this term is scorable.

## Term Vector

### `TermVector`

```python
class whoosh.reading.TermVector(docnum, fieldname, format_, terms, store_term_vector)
```

Represents the term vector for a single document/field pair.

**Methods:**

#### `tokens(text=None)`

Yields `(t, w, v, p)` tuples for terms in this field.

- `t`: The term string.
- `w`: The term weight (frequency in this document).
- `v`: The list of positions where the term occurs. (`None` if positions
  are not stored.)
- `p`: The list of characters where the term occurs. (`None` if character
  vectors are not stored.)

#### `items(text=None)`

Like `tokens()` but includes term strings in the result.

```python
tv = my_index.reader().term_vector(0, "content")
for token, frequency, positions, chars in tv.tokens():
    print(token, frequency, positions)
```

**Parameters:**
- `text`: Optional `Bytes` object. If given, only yield terms starting with
  this text (used for multi-byte tokenization).

## Reader Utilities

### `get_storage`

```python
whoosh.reading.get_storage(searcher) -> Storage
```

Returns the storage object associated with the searcher.

### `get_index_schema`

```python
whoosh.reading.get_index_schema(searcher) -> Schema
```

Returns the schema object associated with the searcher.

### `load_termdocs`

```python
whoosh.reading.load_termdocs(reader, fieldname, text) -> list
```

Returns a list of document numbers that have the given term.

### `read_pattern`

```python
whoosh.reading.read_pattern(reader, fieldname, expression) -> list
```

Returns sorted term list from `reader.lexicon(fieldname)` filtered to those
matching `expression`.

### `read_terminfo`

```python
whoosh.reading.read_terminfo(reader, fieldname, text) -> TermInfo or None
```

Returns a `TermInfo` for the given term, or `None` if not found.


## DOCUMENT (FR): Reference

# Référence API

The Whoosh-NG API reference is générée automatiquement à partir du code source avec
[pydoctor](https://pydoctor.readthedocs.io/), qui analyse les modules Python
et génère une documentation HTML à partir des docstrings.

:::note
Si la documentation intégrée ne s'affiche pas, il se peut que les docs API
n'aient pas encore été générées dans ce déploiement. Consultez
[la page GitHub](https://github.com/dorel14/whoosh-ng/tree/master/website/static/api_docs)
pour la documentation API complète, ou reportez-vous à la
[liste des modules API](#api-modules) ci-dessous.
:::

## Modules API

### API Core

| Module | Description |
|--------|-------------|
| `whoosh.index` | Création, ouverture et gestion d'index de haut niveau |
| `whoosh.fields` | Schéma et définitions de types de champs |
| `whoosh.writing` | Classes d'écriture et politiques de fusion |
| `whoosh.searching` | Searcher, Results et collectors |
| `whoosh.query` | Classes de requêtes et parseurs |
| `whoosh.qparser` | Implémentation du parseur de requêtes |
| `whoosh.analysis` | Tokenizers, filtres et analyseurs |
| `whoosh.highlight` | Surlignage des résultats de recherche |
| `whoosh.spelling` | Correction orthographique |
| `whoosh.sorting` | Facettes et tri |
| `whoosh.event_bus` | Système d'événements |
| `whoosh.hooks` | Système de hooks |
| `whoosh.middleware` | Pipeline de middleware |
| `whoosh.plugins` | Système de plugins et registre |
| `whoosh.backends` | Backends de stockage |

### API Modern

| Module | Description |
|--------|-------------|
| `whoosh_modern.data_sources` | Protocole et implémentations de sources de données |
| `whoosh_modern.views` | Interface unifiée SearchView |
| `whoosh_modern.middleware` | Middleware de retry, cache, logging |
| `whoosh_modern.facets` | FacetManager pour l'auto-découverte |
| `whoosh_modern.validation` | Framework de validation à 4 niveaux |
| `whoosh_modern.indexing` | BatchIndexWriter, AnalyzerCache |
| `whoosh_modern.linguistics` | Moteur linguistique (stemmers, synonymes) |
| `whoosh_modern.storage` | Providers de stockage (HybridStorage, etc.) |
| `whoosh_modern.vector` | NumpyProvider pour la similarité vectorielle |
| `whoosh_modern.autocomplete` | Plugins de providers d'autocomplétion |
| `whoosh_fastapi` | Endpoints API REST FastAPI |
| `whoosh_admin` | Tableau de bord d'administration |

:::info
For the full interactive API documentation, run:
```bash
pip install pydoctor
python scripts/generate_api_docs.py
```
Then open `website/static/api_docs/index.html` in your browser.
:::


## DOCUMENT (FR): Searching

# API Recherche

Exécuter des requêtes et récupérer les résultats.

## Searcher

```python
class whoosh.searching.Searcher
```

Interface principale pour lire l'index.

### Méthodes

| Méthode | Description |
|---------|-------------|
| `search(query, limit=10)` | Exécute une requête |
| `search_page(query, pagenum, pagelen=10)` | Récupère une page de résultats |
| `search_with_collector(query, collector)` | Recherche avancée avec collector |
| `find(fieldname, text)` | Recherche dans un champ |
| `documents(**kwargs)` | Documents stockés correspondants |
| `document(**kwargs)` | Un document stocké |
| `lexicon(fieldname)` | Liste des termes d'un champ |
| `all_stored_fields()` | Itère sur tous les champs stockés |

## Results

```python
class whoosh.searching.Results
```

Conteneur de résultats (semblable à une liste).

### Méthodes

| Méthode/attribut | Description |
|------------------|-------------|
| `len(results)` | Nombre total de correspondances |
| `results.scored_length()` | Nombre de résultats scorés |
| `results[0]` | Premier résultat |
| `results[0:10]` | Slice de résultats |
| `results.has_matched_terms()` | Vérifie si les termes matchés sont collectés |
| `results.filtered_count` | Nombre de documents filtrés |
| `results.collapsed_counts` | Comptage par clé d'effondrement |

## Hit

```python
class whoosh.searching.Hit
```

Un document matché.

### Attributs

- `hit["champ"]`: Valeur du champ stocké
- `hit.score`: Score de pertinence
- `hit.docnum`: Numéro interne du document

### Méthodes

| Méthode | Description |
|---------|-------------|
| `hit.highlights("content", top=3)` | Extrait surlignés |
| `hit.matched_terms()` | Termes matchés (si `terms=True`) |

## Highlight

```python
from whoosh.highlight import highlight, Fragment

snippets = hit.highlights("content", top=3)
```

## Collectors

```python
from whoosh.collectors import FacetCollector, TimeLimitCollector
```

## Tri et facettes

```python
from whoosh import sorting

facet = sorting.FieldFacet("categorie")
results = searcher.search(query, sortedby="date")
```


## DOCUMENT (FR): Sorting

> **Note de traduction** : Cette page n'est pas encore traduite en français.
> Le contenu anglais est affiché ci-dessous en attendant la traduction.

<!-- Creez une version francaise de ce fichier et supprimez ce message. -->


# Sorting API

Classes and functions for faceting and sorting search results. The sorting
module is a refactored package exposing the same public API as the former
monolithic module.

## Overview

Sorting and faceting use `FacetType` objects to compute sort keys for documents.
A `FacetType` creates a `Categorizer` that computes a key for each document.
The key is used for sorting and grouping. `FacetMap` objects hold the
results of grouping documents by a facet.

## Facet Types

### `FacetType`

```python
class whoosh.sorting.FacetType
```

Base class for "facets" — aspects that can be sorted and/or faceted.

**Attributes:**
- `maptype`: Default `FacetMap` class to use for this facet.

**Methods:**

#### `categorizer(global_searcher)`

Returns a `Categorizer` corresponding to this facet.

- `global_searcher`: A parent searcher for global document ID references.

#### `map(default=None)`

Returns a `FacetMap` instance for holding facet results.

#### `default_name()`

Returns the default name for this facet (default `"facet"`).

### `Categorizer`

```python
class whoosh.sorting.Categorizer
```

Base class for objects that compute a key value for a document for sorting and
faceting. Created by `FacetType` objects via `categorizer()`.

**Attributes:**
- `allow_overlap (bool)`: If `True`, use `keys_for()` to allow overlapping
  groups. Default `False`.
- `needs_current (bool)`: If `True`, the categorizer needs the matcher to be
  in a valid state when `key_for()` is called. Default `False`.

**Methods:**

#### `set_searcher(segment_searcher, docoffset)`

Called when the collector moves to a new segment. Sets up segment-specific
data.

- `segment_searcher`: The atomic sub-searcher for the current segment.
- `docoffset`: Offset of the segment's docnums relative to the full index.

#### `key_for(matcher, segment_docnum)`

Returns a sort key for the current match.

- `matcher`: A `Matcher` object. If `needs_current` is `False`, do not use
  this object as it may be inconsistent.
- `segment_docnum`: Segment-relative document number.

#### `keys_for(matcher, segment_docnum)`

Yields multiple keys for the current match. Called instead of `key_for()`
when `allow_overlap` is `True`.

#### `key_to_name(key)`

Translates the sort key into a human-readable representation for facet
group names (e.g., converts an integer date sort key to a `datetime`).

### `FieldFacet`

```python
class whoosh.sorting.FieldFacet(
    fieldname,
    reverse=False,
    allow_overlap=False,
    maptype=None
)
```

Sorts/facets by the contents of a field.

**Constructor:**
- `fieldname`: Name of the field to sort/facet on.
- `reverse`: If `True`, reverse the sort order.
- `allow_overlap`: If `True`, allow documents to appear in multiple groups
  when they have multiple terms in the field.
- `maptype`: `FacetMap` class for holding results.

```python
paths = FieldFacet("path", reverse=True)
tags = FieldFacet("tag")
results = searcher.search(myquery, sortedby=paths, groupedby=tags)
```

### `ColumnCategorizer`

Categorizer that reads values from a column for sorting. Used when a field
has a column type.

### `ReversedColumnCategorizer`

Categorizer that reverses column values for fields that are not naturally
reversible.

### `OverlappingCategorizer`

```python
class whoosh.sorting.OverlappingCategorizer
```

Categorizer used when `allow_overlap=True`. A single document can belong to
multiple facet groups.

### `PostingCategorizer`

```python
class whoosh.sorting.PostingCategorizer
```

Categorizer for fields without column values. Builds an array caching the
order of all documents. Used as a fallback; prefer setting
`sortable=True` on fields.

### `QueryFacet`

```python
class whoosh.sorting.QueryFacet(
    querydict: dict,
    other=None,
    allow_overlap=False,
    maptype=None
)
```

Sorts/facets based on the results of a series of queries.

**Constructor:**
- `querydict`: Dictionary mapping keys to `Query` objects.
- `other`: Key to use for documents matching no queries.

### `RangeFacet`

```python
class whoosh.sorting.RangeFacet(
    fieldname,
    start,
    end,
    gap,
    hardend=False,
    maptype=None
)
```

Sorts/facets based on numeric ranges. Ranges are inclusive at the start and
exclusive at the end.

```python
prices = RangeFacet("price", 0, 1000, 100)
results = searcher.search(myquery, groupedby=prices)
```

- `fieldname`: The numeric field to facet on.
- `start`: Start of the entire range.
- `end`: End of the entire range.
- `gap`: Size of each bucket (can be a sequence for progressive gaps).
- `hardend`: If `True`, clamp the last bucket to `end`.

### `DateRangeFacet`

```python
class whoosh.sorting.DateRangeFacet(
    fieldname,
    startdate,
    enddate,
    gap,
    hardend=False,
    maptype=None
)
```

Sorts/facets based on date ranges. Extends `RangeFacet` but uses
`datetime` objects for start/end and `timedelta`/`relativedelta` for gaps.
Generates `DateRange` queries instead of `TermRange` queries.

```python
from datetime import datetime
from whoosh.support.relativedelta import relativedelta

startdate = datetime(1920, 1, 1)
enddate = datetime.now()
gap = relativedelta(years=5)
bdays = DateRangeFacet("birthday", startdate, enddate, gap)
```

### `ScoreFacet`

```python
class whoosh.sorting.ScoreFacet
```

Uses a document's relevance score as a sorting criterion.

```python
tag_score = MultiFacet(["tag", ScoreFacet()])
results = searcher.search(myquery, sortedby=tag_score)
```

### `FunctionFacet`

```python
class whoosh.sorting.FunctionFacet(fn)
```

Lets you pass an arbitrary function that computes the sort key. The function
is called with `(searcher, docid)` where `docid` is an absolute index
document number.

```python
fn = lambda s, docid: s.doc_field_length(docid, "content")
lengths = FunctionFacet(fn)
```

### `TranslateFacet`

```python
class whoosh.sorting.TranslateFacet(fn, *facets)
```

Applies a custom function to the key generated by one or more wrapped facets.
Useful for custom collation, such as Unicode Collation Algorithm (UCA) sorting.

```python
from pyuca import Collator

c = Collator("allkeys.txt")
facet = FieldFacet("name")
facet = TranslateFacet(c.sort_key, facet)
results = searcher.search(myquery, sortedby=facet)
```

**Constructor:**
- `fn`: Function applied to the computed key values.
- `*facets`: One or more `FacetType` objects whose keys are passed to `fn`.

### `StoredFieldFacet`

```python
class whoosh.sorting.StoredFieldFacet(
    fieldname,
    allow_overlap=False,
    split_fn=None,
    maptype=None
)
```

Sorts/groups using the value in an unindexed, stored field (e.g., `STORED`).
Usually slower than using an indexed field.

**Constructor:**
- `fieldname`: Name of the stored field.
- `allow_overlap`: If `True`, when grouping, allow documents to appear in
  multiple groups when they have multiple values (split by `split_fn` or
  `string.split()`).
- `split_fn`: Custom function to split a stored field value into multiple
  facet values (only used when `allow_overlap=True`).

### `MultiFacet`

```python
class whoosh.sorting.MultiFacet(items=None, maptype=None)
```

Sorts/facets by the combination of multiple sub-facets.

```python
facet = MultiFacet([FieldFacet("tag"), FieldFacet("path")])
results = searcher.search(myquery, sortedby=facet)
```

Strings in the items list are treated as field names:

```python
facet = MultiFacet(["tag", "path"])
```

**Methods:**
- `from_sortedby(sortedby)`: Class method that creates a `MultiFacet` from
  a field name, facet, or list thereof.
- `add_field(fieldname, reverse=False)`: Add a `FieldFacet`.
- `add_query(querydict, other=None, allow_overlap=False)`: Add a `QueryFacet`.
- `add_score()`: Add a `ScoreFacet`.
- `add_facet(facet)`: Add an arbitrary `FacetType`.

### `Facets`

```python
class whoosh.sorting.Facets(x=None)
```

Maps facet names to `FacetType` objects for creating multiple independent
groupings of documents.

```python
facets = Facets()
facets.add_field("tag")
facets.add_facet("price", RangeFacet("price", 0, 1000, 100))
results = searcher.search(myquery, groupedby=facets)

tag_groups = results.groups("tag")
price_groups = results.groups("price")
```

**Class Methods:**
- `from_groupedby(groupedby)`: Creates a `Facets` object from a field name,
  `FacetType`, dict, list, or another `Facets` object.

**Methods:**
- `names()`: Returns an iterator of facet names.
- `items()`: Returns a list of `(name, facet)` tuples.
- `add_field(fieldname, **kwargs)`: Adds a `FieldFacet`.
- `add_query(name, querydict, **kwargs)`: Adds a `QueryFacet`.
- `add_facet(name, facet)`: Adds a `FacetType` under the given name.
- `add_facets(facets, replace=True)`: Adds the contents of a `Facets` or
  `dict` to this object.

## Facet Maps

### `FacetMap`

```python
class whoosh.sorting.FacetMap
```

Base class for objects holding the results of grouping search results by a
facet. Use `as_dict()` to access results.

```python
myfacet = FieldFacet("size", maptype=OrderedList)
myfacet = FieldFacet("size", maptype=Count)
```

**Methods:**
- `add(groupname, docid, sortkey)`: Adds a document to the facet results.
- `as_dict()`: Returns a dictionary mapping group names to values.

### `OrderedList`

```python
class whoosh.sorting.OrderedList
```

Stores a list of document numbers for each group, in sorted order.

### `UnorderedList`

```python
class whoosh.sorting.UnorderedList
```

Stores a list of document numbers for each group in arbitrary order. Slightly
faster and more memory-efficient than `OrderedList` when ordering doesn't
matter.

### `Count`

```python
class whoosh.sorting.Count
```

Stores the count of documents in each group.

### `Best`

```python
class whoosh.sorting.Best
```

Stores the "best" (highest sort key) document in each group.

## Sorting Utilities

### `add_sortable`

```python
whoosh.sorting.add_sortable(
    writer,
    fieldname,
    facet,
    column=None
)
```

Adds a per-document value column to an existing field, making it sortable.
Useful for retrofitting fields that were created without `sortable=True`.

**Example:**
```python
from whoosh import index, sorting

ix = index.open_dir("indexdir")
with ix.writer() as w:
    facet = sorting.FieldFacet("price")
    sorting.add_sortable(w, "price", facet)
```

**Parameters:**
- `writer`: An `IndexWriter` object.
- `fieldname`: Name of the field to add sortable values to.
- `facet`: A `FacetType` object to generate per-document values.
- `column`: Optional `ColumnType` to store the values. If omitted, uses the
  field's default column type.


## DOCUMENT (FR): Spelling

> **Note de traduction** : Cette page n'est pas encore traduite en français.
> Le contenu anglais est affiché ci-dessous en attendant la traduction.

<!-- Creez une version francaise de ce fichier et supprimez ce message. -->


# Spelling API

Functions and classes for correcting typos in user queries using edit-distance
(Damerau-Levenshtein) matching against the terms in the index.

## Corrector Objects

### `Corrector`

```python
class whoosh.spelling.Corrector
```

Base class for spelling correction objects. Concrete subclasses implement the
`_suggestions()` method.

**Methods:**

#### `suggest(text, limit=5, maxdist=2, prefix=0)`

Returns a list of suggested corrections for `text`, ranked by edit distance
then by frequency.

- `text`: The text to check. Will **not** be added to suggestions even if it
  appears in the index.
- `limit`: Maximum number of suggestions to return.
- `maxdist`: Maximum edit distance to look at (values > 2 are inefficient).
- `prefix`: Require suggestions to share this length of prefix with `text`.
  Increasing to even `1` dramatically speeds up suggestions.

#### `_suggestions(text, maxdist, prefix)`

Low-level method yielding `(score, suggestion)` tuples. Subclasses must
implement this.

### `ReaderCorrector`

```python
class whoosh.spelling.ReaderCorrector(reader, fieldname, fieldobj)
```

Suggests corrections based on terms in a specific field of an `IndexReader`.

**Ranks suggestions by edit distance, then by highest to lowest frequency.**

**Constructor:**
- `reader`: An `IndexReader` object.
- `fieldname`: The name of the field to get suggestions from.
- `fieldobj`: The `FieldType` for the field.

### `ListCorrector`

```python
class whoosh.spelling.ListCorrector(wordlist)
```

Suggests corrections based on a sorted list of strings.

**Constructor:**
- `wordlist`: A sorted list of words to match against.

### `MultiCorrector`

```python
class whoosh.spelling.MultiCorrector(correctors, op)
```

Merges suggestions from a list of sub-correctors.

**Constructor:**
- `correctors`: List of `Corrector` objects.
- `op`: A function (e.g., `max` or `operator.add`) to combine scores from
  multiple correctors for the same suggestion.

## Query Correction

### `Correction`

```python
class whoosh.spelling.Correction(q, qstring, corr_q, tokens)
```

Represents the corrected version of a user query string.

**Attributes:**
- `query`: The corrected `Query` object.
- `string`: The corrected user query string.
- `original_query`: The original `Query` object.
- `original_string`: The original user query string.
- `tokens`: List of token objects representing corrected words.

**Methods:**

#### `format_string(formatter)`

Highlights corrected words in the original query string using the given
`Formatter`.

```python
from whoosh import highlight

correction = searcher.correct_query(q, qstring)
hf = highlight.HtmlFormatter(classname="change")
html = correction.format_string(hf)
```

- `formatter`: A `Formatter` instance (or class, which will be instantiated).
- Returns: Formatted string, typically with corrections emphasized.

### `QueryCorrector`

```python
class whoosh.spelling.QueryCorrector(fieldname)
```

Base class for objects that correct words in a user query.

**Constructor:**
- `fieldname`: The default field name for corrections.

**Methods:**

#### `correct_query(q, qstring)`

Returns a `Correction` object representing the corrected form of the given
query.

- `q`: The original `Query` tree to be corrected.
- `qstring`: The original user query string (may be `None`).
- Returns: A `Correction` object.

#### `field()`

Returns the field name this corrector operates on.

### `SimpleQueryCorrector`

```python
class whoosh.spelling.SimpleQueryCorrector(
    correctors: dict,
    terms: list,
    aliases=None,
    prefix: int = 0,
    maxdist: int = 2
)
```

A simple query corrector based on a mapping of field names to `Corrector`
objects, and a list of `(fieldname, text)` tuples to correct.

**Constructor:**
- `correctors`: Dictionary mapping field names to `Corrector` objects.
- `terms`: Sequence of `(fieldname, text)` tuples representing terms to be
  corrected.
- `aliases`: Dictionary mapping field names in the query to field names for
  spelling suggestions.
- `prefix`: Suggested replacement words must share this number of initial
  characters. Default `0`.
- `maxdist`: Maximum edit distance for suggestions. Values > 2 may be slow.


## DOCUMENT (FR): Writing

# API Écriture

Écrire, mettre à jour et supprimer des documents via l'interface `IndexWriter`.

## IndexWriter

```python
class whoosh.writing.IndexWriter
```

Classe de base pour l'écriture de documents.

### Contexte manager

```python
with ix.writer() as writer:
    writer.add_document(title="Bonjour", content="Monde")
    # commit() appelé automatiquement
```

### Méthodes

#### `add_document(**fields)`

Ajoute un document.

**Kwargs spéciaux:**
- `_stored_<nom_champ>`: Valeur stockée alternative
- `_<nom_champ>_boost`: Boost spécifique au champ
- `_boost`: Boost global du document

---

#### `update_document(**fields)`

Met à jour/remplace un document. Utilise les champs `unique` pour trouver les documents existants.

---

#### `delete_document(docnum, delete=True)`

Supprime par numéro de document.

---

#### `delete_by_term(fieldname, text) -> int`

Supprime tous les documents avec le terme dans le champ.

**Retourne:**
- `int`: Nombre de documents supprimés.

---

#### `delete_by_query(q, searcher=None) -> int`

Supprime les documents correspondant à la requête.

---

#### `commit(mergetype=None, optimize=False, merge=True)`

Commit les changements sur disque.

**Args:**
- `mergetype`: Fonction de fusion personnalisée
- `optimize`: Fusionner tous les segments en un seul
- `merge`: Si False, ne pas fusionner les segments existants

---

#### `cancel()`

Annule les changements et libère le verrou.

---

#### `add_field(fieldname, fieldtype, **kwargs)`

Ajoute un champ (avant d'ajouter des documents).

---

#### `remove_field(fieldname)`

Supprime un champ du schéma.

---

#### `searcher(**kwargs) -> Searcher`

Retourne un searcher (pour lecture pendant la session d'écriture).

---

#### `reader(**kwargs) -> IndexReader`

Retourne un reader pour l'état actuel.

---

#### `group()`

Context manager pour grouper des documents dans un segment.

## SegmentWriter

Implémentation concrète d'IndexWriter.

## AsyncWriter

Writer threaded qui réessaie automatiquement en cas de contention.

```python
from whoosh.writing import AsyncWriter

writer = AsyncWriter(index, delay=0.25, writerargs={})
```

## BufferedWriter

Buffer les documents en mémoire et commit périodiquement.

```python
from whoosh.writing import BufferedWriter

writer = BufferedWriter(
    index,
    period=60,      # Max secondes entre commits
    limit=100       # Max documents par commit
)
```

## Politiques de fusion

```python
from whoosh.writing import NO_MERGE, MERGE_SMALL, OPTIMIZE, CLEAR

writer.commit(mergetype=NO_MERGE)
writer.commit(mergetype=MERGE_SMALL)
writer.commit(mergetype=OPTIMIZE)
writer.commit(mergetype=CLEAR)
```

## Exceptions

### IndexingError

```python
class whoosh.writing.IndexingError(Exception)
```

Levée quand une opération d'indexation échoue.


## DOCUMENT (FR): Analysis

﻿---
title: "Analyseurs"
sidebar_position: 6
Module: whoosh.analysis
Version: 2.7.4
---

:::info
Suite au renommage de `whoosh-reloaded` en `whoosh-ng`, les nouveaux modules spécifiques à Whoosh-NG se trouvent généralement sous `whoosh_modern`.
Les composants Whoosh core (comme `whoosh.analysis`, `whoosh.index`) restent accessibles directement sous l'espace de noms `whoosh` pour la rétrocompatibilité.
:::

# Analyseurs

## Overview

Un analyseur est une fonction ou une classe callable (une classe avec une méthode `__call__`) qui prend une chaîne unicode et retourne un générateur de tokens. En général, un "token" est un mot, par exemple la chaîne "Mary had a little lamb" peut produire les tokens "Mary", "had", "a", "little", et "lamb". Cependant, les tokens ne sont pas toujours des mots ; cela dépend de l'analyseur utilisé.

## Tokenizers

Un tokenizer est un type d'analyseur qui divise une chaîne de caractères en tokens.

Par exemple, la classe `whoosh.analysis.RegexTokenizer` implémente une expression régulière pour diviser le texte en tokens :

```python
from whoosh.analysis import RegexTokenizer

tokenizer = RegexTokenizer()
for token in tokenizer("Hello there my friend!"):
    print(repr(token.text))
# u'Hello'
# u'there'
# u'my'
# u'friend'
```

## Filtres

Un filtre est un callable qui prend un générateur de Tokens (soit un tokenizer, soit un autre filtre) et retourne à son tour une série de Tokens.

Par exemple, le `whoosh.analysis.LowercaseFilter()` fourni filtre les tokens en convertissant leur texte en minuscules. L'implémentation est très simple :

```python
def LowercaseFilter(tokens):
    """Utilise lower() pour mettre le texte des tokens en minuscules."""
    for t in tokens:
        t.text = t.text.lower()
        yield t
```

Vous pouvez envelopper le filtre autour d'un tokenizer pour le voir en action :

```python
from whoosh.analysis import LowercaseFilter, RegexTokenizer

tokenizer = RegexTokenizer()
for token in LowercaseFilter(tokenizer("These ARE the things I want!")):
    print(repr(token.text))
# u'these'
# u'are'
# u'the'
# u'things'
# u'i'
# u'want'
```

## Analyseurs

Un analyseur est simplement un moyen de combiner un tokenizer et quelques filtres en un seul package.

Vous pouvez implémenter un analyseur comme une classe ou fonction personnalisée, ou composer des tokenizers et filtres en utilisant le caractère `|` :

```python
my_analyzer = RegexTokenizer() | LowercaseFilter() | StopFilter()
```

## Voir aussi

- [API: analysis](../api/analysis) — Référence complète pour les analyseurs, tokenizers et filtres


## DOCUMENT (FR): Backends

# Backends

Whoosh-NG supporte des backends de stockage pluggables via l'architecture Provider. Le backend par défaut stocke les données comme fichiers sur disque, mais vous pouvez utiliser SQLite, PostgreSQL, S3, et plus encore.

## Backends intégrés

| Backend | Description |
|---------|-------------|
| Fichier (défaut) | Stocke l'index comme fichiers sur disque |
| SQLite | Stocke l'index dans une base SQLite |
| Mémoire | Backend en mémoire (tests uniquement) |

## Backend Fichier (défaut)

```python
from whoosh.index import create_in

# Utilise FileBackend par défaut
ix = create_in("indexdir", schema)
```

## Backend SQLite

```python
from whoosh.backends.sqlite import SQLiteBackend
from whoosh.store.sqlite import SQLiteStorage

storage = SQLiteStorage("index.db")
backend = SQLiteBackend(storage=storage)
```

### Avantages

- Index en un seul fichier
- Meilleur pour les charges transactionnelles
- Sauvegardes plus faciles
- Supporte les lectures concurrentes

## Backend Mémoire

```python
from whoosh.backends.memory import MemoryBackend

backend = MemoryBackend()  # Utile pour les tests
```

## Bonnes pratiques

1. **File backend pour production**: Le plus éprouvé
2. **SQLite pour déploiement mono-fichier**: Plus facile à déployer
3. **Mémoire pour les tests**: Rapide, pas de nettoyage nécessaire
4. **Fichiers composés**: Activez pour réduire le nombre de fichiers
5. **Stratégie de sauvegarde**: File = copier le répertoire; SQLite = copier le fichier


## DOCUMENT (FR): Batch

﻿---
title: "Batch Indexing Performance"
sidebar_position: 15
Module: whoosh.index, whoosh.writing
Version: 2.7.4
---
> **Note de traduction** : Cette page n'est pas encore traduite en francais.
> Le contenu anglais est affiche ci-dessous en attendant la traduction.

<!-- Creez une version francaise de ce fichier et supprimez ce message. -->


# Tips for speeding up batch indexing

## Overview

Indexing documents tends to fall into two general patterns: adding documents
one at a time as they are created (as in a web application), and adding a bunch
of documents at once (batch indexing).

The following settings and alternate workflows can make batch indexing faster.

## StemmingAnalyzer cache

The stemming analyzer by default uses a least-recently-used (LRU) cache to
limit the amount of memory it uses, to prevent the cache from growing very
large if the analyzer is reused for a long period of time. However, the LRU
cache can slow down indexing by almost 200% compared to a stemming analyzer
with an "unbounded" cache.

When you're indexing in large batches with a one-shot instance of the analyzer,
consider using an unbounded cache:

> **Note**: For new implementations or complex multilingual scenarios, consider
> using the `CachedStemmingAnalyzer` (from `whoosh_modern.analysis.cached_stemming_analyzer`)
> which offers integrated LRU caching and flexible configuration.

```python
w = myindex.writer()
# Get the analyzer object from a text field
stem_ana = w.schema["content"].analyzer
# Set the cachesize to -1 to indicate unbounded caching
stem_ana.cachesize = -1
# Reset the analyzer to pick up the changed attribute
stem_ana.clear()

# Use the writer to index documents...
```

## The `limitmb` parameter

The `limitmb` parameter to `whoosh.index.Index.writer()` controls the
*maximum* memory (in megabytes) the writer will use for the indexing pool. The
higher the number, the faster indexing will be.

The default value of `128` is actually somewhat low, considering many people
have multiple gigabytes of RAM these days. Setting it higher can speed up
indexing considerably:

```python
from whoosh import index

ix = index.open_dir("indexdir")
writer = ix.writer(limitmb=256)
```

> The actual memory used will be higher than this value because of interpreter
> overhead (up to twice as much!). It is very useful as a tuning parameter, but
> not for trying to exactly control the memory usage of Whoosh.

## The `procs` parameter

The `procs` parameter to `whoosh.index.Index.writer()` controls the number of
processors the writer will use for indexing (via the `multiprocessing` module):

```python
from whoosh import index

ix = index.open_dir("indexdir")
writer = ix.writer(procs=4)
```

When you use multiprocessing, the `limitmb` parameter controls the amount of
memory used by *each process*, so the actual memory used will be
`limitmb * procs`:

```python
# Each process will use a limit of 128, for a total of 512
writer = ix.writer(procs=4, limitmb=128)
```

## The `multisegment` parameter

The `procs` parameter causes the default writer to use multiple processors to
do much of the indexing, but then still uses a single process to merge the pool
of each sub-writer into a single segment.

You can get much better indexing speed by also using the `multisegment=True`
keyword argument, which instead of merging the results of each sub-writer,
simply has them each just write out a new segment:

```python
from whoosh import index

ix = index.open_dir("indexdir")
writer = ix.writer(procs=4, multisegment=True)
```

The drawback is that instead of creating a single new segment, this option
creates a number of new segments **at least** equal to the number of processors
you use. For example, if you use `procs=4`, the writer will create four new
segments.

So, while `multisegment=True` is much faster than a normal writer, you should
only use it for large batch indexing jobs (or perhaps only for indexing from
scratch). It should not be the only method you use for indexing, because
otherwise the number of segments will tend to increase forever!

## See also

- [Indexing](/core/indexing) â€” Writer options and merge policies
- [API: writing](../api/writing) â€” `Index.writer()` parameters


## DOCUMENT (FR): Changelog

# Historique des modifications

Notes de version pour Whoosh-NG, generees automatiquement a partir des releases GitHub et des messages de commits.

## v5.1.0 (2026-08-11)
**Tag**: `v5.1.0`

_This release is published under the BSD-2-Clause License._

### Breaking Changes

- **Changement de chemin d'import** : Le projet a été renommé de `whoosh-reloaded` en
  `whoosh-ng`. Les modules d'extension modernes précédemment disponibles sous
  `whoosh_reloaded` sont maintenant importables sous `whoosh_modern`. Le code
  existant utilisant `whoosh_reloaded` doit être mis à jour vers `whoosh_modern`.
  Les composants Whoosh core restent disponibles sous l'espace de noms `whoosh`.

### Bug Fixes

- **config,linguistics**: Resolve mypy/pyright issues in config loader and yaml provider ([`3f59f96`](https://github.com/dorel14/whoosh-ng/commit/3f59f9607398547225b090a2eddc0fc7e1f62efd))

- **fastapi**: Correct WebSocket autocomplete test assertion ([`b9b1d57`](https://github.com/dorel14/whoosh-ng/commit/b9b1d57524b6f121bff60833c6b5563264bc4152))

- **storage**: Validate SnapshotStorage key before remote read ([`3ec0b43`](https://github.com/dorel14/whoosh-ng/commit/3ec0b43120e05578a4191d25e0d66b10ab06b914))

- **tests**: Import CoreStorageAdapter in test_storage_providers.py ([`18484b0`](https://github.com/dorel14/whoosh-ng/commit/18484b00c2e3f66e1baab76349d28694a3fde45a))

### Documentation

- Add Configuration Engine docs, update CHANGELOG, FastAPI WebSocket, CoreStorageAdapter, and SnapshotStorage fix ([`5e8ec4a`](https://github.com/dorel14/whoosh-ng/commit/5e8ec4ae49aaa64d66c554751f7cd67d9ca37f31))

- Auto-update llms context files ([`916bb34`](https://github.com/dorel14/whoosh-ng/commit/916bb34e16c4b5adf7cf6cc7a1449864db60873a))

- Auto-update llms context files ([`5b6b9a6`](https://github.com/dorel14/whoosh-ng/commit/5b6b9a63346559febe51c16a23bbe1602188a2c2))

- Auto-update llms context files ([`25f4477`](https://github.com/dorel14/whoosh-ng/commit/25f4477091e5506c58bfe6787e2fa7a6fcc22943))

- Auto-update llms context files [skip ci] ([`127bc4a`](https://github.com/dorel14/whoosh-ng/commit/127bc4aa3277c109f73d932e878be90807c4aafc))

- **config**: Clarify list merge behavior, refactor PyYAML import, document pre-push hook rationale ([`3d122d5`](https://github.com/dorel14/whoosh-ng/commit/3d122d58b85d10b85bf079ad9d3d15acdeab09e2))

- **config**: Clarify list merge rationale and make unsupported format error dynamic ([`25076b6`](https://github.com/dorel14/whoosh-ng/commit/25076b61ab91df44cd81327bf827732325fc1aab))

- **fastapi,config**: Clarify optional FastAPI dependency and warn on list merge behavior ([`e917567`](https://github.com/dorel14/whoosh-ng/commit/e917567509d1b1d84bb27b7ceb75b1b8c15941a6))

### Features

- Add PyYAML extra, make WebSocket limit configurable, validate ConfigEngine priority ([`5fb4a38`](https://github.com/dorel14/whoosh-ng/commit/5fb4a382ccda5e6408ec93ce1e6e2efa8fd583d7))

- **config**: Implement Configuration Engine core with Pydantic models, YAML/JSON loader, and hierarchical merging ([`de7bbca`](https://github.com/dorel14/whoosh-ng/commit/de7bbcadd384391adef090f1944865490b93d8fe))

- **fastapi**: Make WebSocket limit configurable and run autocomplete off the event loop ([`c538ea6`](https://github.com/dorel14/whoosh-ng/commit/c538ea6af2195bd292035c5de3e520577f090bc4))

- **storage**: Add CoreStorageAdapter wrapping core FileStorage for SyncStorageProvider ([`52ab7e6`](https://github.com/dorel14/whoosh-ng/commit/52ab7e614de1f92a7ac8d7afbf95bbc52aaba0e2))

- **website**: Ajouter les fichiers de configuration du site statique ([`103fcff`](https://github.com/dorel14/whoosh-ng/commit/103fcff73199a4e8e126f7f887d0f31bcd003999))

---

**Detailed Changes**: [v5.0.0...v5.1.0](https://github.com/dorel14/whoosh-ng/compare/v5.0.0...v5.1.0)

### Commits

### Code Refactoring

- Sprint D cleanup + P1-1/P3 dedup fixes
- improve WebSocket error handling and document list merge behavior

### Features

- add CoreStorageAdapter wrapping core FileStorage for SyncStorageProvider
- implement Configuration Engine core with Pydantic models, YAML/JSON loader, and hierarchical merging
- ajouter les fichiers de configuration du site statique
- add PyYAML extra, make WebSocket limit configurable, validate ConfigEngine priority
- make WebSocket limit configurable and run autocomplete off the event loop

### Bug Fixes

- correct WebSocket autocomplete test assertion
- validate SnapshotStorage key before remote read
- import CoreStorageAdapter in test_storage_providers.py
- resolve mypy/pyright issues in config loader and yaml provider

### Other

- .
- Rename LICENSE.txt to LICENSE_OLD.txt
- Merge branch 'dev' of https://github.com/dorel14/whoosh-ng into dev
- Merge branch 'dev' of https://github.com/dorel14/whoosh-ng into dev
- Merge pull request #418 from dorel14/dev

### Chores

- merge master into dev [skip ci]
- synchronize version from pyproject.toml [skip ci]
- apply pre-commit fixes
- apply pre-commit fixes
- v5.1.0 [skip ci]

### Documentation

- add Configuration Engine docs, update CHANGELOG, FastAPI WebSocket, CoreStorageAdapter, and SnapshotStorage fix
- auto-update llms context files
- auto-update llms context files
- clarify list merge behavior, refactor PyYAML import, document pre-push hook rationale
- auto-update llms context files
- clarify list merge rationale and make unsupported format error dynamic
- clarify optional FastAPI dependency and warn on list merge behavior
- auto-update llms context files [skip ci]

### CI/CD

- regenerate LLM context files only on documentation changes


[View on GitHub](https://github.com/dorel14/whoosh-ng/releases/tag/v5.1.0)

## v5.0.0 (2026-08-11)
**Tag**: `v5.0.0`

## v5.0.0 (2026-08-11)

_This release is published under the BSD-2-Clause License._

### Bug Fixes

- **ci**: Ensure git push runs even when pre-commit commit is a no-op ([`b4fa710`](https://github.com/dorel14/whoosh-ng/commit/b4fa710bfb979a764373b1b9dc67e60f0a84ff0e))

- **ci**: Restore || true suppression for git pull --rebase in test workflow ([`6b8ae46`](https://github.com/dorel14/whoosh-ng/commit/6b8ae46f08908aa132b55261643589993de11ba9))

- **indexing**: Avoid NameError when ix.writer() fails in ParallelIndexBuilder ([`b629fed`](https://github.com/dorel14/whoosh-ng/commit/b629feda7398ece400636ef1ab7bf1df742c4255))

- **linguistics**: Restore constructor-style calls for language analyzers ([`8307731`](https://github.com/dorel14/whoosh-ng/commit/8307731b5958654307f6014743fe1e39700ba547))

- **s3**: Corriger l'ordre de validation des chemins dans SnapshotStorage ([`4fdf384`](https://github.com/dorel14/whoosh-ng/commit/4fdf384158249d66937f16976307db01f232ddf0))

- **storage**: Sanitize S3 keys in SnapshotStorage.read to prevent path traversal ([`b57b00d`](https://github.com/dorel14/whoosh-ng/commit/b57b00d84e2adc8d73e83a93bdc9813eeeff69bf))

### Documentation

- Auto-update llms context files ([`6318ddd`](https://github.com/dorel14/whoosh-ng/commit/6318dddda1db67f1d9ffc1492038340eafd2b62e))

- Auto-update llms context files ([`eb618ab`](https://github.com/dorel14/whoosh-ng/commit/eb618ab65aceb212b417a09d5c2ca74e3296bd0d))

### Features

- **workflows**: Amélioration des workflows CI et ajout de la section LLM Context ([`359e19a`](https://github.com/dorel14/whoosh-ng/commit/359e19a79002a1b67ea8faeef868a0cbe877612b))

---

**Detailed Changes**: [v4.3.0...v5.0.0](https://github.com/dorel14/whoosh-ng/compare/v4.3.0...v5.0.0)

### Commits

### Other

- Merge pull request #15 from dorel14/master
- Merge branch 'master' into dev
- Merge branch 'dev' of https://github.com/dorel14/whoosh-ng into dev
- Merge branch 'master' into feat/last_updates
- Merge pull request #417 from dorel14/feat/last_updates

### Documentation

- auto-update llms context files
- rename sprint-c/d docs, add provider-integration guide, sync website docs
- auto-update llms context files
- auto-update llms context files
- auto-update llms context files
- auto-update llms context files

### Chores

- synchronize version from pyproject.toml [skip ci]
- synchronize version from pyproject.toml [skip ci]
- apply pre-commit fixes
- synchronize version from pyproject.toml [skip ci]
- v5.0.0 [skip ci]

### Code Refactoring

- modernisation architecturale et unification sur les composants core
- améliorations de la sécurité S3 et du typage statique

### CI/CD

- ajout de la détection des doublons de code

### Bug Fixes

- sanitize S3 keys in SnapshotStorage.read to prevent path traversal
- restore constructor-style calls for language analyzers
- corriger l'ordre de validation des chemins dans SnapshotStorage
- ensure git push runs even when pre-commit commit is a no-op
- restore || true suppression for git pull --rebase in test workflow
- avoid NameError when ix.writer() fails in ParallelIndexBuilder

### Features

- amélioration des workflows CI et ajout de la section LLM Context


[View on GitHub](https://github.com/dorel14/whoosh-ng/releases/tag/v5.0.0)

## v4.3.0 (2026-08-09)
**Tag**: `v4.3.0`

## v4.3.0 (2026-08-09)

_This release is published under the BSD-2-Clause License._

### Bug Fixes

- Add actions: read permission to workflow_run triggers ([`d1cb49c`](https://github.com/dorel14/whoosh-ng/commit/d1cb49c7d5d84b4d790090d8b24250bb0bf5791b))

- Normalize line endings to LF in workflow files ([`d68513b`](https://github.com/dorel14/whoosh-ng/commit/d68513b84b97115f1708412e731c6bfcc7a2b6b1))

- Use repository_dispatch instead of workflow_run for Pages trigger ([`c601ccb`](https://github.com/dorel14/whoosh-ng/commit/c601ccbc6469b4048083ab71c334f937422b2b2f))

- **pages**: Ensure Pages deploys on all master pushes + changelog sidebar fix ([`d5d297f`](https://github.com/dorel14/whoosh-ng/commit/d5d297f17ef941e5575f7f0ac7f5c99d0c188f76))

### Features

- **website**: Ajouter des sidebars dédiées et refondre la page de référence API ([`a69d3c9`](https://github.com/dorel14/whoosh-ng/commit/a69d3c9c9093fa78c246010f79d751ad36c8b9b0))

- **website**: Ajouter des sidebars dédiées par section de documentation ([`ffaf1a9`](https://github.com/dorel14/whoosh-ng/commit/ffaf1a969c53c80fa2d968f5f733b40ae9e2cc00))

---

**Detailed Changes**: [v4.2.3...v4.3.0](https://github.com/dorel14/whoosh-ng/compare/v4.2.3...v4.3.0)

### Commits

### Chores

- synchronize version from pyproject.toml [skip ci]
- synchronize version from pyproject.toml [skip ci]
- apply pre-commit fixes
- synchronize version from pyproject.toml [skip ci]
- v4.3.0 [skip ci]

### CI/CD

- restructurer la chaîne de déploiement CI/CD et optimiser les workflows
- ajouter des garde-fous et corriger le script de synchronisation de version
- améliorer la logique de déclenchement du déploiement GitHub Pages
- ajouter un déclenchement push pour les modifications de documentation

### Other

- Merge branch 'master' of https://github.com/dorel14/whoosh-ng

### Features

- ajouter des sidebars dédiées par section de documentation
- ajouter des sidebars dédiées et refondre la page de référence API

### Code Refactoring

- simplifier la configuration des sidebars

### Bug Fixes

- normalize line endings to LF in workflow files
- ensure Pages deploys on all master pushes + changelog sidebar fix
- add actions: read permission to workflow_run triggers
- use repository_dispatch instead of workflow_run for Pages trigger


[View on GitHub](https://github.com/dorel14/whoosh-ng/releases/tag/v4.3.0)

## v4.2.3 (2026-08-08)
**Tag**: `v4.2.3`

## v4.2.3 (2026-08-08)

_This release is published under the BSD-2-Clause License._

### Bug Fixes

- **ci**: Add concurrency groups to prevent workflow race conditions ([`76e38f0`](https://github.com/dorel14/whoosh-ng/commit/76e38f03045d19b8d8032f066432af4e9d17d133))

---

**Detailed Changes**: [v4.2.2...v4.2.3](https://github.com/dorel14/whoosh-ng/compare/v4.2.2...v4.2.3)

### Commits

### Chores

- synchronize version from pyproject.toml [skip ci]
- sync version to 4.2.1 [skip ci]
- v4.2.3 [skip ci]

### Other

- Merge: resolve sync-version conflicts (CI and local generated same changes)

### Bug Fixes

- add concurrency groups to prevent workflow race conditions


[View on GitHub](https://github.com/dorel14/whoosh-ng/releases/tag/v4.2.3)

## v4.2.2 (2026-08-08)
**Tag**: `v4.2.2`

## v4.2.2 (2026-08-08)

_This release is published under the BSD-2-Clause License._

### Bug Fixes

- **ci**: Fix changelog workflow detached HEAD ([`db4b78e`](https://github.com/dorel14/whoosh-ng/commit/db4b78e6dc51a0e10fde48fd93eb2ce5d1dbecd0))

---

**Detailed Changes**: [v4.2.1...v4.2.2](https://github.com/dorel14/whoosh-ng/compare/v4.2.1...v4.2.2)

### Commits

### Chores

- synchronize version from pyproject.toml [skip ci]
- v4.2.2 [skip ci]

### Bug Fixes

- fix changelog workflow detached HEAD


[View on GitHub](https://github.com/dorel14/whoosh-ng/releases/tag/v4.2.2)

## v4.2.1 (2026-08-07)
**Tag**: `v4.2.1`

## v4.2.1 (2026-08-07)

_This release is published under the BSD-2-Clause License._

---

**Detailed Changes**: [v4.2.0...v4.2.1](https://github.com/dorel14/whoosh-ng/compare/v4.2.0...v4.2.1)

### Commits

### Bug Fixes

- fix sync-changelog.yml YAML syntax error

### Other

- Merge branch 'master' of https://github.com/dorel14/whoosh-ng
- i18n(fr): translate nested.md, storage-providers.md, stemming.md, ngrams.md, glossary.md

### Chores

- synchronize version to 4.2.0 [skip ci]
- synchronize version from pyproject.toml [skip ci]
- v4.2.1 [skip ci]


[View on GitHub](https://github.com/dorel14/whoosh-ng/releases/tag/v4.2.1)

## v4.2.0 (2026-08-07)
**Tag**: `v4.2.0`

## v4.2.0 (2026-08-07)

_This release is published under the BSD-2-Clause License._

### Bug Fixes

- **ci**: Fix changelog workflow typo and pages.yml checkout depth ([`d88b82b`](https://github.com/dorel14/whoosh-ng/commit/d88b82b68f7ea2ed28c4530cbda1ecad77308acf))

### Features

- **docs**: Enable dark theme by default and remove edit button ([`2c3d98b`](https://github.com/dorel14/whoosh-ng/commit/2c3d98b27087125073729505cba156d1c2578d66))

---

**Detailed Changes**: [v4.1.0...v4.2.0](https://github.com/dorel14/whoosh-ng/compare/v4.1.0...v4.2.0)

### Commits

### Chores

- synchronize version from pyproject.toml [skip ci]
- v4.2.0 [skip ci]

### Features

- enable dark theme by default and remove edit button

### Bug Fixes

- fix changelog workflow typo and pages.yml checkout depth


[View on GitHub](https://github.com/dorel14/whoosh-ng/releases/tag/v4.2.0)

## v4.1.0 (2026-08-07)
**Tag**: `v4.1.0`

## v4.1.0 (2026-08-07)

_This release is published under the BSD-2-Clause License._

### Bug Fixes

- **ci**: Fix broken workflows and Docusaurus build errors after migration ([`0e86e6c`](https://github.com/dorel14/whoosh-ng/commit/0e86e6c51e3d41fec984f0f79a4457e9d5d0b1ef))

- **ci**: Fix sync_version.py NameError and missing imports ([`f809f56`](https://github.com/dorel14/whoosh-ng/commit/f809f563851b102c7220cec0371c560191bdb217))

### Features

- **docs**: Migrate Jekyll/Just the Docs to Docusaurus v3 ([`7e70791`](https://github.com/dorel14/whoosh-ng/commit/7e70791d23fb0f3097e3603ba0ff3fa5c8d822c2))

---

**Detailed Changes**: [v4.0.1...v4.1.0](https://github.com/dorel14/whoosh-ng/compare/v4.0.1...v4.1.0)

### Commits

### Chores

- synchronize version from pyproject.toml [skip ci]
- v4.1.0 [skip ci]

### Features

- migrate Jekyll/Just the Docs to Docusaurus v3

### Bug Fixes

- fix broken workflows and Docusaurus build errors after migration
- fix sync_version.py NameError and missing imports


[View on GitHub](https://github.com/dorel14/whoosh-ng/releases/tag/v4.1.0)

## v4.0.1 (2026-08-07)
**Tag**: `v4.0.1`

## v4.0.1 (2026-08-07)

_This release is published under the BSD-2-Clause License._

### Bug Fixes

- **pages**: Pointer Bundler sur docs/Gemfile via BUNDLE_GEMFILE ([`0f3a8f1`](https://github.com/dorel14/whoosh-ng/commit/0f3a8f11087c5df910d1762f31ae1cee4017ed31))

---

**Detailed Changes**: [v4.0.0...v4.0.1](https://github.com/dorel14/whoosh-ng/compare/v4.0.0...v4.0.1)

### Commits

### Chores

- synchronize version from pyproject.toml [skip ci]
- v4.0.1 [skip ci]

### Bug Fixes

- pointer Bundler sur docs/Gemfile via BUNDLE_GEMFILE


[View on GitHub](https://github.com/dorel14/whoosh-ng/releases/tag/v4.0.1)

## v4.0.0 (2026-08-07)
**Tag**: `v4.0.0`

## v4.0.0 (2026-08-07)

_This release is published under the BSD-2-Clause License._

### Bug Fixes

- **ci**: Corriger les erreurs de lint, mypy et pyright ([`4283355`](https://github.com/dorel14/whoosh-ng/commit/428335590e641870f6bc399b241b453745e4b9a3))

---

**Detailed Changes**: [v3.0.0...v4.0.0](https://github.com/dorel14/whoosh-ng/compare/v3.0.0...v4.0.0)

### Commits

### Features

- restructurer le système de middleware et étendre PluginManager
- ajouter le module linguistics avec analyseurs multilingues
- add asearch/awriter bridges and AsyncFileStorage
- enhance FastAPI models and Admin Studio modules
- add SearchApplication and FileStorage exports
- add S3Storage, HybridStorage, AsyncHybridStorage
- add SnapshotStorage, CachedObjectStorage alias, and Phase 3 roadmap
- publier la version 3.0.0 et ajouter la documentation LLM

### Documentation

- ajouter les guides Whoosh-NG 2.0 et ajuster la configuration de release
- add Gemfile.lock for reproducible Jekyll build and fix French index permalinks
- add S3 storage benchmarks and documentation
- auto-update llms context files

### Chores

- restructurer les workflows CI/CD et nettoyer le code
- apply pre-commit fixes
- v4.0.0 [skip ci]

### Other

- Merge branch 'master' into dev
- Merge pull request #14 from dorel14/dev

### Bug Fixes

- corriger les erreurs de lint, mypy et pyright


[View on GitHub](https://github.com/dorel14/whoosh-ng/releases/tag/v4.0.0)

## v3.0.0 (2026-08-06)
**Tag**: `v3.0.0`

## v3.0.0 (2026-08-06)

_This release is published under the BSD-2-Clause License._

### Bug Fixes

- Ajouter reportAssignmentType en warning dans pyrightconfig.json ([`3259c76`](https://github.com/dorel14/whoosh-ng/commit/3259c76660a4f99135a8cda1ac26baa3f642a873))

- Corriger docstring PathTokenizer et restaurer Literal pour engine ([`b90222e`](https://github.com/dorel14/whoosh-ng/commit/b90222e5962671dfb20f496d396f546f33c95747))

- Mapper engine pyarrow vers auto pour pandas et exclure tests/ de mypy ([`dadeb91`](https://github.com/dorel14/whoosh-ng/commit/dadeb91cffd9462d0c5c2aff77fd36f4cbac8f4a))

- Ne pas yield de token vide dans RegexTokenizer gaps=True ([`6385a24`](https://github.com/dorel14/whoosh-ng/commit/6385a24cb048777aef9cc5d2bad46f2e5d2e5dfa))

- Remove unreachable dead code after build() return in parallel_builder ([`c2e03c9`](https://github.com/dorel14/whoosh-ng/commit/c2e03c97bda6ac61d78c38c6ac3ad37df4f9345a))

- Rendre test_buffered_threads deterministe en couverture des valeurs ([`77a66f6`](https://github.com/dorel14/whoosh-ng/commit/77a66f6320f9ff7bfe8a1c787c52d1bb0e3cf0d6))

- Resolve mypy errors in CI (parquet, tortoise, hnswlib) ([`514f1f6`](https://github.com/dorel14/whoosh-ng/commit/514f1f62d55edd92633654e0096caf920a9ff3a6))

- Réintégrer assert segment_reader is not None après merge distant ([`c42b065`](https://github.com/dorel14/whoosh-ng/commit/c42b065355bb0004094223748a7c9b6eca810405))

- Résoudre les erreurs pyright dans pre-commit (Token typing, reportAttributeAccessIssue) ([`eff5adf`](https://github.com/dorel14/whoosh-ng/commit/eff5adf55cbbae6169bf2efacc76fddacc1891dd))

- Résoudre les erreurs pyright reportOptionalMemberAccess et reportAssignmentType ([`572529e`](https://github.com/dorel14/whoosh-ng/commit/572529e1fb7f0828f3729ed7920e219a71df139c))

- Supprimer cast redondant et ajouter reportAssignmentType en warning ([`0834923`](https://github.com/dorel14/whoosh-ng/commit/0834923f185a4fb62ded3e5020ea6d36b90bfc42))

- **analysis**: Corriger l'indentation du RegexTokenizer et ajuster les annotations de type ([`69e9ad8`](https://github.com/dorel14/whoosh-ng/commit/69e9ad815192ed9113acea4075e21eaf9e514f8d))

- **deps**: Retirer les modules obsolètes des exclusions mypy ([`f53e157`](https://github.com/dorel14/whoosh-ng/commit/f53e15729647ac7473066f193a33fcbfb2a3dce0))

- **indexing**: Close segment_ix in ParallelIndexBuilder to prevent fd leak ([`bad2928`](https://github.com/dorel14/whoosh-ng/commit/bad2928155d424e19de24e5b00822831695e7449))

- **indexing**: Corriger les fuites de handles et erreurs de nettoyage sous Windows ([`3cb85c7`](https://github.com/dorel14/whoosh-ng/commit/3cb85c7d9f80eccdbb5d2225d270d9f4c15b79bd))

- **indexing**: Merge parallel worker segments into main index ([`6f18248`](https://github.com/dorel14/whoosh-ng/commit/6f182488b18aedaceaf4c886b1b0d87eadc02a99))

- **indexing**: Merge worker segments into main index in ParallelIndexBuilder ([`27b0541`](https://github.com/dorel14/whoosh-ng/commit/27b054140d1a99228a804b75a1d4efd3e4f300ce))

- **mypy**: Restore ignore_missing_imports for pytest, peewee, httpx, re2, psutil ([`699ee4f`](https://github.com/dorel14/whoosh-ng/commit/699ee4f2fb3007012913d5970376f97fc99a0296))

- **profiling**: Add segment_write() and sibling step context managers to CommitProfilerV2 ([`599b650`](https://github.com/dorel14/whoosh-ng/commit/599b6507149332b3e0522b753e547bae718b468e))

- **profiling**: Implement SegmentProfiler to resolve NameError in benchmark.py ([`1b03b3a`](https://github.com/dorel14/whoosh-ng/commit/1b03b3ae7fbc3d03c7223ab4f7759eb500c9c8bf))

### Documentation

- Auto-update llms context files ([`5cca243`](https://github.com/dorel14/whoosh-ng/commit/5cca243cba55d49836e89793af3ac126815b862c))

- Auto-update llms context files ([`55825c1`](https://github.com/dorel14/whoosh-ng/commit/55825c15502f0b15041aec9ac15d7f9c4bc23e6c))

- Auto-update llms context files ([`c0d74a4`](https://github.com/dorel14/whoosh-ng/commit/c0d74a4cda58a44898a0b3f67fdc009fefa565fe))

- Restructurer la documentation et ajouter les pages API et guides ([`4e9b1e2`](https://github.com/dorel14/whoosh-ng/commit/4e9b1e2ce8a494c66f1725cec3b03e5025643e8d))

- **guides**: Ajouter le guide d'indexation moderne et mettre à jour les index de documentation ([`9fbc4ff`](https://github.com/dorel14/whoosh-ng/commit/9fbc4ff41dafc184219f0b4f1da3ee4f3e0ecb71))

### Features

- Ajouter FastCSVSource, indexation par lots, infrastructure de profiling et optimisations du cœur ([`fc63f9c`](https://github.com/dorel14/whoosh-ng/commit/fc63f9c890155fa9ce0b25c03e9db733eb7f0139))

- **analysis**: Ajouter le système de stemmers, FastCSVSource et l'infrastructure de profiling des performances ([`a077319`](https://github.com/dorel14/whoosh-ng/commit/a077319a8fc7435ef283b5c2b5402538c6550819))

- **core**: Ajouter CacheMiddleware et ObservableDataSource ([`508589a`](https://github.com/dorel14/whoosh-ng/commit/508589a8c86c7d1eadeb6ffd044a340fe84363be))

- **data-sources**: Ajouter les sources de données et le pooling de connexions ([`51805d4`](https://github.com/dorel14/whoosh-ng/commit/51805d4c08109b78704f6938deb4c135f57b0674))

- **data-sources**: Améliorer la robustesse et la validation des sources de données ([`35281b5`](https://github.com/dorel14/whoosh-ng/commit/35281b534865111e30d0d1195c492d72b8769e75))

- **profiling**: Ajouter les groupes d'options profiling et fast-stemming, stream_batches et restructurer les chemins d'import des sources de données ([`152e9c4`](https://github.com/dorel14/whoosh-ng/commit/152e9c44b68380a9d238489a29ebfd52e28f97a9))

---

**Detailed Changes**: [v2.0.0...v3.0.0](https://github.com/dorel14/whoosh-ng/compare/v2.0.0...v3.0.0)

### Commits

### Other

- Remove workflows permission from test.yml
- Revise README for version 2.0.0 updates
- Merge pull request #12 from dorel14/master
- Merge branch 'dev' of https://github.com/dorel14/whoosh-ng into dev
- Merge branch 'dev' of https://github.com/dorel14/whoosh-ng into dev
- Potential fix for pull request finding 'CodeQL / Workflow does not contain permissions'
- Merge branch 'dev' of https://github.com/dorel14/whoosh-ng into dev
- Merge branch 'dev' of https://github.com/dorel14/whoosh-ng into dev
- Merge branch 'dev' of https://github.com/dorel14/whoosh-ng into dev
- .
- Update src/whoosh_modern/indexing/parallel_builder.py
- codec/base: restore missing out-of-order term check in add_postings
- Update src/whoosh_modern/profiling/segment_profiler.py
- Merge branch 'dev' of https://github.com/dorel14/whoosh-ng into dev
- Merge branch 'dev' of https://github.com/dorel14/whoosh-ng into dev
- Merge c42b065355bb0004094223748a7c9b6eca810405 into 9988770df03a2d255a21b74843772776dbbf998a
- Merge pull request #13 from dorel14/dev
- Update commit_parser_options in pyproject.toml
- Add files via upload

### Build System

- automatiser la synchronisation de la version entre les fichiers du projet

### Chores

- apply pre-commit fixes
- simplifier la configuration type-checking et retirer les dépendances inutilisées
- apply pre-commit fixes
- apply pre-commit fixes
- trigger pre-commit workflow
- v3.0.0 [skip ci]

### Features

- ajouter CacheMiddleware et ObservableDataSource
- ajouter les sources de données et le pooling de connexions
- ajouter FastCSVSource, indexation par lots, infrastructure de profiling et optimisations du cœur
- ajouter le système de stemmers, FastCSVSource et l'infrastructure de profiling des performances
- ajouter les groupes d'options profiling et fast-stemming, stream_batches et restructurer les chemins d'import des sources de données
- améliorer la robustesse et la validation des sources de données

### Code Refactoring

- simplifier les expressions multi-lignes et optimiser Token avec __slots__
- nettoyer les annotations de type et supprimer les dépendances inutilisées

### Documentation

- auto-update llms context files
- restructurer la documentation et ajouter les pages API et guides
- auto-update llms context files
- ajouter le guide d'indexation moderne et mettre à jour les index de documentation
- auto-update llms context files

### Bug Fixes

- corriger l'indentation du RegexTokenizer et ajuster les annotations de type
- resolve mypy errors in CI (parquet, tortoise, hnswlib)
- ne pas yield de token vide dans RegexTokenizer gaps=True
- résoudre les erreurs pyright dans pre-commit (Token typing, reportAttributeAccessIssue)
- supprimer cast redondant et ajouter reportAssignmentType en warning
- ajouter reportAssignmentType en warning dans pyrightconfig.json
- corriger docstring PathTokenizer et restaurer Literal pour engine
- mapper engine pyarrow vers auto pour pandas et exclure tests/ de mypy
- remove unreachable dead code after build() return in parallel_builder
- add segment_write() and sibling step context managers to CommitProfilerV2
- implement SegmentProfiler to resolve NameError in benchmark.py
- merge parallel worker segments into main index
- merge worker segments into main index in ParallelIndexBuilder
- rendre test_buffered_threads deterministe en couverture des valeurs
- retirer les modules obsolètes des exclusions mypy
- close segment_ix in ParallelIndexBuilder to prevent fd leak
- résoudre les erreurs pyright reportOptionalMemberAccess et reportAssignmentType
- réintégrer assert segment_reader is not None après merge distant
- restore ignore_missing_imports for pytest, peewee, httpx, re2, psutil
- corriger les fuites de handles et erreurs de nettoyage sous Windows


[View on GitHub](https://github.com/dorel14/whoosh-ng/releases/tag/v3.0.0)

## v2.0.0 (2026-07-31)
**Tag**: `v2.0.0`

## v2.0.0 (2026-07-31)

_This release is published under the BSD-2-Clause License._

### Bug Fixes

- Address review findings in CI/CD workflows and documentation ([`b1e0b89`](https://github.com/dorel14/whoosh-ng/commit/b1e0b89f10ad182ad0120d6d19bcac2f8a04470a))

### Documentation

- Auto-update llms context files ([`10e6c90`](https://github.com/dorel14/whoosh-ng/commit/10e6c90fb6e7ffd9ec9f1b3fa7f4e398a933077b))

- Auto-update llms context files ([`992e4d7`](https://github.com/dorel14/whoosh-ng/commit/992e4d780fd7829bd804d68b215e06f255e9cacf))

- Auto-update llms context files ([`5dfe9dc`](https://github.com/dorel14/whoosh-ng/commit/5dfe9dcfdd725d1ca12f3bba0c09a2f7d163aa88))

- Auto-update llms context files ([`aadce99`](https://github.com/dorel14/whoosh-ng/commit/aadce99460c30cd5950e44c927c07eb6fa5ffde8))

### Features

- **deps**: Add sqlalchemy and sqlmodel to models extra and configure mypy overrides ([`4486b5f`](https://github.com/dorel14/whoosh-ng/commit/4486b5feef2700fd8720a0dec1419985f9479951))

- **models**: Ajouter AutoIndexer et améliorer la génération de schémas ([`919131c`](https://github.com/dorel14/whoosh-ng/commit/919131ce7f475b8584e74d98d9cf41e04891b7c7))

- **models**: ✨ Introduce ModelIndex and SearchField for auto-mapping ([`6c202bc`](https://github.com/dorel14/whoosh-ng/commit/6c202bc6c4e20ad9dba7e4cd81deadfdaf3dcf2a))

- **whoosh_modern**: Add modern API with data sources, schema discovery, facets, validation, middleware, and SearchView ([`36bfbae`](https://github.com/dorel14/whoosh-ng/commit/36bfbaebd6acc18cda06aba83bebade7154c9972))

---

**Detailed Changes**: [v1.3.3...v2.0.0](https://github.com/dorel14/whoosh-ng/compare/v1.3.3...v2.0.0)

### Commits

### Documentation

- merge duplicate FastAPI example into fastapi-search.md
- auto-update llms context files
- auto-update llms context files
- auto-update llms context files
- auto-update llms context files

### Other

- Merge branch 'master' of https://github.com/dorel14/whoosh-ng
- ..
- Merge pull request #10 from dorel14/master
- Merge branch 'dev' of https://github.com/dorel14/whoosh-ng into dev
- Merge branch 'dev' of https://github.com/dorel14/whoosh-ng into dev
- Merge branch 'dev' of https://github.com/dorel14/whoosh-ng into dev
- style(benchmark): ajouter un saut de ligne final manquant dans reuters_modern.py
- Merge branch 'dev' of https://github.com/dorel14/whoosh-ng into dev
- Potential fix for pull request finding 'CodeQL / Workflow does not contain permissions'
- Potential fix for pull request finding 'CodeQL / Workflow does not contain permissions'
- Potential fix for pull request finding 'CodeQL / Workflow does not contain permissions'
- .
- .
- .
- Merge pull request #11 from dorel14/dev

### Features

- ✨ Introduce ModelIndex and SearchField for auto-mapping
- ajouter AutoIndexer et améliorer la génération de schémas
- add modern API with data sources, schema discovery, facets, validation, middleware, and SearchView
- add sqlalchemy and sqlmodel to models extra and configure mypy overrides

### Bug Fixes

- address review findings in CI/CD workflows and documentation

### Code Refactoring

- moderniser les annotations de type avec la syntaxe union PEP 604
- moderniser les annotations de type avec Coroutine et ajouter des ignores pyright
- ajouter des annotations de retour aux méthodes replace des matchers
- moderniser la vérification isinstance avec la syntaxe union PEP 604

### CI/CD

- ajouter des extras d'installation et simplifier la couverture

### Chores

- v2.0.0 [skip ci]


[View on GitHub](https://github.com/dorel14/whoosh-ng/releases/tag/v2.0.0)

## v1.3.3 (2026-07-26)
**Tag**: `v1.3.3`

## v1.3.3 (2026-07-26)

_This release is published under the BSD-2-Clause License._

---

**Detailed Changes**: [v1.3.2...v1.3.3](https://github.com/dorel14/whoosh-ng/compare/v1.3.2...v1.3.3)

### Commits

### Bug Fixes

- reorganize nav_order for coherent navigation (Guides 1-90, API 100-190, Examples 200-270)

### Other

- Merge branch 'master' of https://github.com/dorel14/whoosh-ng

### Chores

- v1.3.3 [skip ci]


[View on GitHub](https://github.com/dorel14/whoosh-ng/releases/tag/v1.3.3)

## v1.3.2 (2026-07-26)
**Tag**: `v1.3.2`

## v1.3.2 (2026-07-26)

_This release is published under the BSD-2-Clause License._

---

**Detailed Changes**: [v1.3.1...v1.3.2](https://github.com/dorel14/whoosh-ng/compare/v1.3.1...v1.3.2)

### Commits

### Bug Fixes

- remove color_scheme from individual pages, use global config

### Other

- Merge branch 'master' of https://github.com/dorel14/whoosh-ng

### Chores

- v1.3.2 [skip ci]


[View on GitHub](https://github.com/dorel14/whoosh-ng/releases/tag/v1.3.2)

## v1.3.1 (2026-07-26)
**Tag**: `v1.3.1`

## v1.3.1 (2026-07-26)

_This release is published under the BSD-2-Clause License._

---

**Detailed Changes**: [v1.3.0...v1.3.1](https://github.com/dorel14/whoosh-ng/compare/v1.3.0...v1.3.1)

### Commits

### Bug Fixes

- align _config.yml with taskiq-flow and clean README front matter

### Other

- Merge branch 'master' of https://github.com/dorel14/whoosh-ng

### Chores

- v1.3.1 [skip ci]


[View on GitHub](https://github.com/dorel14/whoosh-ng/releases/tag/v1.3.1)

## v1.3.0 (2026-07-26)
**Tag**: `v1.3.0`

## v1.3.0 (2026-07-26)

_This release is published under the BSD-2-Clause License._

---

**Detailed Changes**: [v1.2.4...v1.3.0](https://github.com/dorel14/whoosh-ng/compare/v1.2.4...v1.3.0)

### Commits

### Features

- éviter les exécutions inutiles du workflow lors des commits de release

### Other

- Merge branch 'master' of https://github.com/dorel14/whoosh-ng

### Chores

- v1.3.0 [skip ci]


[View on GitHub](https://github.com/dorel14/whoosh-ng/releases/tag/v1.3.0)

## v1.2.4 (2026-07-26)
**Tag**: `v1.2.4`

## v1.2.4 (2026-07-26)

_This release is published under the BSD-2-Clause License._

---

**Detailed Changes**: [v1.2.3...v1.2.4](https://github.com/dorel14/whoosh-ng/compare/v1.2.3...v1.2.4)

### Commits

### Bug Fixes

- remove invalid parent fields and align config with taskiq-flow

### Other

- Merge branch 'master' of https://github.com/dorel14/whoosh-ng

### Chores

- v1.2.4


[View on GitHub](https://github.com/dorel14/whoosh-ng/releases/tag/v1.2.4)

## v1.2.3 (2026-07-26)
**Tag**: `v1.2.3`

## v1.2.3 (2026-07-26)

_This release is published under the BSD-2-Clause License._

---

**Detailed Changes**: [v1.2.2...v1.2.3](https://github.com/dorel14/whoosh-ng/compare/v1.2.2...v1.2.3)

### Commits

### Bug Fixes

- remove invalid parent fields from all pages

### Other

- Merge branch 'master' of https://github.com/dorel14/whoosh-ng

### Chores

- v1.2.3


[View on GitHub](https://github.com/dorel14/whoosh-ng/releases/tag/v1.2.3)

## v1.2.2 (2026-07-26)
**Tag**: `v1.2.2`

## v1.2.2 (2026-07-26)

_This release is published under the BSD-2-Clause License._

### Bug Fixes

- **docs**: Align _config.yml with taskiq-flow pattern ([`2124be0`](https://github.com/dorel14/whoosh-ng/commit/2124be0cf1fd4669c9ebdaa4ad485eabae22c279))

---

**Detailed Changes**: [v1.2.1...v1.2.2](https://github.com/dorel14/whoosh-ng/compare/v1.2.1...v1.2.2)

### Commits

### Code Refactoring

- supprimer la navigation statique codée en dur

### Other

- Merge branch 'master' of https://github.com/dorel14/whoosh-ng

### Bug Fixes

- align _config.yml with taskiq-flow pattern

### Chores

- v1.2.2


[View on GitHub](https://github.com/dorel14/whoosh-ng/releases/tag/v1.2.2)

## v1.2.1 (2026-07-26)
**Tag**: `v1.2.1`

## v1.2.1 (2026-07-26)

_This release is published under the BSD-2-Clause License._

### Bug Fixes

- **docs**: Restore front matter and add explicit nav config ([`74dc151`](https://github.com/dorel14/whoosh-ng/commit/74dc151104a7a89ffd1459a60e0e28488bf110ff))

### Documentation

- Fix Jekyll links with relative_url and clean deploy workflow ([`2f9afc2`](https://github.com/dorel14/whoosh-ng/commit/2f9afc20f5e41022117a636b0d223f92c660df3d))

---

**Detailed Changes**: [v1.2.0...v1.2.1](https://github.com/dorel14/whoosh-ng/compare/v1.2.0...v1.2.1)

### Commits

### Documentation

- fix Jekyll links with relative_url and clean deploy workflow

### Bug Fixes

- restore front matter and add explicit nav config

### Chores

- v1.2.1


[View on GitHub](https://github.com/dorel14/whoosh-ng/releases/tag/v1.2.1)

## v1.2.0 (2026-07-26)
**Tag**: `v1.2.0`

## v1.2.0 (2026-07-26)

_This release is published under the BSD-2-Clause License._

### Documentation

- Ajouter la documentation complète bilingue et le déploiement GitHub Pages ([`a091ced`](https://github.com/dorel14/whoosh-ng/commit/a091cede82bb5d722e5db0ff233c1dcbcecd35ea))

- Auto-update llms context files ([`96e4f2c`](https://github.com/dorel14/whoosh-ng/commit/96e4f2c251ccdde5e9fd7e9cb0ce6b2650ab64d9))

- Auto-update llms context files ([`6abefe4`](https://github.com/dorel14/whoosh-ng/commit/6abefe4b84d99e29c1d6ccb471dd7bf27ea4f31d))

### Features

- **docs**: Ajouter le support multilingue dans la configuration ([`967ed12`](https://github.com/dorel14/whoosh-ng/commit/967ed125e5fcd167e237e1c32b7feeb3ab6f9c4f))

---

**Detailed Changes**: [v1.1.0...v1.2.0](https://github.com/dorel14/whoosh-ng/compare/v1.1.0...v1.2.0)

### Commits

### CI/CD

- restructurer le workflow de release sémantique

### Other

- Merge pull request #7 from dorel14/dev
- revert: supprimer la documentation complète bilingue et restaurer l'état précédent
- Merge branch 'dev' of https://github.com/dorel14/whoosh-ng into dev
- Merge pull request #8 from dorel14/dev
- Merge branch 'dev' of https://github.com/dorel14/whoosh-ng into dev
- Merge pull request #9 from dorel14/dev

### Documentation

- ajouter la documentation complète bilingue et le déploiement GitHub Pages
- auto-update llms context files
- auto-update llms context files

### Features

- ajouter le support multilingue dans la configuration

### Chores

- v1.2.0


[View on GitHub](https://github.com/dorel14/whoosh-ng/releases/tag/v1.2.0)

## v1.1.0 (2026-07-26)
**Tag**: `v1.1.0`

## v1.1.0 (2026-07-26)

_This release is published under the BSD-2-Clause License._

### Bug Fixes

- **bench**: 🐛 add type ignores for method overrides in `XappyModule` ([`822e957`](https://github.com/dorel14/whoosh-ng/commit/822e957c986f603e9fd1c2de2799fb60d3c122a9))

- **schema**: 🐛 corriger l'initialisation de l'objet dans `__new__` ([`5a764a7`](https://github.com/dorel14/whoosh-ng/commit/5a764a78751a6a2bb94118340784da074fdcf2c8))

- **stress**: 🐛 ensure correct handling of string encoding in `test_bigtable` ([`822e957`](https://github.com/dorel14/whoosh-ng/commit/822e957c986f603e9fd1c2de2799fb60d3c122a9))

### Build System

- **deps**: Supprimer les dépendances obsolètes de la section models ([`227a59f`](https://github.com/dorel14/whoosh-ng/commit/227a59fc195799aca02289c2cb5f3d60135ea063))

### Features

- **benchmark**: Refonte du système de benchmarks avec nouvelles spécifications ([`417efd1`](https://github.com/dorel14/whoosh-ng/commit/417efd1be034fe8be4377ad2716e7b15028ad929))

- **matching**: ✨ add type hints for `supports_block_quality` methods ([`822e957`](https://github.com/dorel14/whoosh-ng/commit/822e957c986f603e9fd1c2de2799fb60d3c122a9))

- **support**: ✨ add compatibility for Python 3 unicode handling ([`822e957`](https://github.com/dorel14/whoosh-ng/commit/822e957c986f603e9fd1c2de2799fb60d3c122a9))

- **writing**: ✨ Implement segment writing and merging policies ([`ae5fc62`](https://github.com/dorel14/whoosh-ng/commit/ae5fc62d2185ac349b56cbbf006bbb7fd4f92c80))

---

**Detailed Changes**: [v1.0.0...v1.1.0](https://github.com/dorel14/whoosh-ng/compare/v1.0.0...v1.1.0)

### Commits

### Chores

- ✏️ Mise à jour de la version dans le README
- prepare whoosh-ng 1.0.0
- bump version to 1.0.1
- 🔄 Update project dependencies
- apply pre-commit fixes
- apply pre-commit fixes
- v1.1.0

### Other

- Potential fix for code scanning alert no. 1: Workflow does not contain permissions
- Potential fix for code scanning alert no. 1: Workflow does not contain permissions
- Merge branch 'dev' of https://github.com/dorel14/whoosh-ng into dev
- Merge pull request #5 from dorel14/alert-autofix-1
- Merge branch 'master' into dev
- Potential fix for pull request finding 'CodeQL / Workflow does not contain permissions'
- Merge branch 'dev' of https://github.com/dorel14/whoosh-ng into dev
- Merge 3d0187143ec5a2a4e19f62d26a622d2c53efc2c7 into 68e67aaefb0d48f136d53576e00f10d68f77f15a
- Merge branch 'dev' of https://github.com/dorel14/whoosh-ng into dev
- Merge 4473223d510f0cde2d30ca638d284f23dc467b14 into 68e67aaefb0d48f136d53576e00f10d68f77f15a
- Merge branch 'dev' of https://github.com/dorel14/whoosh-ng into dev
- Merge pull request #6 from dorel14/dev

### Features

- ✨ Ajout de la validation de l'intégrité des segments
- ✨ Ajout d'un wrapper Asyncio pour la gestion asynchrone des écritures
- ✨ Isolation des espaces de stockage temporaires pour les écrivains concurrents
- ✨ Ajout de la normalisation des boosts pour les sous-requêtes
- ✨ Ajout d'un module de reporting pour les résultats de benchmark
- ✨ Ajout d'un backend LMDB et d'un support d'autocomplétion
- ✨ Implement segment writing and merging policies
- refonte du système de benchmarks avec nouvelles spécifications
- ✨ add type hints for `supports_block_quality` methods

### Bug Fixes

- 🐛 Ajout d'un type d'ignore pour l'appel de la requête
- 🐛 Amélioration des benchmarks avec un échauffement et ajustement des seuils d'alerte
- 🐛 corriger l'initialisation de l'objet dans `__new__`

### Code Refactoring

- improve _posting_size estimation, fix benchmark CLI, update mypy target to 3.12

### Build System

- supprimer les dépendances obsolètes de la section models


[View on GitHub](https://github.com/dorel14/whoosh-ng/releases/tag/v1.1.0)

## v1.0.0 (2026-07-12)
**Tag**: `v1.0.0`

## v1.0.0 (2026-07-12)

_This release is published under the BSD-2-Clause License._

- Initial Release


[View on GitHub](https://github.com/dorel14/whoosh-ng/releases/tag/v1.0.0)


## DOCUMENT (FR): Core Concepts

# Concepts fondamentaux

Whoosh-NG est une bibliothèque de recherche purement Python. Ce guide explique les principaux concepts pour l'utiliser efficacement.

## Architecture

Whoosh-NG suit une architecture en couches :

```text
Application
    ▼
┌─────────────────────────────┐
│       Whoosh-NG Core        │
├─────────────────────────────┤
│ Schema                      │
│ Search Engine               │
│ Plugin Manager              │
│ Registry System             │
│ Middleware Pipeline         │
│ Event Bus                   │
│ Hook System                 │
└─────────────────────────────┘
       ▼
┌─────────────────────────────┐
│           Plugins           │
├─────────────────────────────┤
│ FastAPI                     │
│ Autocomplete                │
│ Vector Search               │
│ PostgreSQL                  │
│ S3                          │
│ Monitoring                  │
│ Admin UI                    │
└─────────────────────────────┘
```

## Composants clés

### Index

Un `Index` est le conteneur de vos documents. Il gère un ou plusieurs segments sur disque.

```python
from whoosh.index import create_in, open_dir

ix = create_in("indexdir", schema)
ix = open_dir("indexdir")
```

### Schema

Le `Schema` définit les champs des documents. Chaque champ a un type qui détermine son indexation et stockage.

```python
from whoosh.fields import Schema, TEXT, ID, NUMERIC

schema = Schema(
    title=TEXT(stored=True),
    path=ID(stored=True, unique=True),
    content=TEXT,
    rating=NUMERIC(float, stored=True)
)
```

### Writer

Un `IndexWriter` permet d'ajouter, modifier et supprimer des documents.

```python
writer = ix.writer()
writer.add_document(title="Bonjour", content="Monde")
writer.commit()
```

### Searcher

Un `Searcher` interroge l'index et retourne des résultats.

```python
with ix.searcher() as s:
    results = s.search("bonjour")
```

### QueryParser

Convertit une chaîne de requête en objet Query.

```python
from whoosh.qparser import QueryParser

qp = QueryParser("content", schema)
query = qp.parse("bonjour monde")
```

## Fonctionnalités modernes

### Système de plugins

Les plugins étendent Whoosh-NG sans modifier le core. Ils peuvent :

- Enregistrer de nouveaux providers vectoriels
- Ajouter des endpoints FastAPI
- Fournir des analyseurs personnalisés
- S'intégrer au pipeline de middleware

```python
from whoosh.plugins.manager import PluginManager

# Auto-découverte depuis les entry points
PluginManager.load_plugins()
```

### Pipeline de middleware

Le middleware intercepte les opérations d'indexation et de recherche :

```python
from whoosh.middleware import Middleware, MiddlewareContext

class LoggingMiddleware(Middleware):
    def before_search(self, context: MiddlewareContext):
        print(f"Recherche: {context.query}")
        return context

    def after_search(self, context: MiddlewareContext):
        print(f"Trouvé: {len(context.results) if context.results else 0} résultats")
        return context
```

### Recherche vectorielle

Permet la recherche sémantique via des embeddings :

```python
from whoosh.fields import Schema, TEXT, VectorField

schema = Schema(
    content=TEXT,
    embedding=VectorField(dimensions=384)
)
```

### Event Bus

Système d'événements pour un couplage lâche :

```python
from whoosh.event_bus import EventBus, DocumentIndexed

bus = EventBus()

@bus.subscribe
def on_document_indexed(event: DocumentIndexed):
    print(f"Document indexé: {event.docnum}")
```

## Principes de conception

1. **Composabilité**: Les composants se combinent via les opérateurs `|` et `+`
2. **Abstractions sans coût**: Pas de middleware = pas de surcoût
3. **Sync-first**: Le core est synchrone; async est optionnel
4. **Isolation des plugins**: Les plugins ne peuvent pas casser le core
5. **Sécurité des types**: Typage complet avec annotations


## DOCUMENT (FR): Dates

> **Note de traduction** : Cette page n'est pas encore traduite en français.
> Le contenu anglais est affiché ci-dessous en attendant la traduction.

<!-- Creez une version francaise de ce fichier et supprimez ce message. -->


# Dates and Numeric Ranges

This guide covers working with `DATETIME` and `NUMERIC` fields, including
range queries, range faceting, and date math.

## DATETIME Fields

`DATETIME` fields store Python `datetime` objects and can be queried with
range queries.

```python
from datetime import datetime
from whoosh import fields, index

schema = fields.Schema(
    title=fields.TEXT(stored=True),
    published_date=fields.DATETIME(stored=True, sortable=True),
)
```

### Indexing Dates

```python
ix = index.create_in("indexdir", schema)
with ix.writer() as w:
    w.add_document(
        title="Article 1",
        published_date=datetime(2024, 6, 15, 14, 30),
    )
```

### Date Range Queries

Use `Range` or `QueryParser` syntax:

```python
from whoosh.qparser import QueryParser
from whoosh.query import Range, Every

# Using QueryParser syntax
qp = QueryParser("published_date", schema=ix.schema)
q = qp.parse("[2024-01-01 TO 2024-12-31]")

# Using Range query directly
from datetime import datetime
q = Range(
    "published_date",
    datetime(2024, 1, 1),
    datetime(2024, 12, 31),
)

with ix.searcher() as searcher:
    results = searcher.search(q)
```

### Sorting by Date

```python
from whoosh.sorting import FieldFacet

# Sort by date, most recent first
results = searcher.search(
    query,
    sortedby=FieldFacet("published_date", reverse=True),
)
```

## NUMERIC Fields

`NUMERIC` fields store integers and floating-point numbers.

```python
schema = fields.Schema(
    title=fields.TEXT(stored=True),
    price=fields.NUMERIC(int, stored=True, sortable=True),
    rating=fields.NUMERIC(float, stored=True),
)
```

### Numeric Range Queries

```python
from whoosh.query import NumericRange

q = NumericRange("price", 100, 500)

# Or with QueryParser
qp = QueryParser("price", schema=ix.schema)
q = qp.parse("[100 TO 500]")
```

### Numeric Faceting

Group results into numeric ranges using `RangeFacet`:

```python
from whoosh.sorting import RangeFacet

price_ranges = RangeFacet("price", 0, 1000, 100)
results = searcher.search(query, groupedby=price_ranges)

for groupname, docnums in results.groups("price").items():
    print(f"Price ${groupname}: {len(docnums)} results")
```

## Date Faceting

Group results by date intervals using `DateRangeFacet`:

```python
from datetime import datetime
from whoosh.sorting import DateRangeFacet

start = datetime(2020, 1, 1)
end = datetime(2026, 1, 1)
date_facet = DateRangeFacet(
    "published_date",
    start,
    end,
    relativedelta(years=1),  # Requires: from dateutil.relativedelta import relativedelta
)
results = searcher.search(query, groupedby=date_facet)

for year_range, docnums in results.groups("published_date").items():
    print(f"Year {year_range}: {len(docnums)} results")
```

## Sorting and Filtering by Numbers

### Sorting

```python
from whoosh.sorting import FieldFacet

# Sort by price ascending
results = searcher.search(query, sortedby=FieldFacet("price"))
```

### Filtering

```python
from whoosh.query import NumericRange

# Only results with price >= 50 and price < 200
filter_q = NumericRange("price", 50, 200)
results = searcher.search(query, filter=filter_q)
```

## Making Date/Numeric Fields Sortable

When defining a schema, set `sortable=True` on `NUMERIC` or `DATETIME` fields
to enable sorting by that field:

```python
schema = fields.Schema(
    title=fields.TEXT(stored=True),
    price=fields.NUMERIC(int, sortable=True),
    date=fields.DATETIME(sortable=True),
)
```

If you forgot to set `sortable=True`, you can add it after indexing:

```python
from whoosh import index, sorting

ix = index.open_dir("indexdir")
with ix.writer() as w:
    sorting.add_sortable(w, "price", sorting.FieldFacet("price"))
```


## DOCUMENT (FR): Fieldcaches

﻿---
title: "Field Caches"
sidebar_position: 16
Module: whoosh.filedb.fieldcache
Version: 2.7.4
---
> **Note de traduction** : Cette page n'est pas encore traduite en francais.
> Le contenu anglais est affiche ci-dessous en attendant la traduction.

<!-- Creez une version francaise de ce fichier et supprimez ce message. -->


# Field caches

The default (`filedb`) backend uses *field caches* in certain circumstances.
The field cache basically pre-computes the order of documents in the index to
speed up sorting and faceting.

Generating field caches can take time the first time you sort/facet on a large
index. The field cache is kept in memory (and by default written to disk when
it is generated) so subsequent sorted/faceted searches should be faster.

The default caching policy never expires field caches, so reused searchers
and/or sorting a lot of different fields could use up quite a bit of memory
with large indexes.

## Customizing cache behaviour

(By default, Whoosh saves field caches to disk. To prevent a reader or
searcher from writing out field caches, do this before you start using it:)

```python
searcher.set_caching_policy(save=False)
```

By default, if caches are written to disk they are saved in the index
directory. To tell a reader or searcher to save cache files to a different
location, create a storage object and pass it to the `storage` keyword
argument:

```python
from whoosh.filedb.filestore import FileStorage

mystorage = FileStorage("path/to/cachedir")
reader.set_caching_policy(storage=mystorage)
```

## Creating a custom caching policy

Expert users who want to implement a custom caching policy (for example, to add
cache expiration) should subclass `whoosh.filedb.fieldcache.FieldCachingPolicy`.
Then you can pass an instance of your policy object to the `set_caching_policy`
method:

```python
searcher.set_caching_policy(MyPolicy())
```

## See also

- [Sorting](/core/sorting) â€” Facets and sort keys
- [API: sorting](../api/sorting) â€” Sorting and faceting reference


## DOCUMENT (FR): Glossary

# Glossaire

Un glossaire des termes clés utilisés dans Whoosh.

## Analyse

Le processus de conversion du texte en jetons (unités individuelles comme
les mots ou les termes) pour l'indexation. Implique la tokenisation, la
normalisation (mise en minuscules, racinement) et le filtrage (suppression
des mots vides, etc.).

## Analyseur

Une chaîne d'objets `Tokenizer` et `Filter` qui traite le texte en jetons.
Exemples : `RegexTokenizer`, `NgramTokenizer`, `LowercaseFilter`,
`StopFilter`, `StemmerFilter`.

## Fichier composé

Un format de fichier qui combine plusieurs fichiers de segment d'index
en un seul fichier `.seg`. Cela peut améliorer les performances sur
certains systèmes de fichiers en réduisant l'utilisation des descripteurs
de fichiers. Configuré via le paramètre `should_assemble` du codec.

## Document

Un enregistrement unique dans l'index, similaire à une ligne dans une base
de données. Un document contient des champs (analogues aux colonnes).

## Champ

Un attribut nommé d'un document. Les champs ont un type (défini par
`FieldType`) qui détermine comment la valeur du champ est indexée et
stockée.

## Type de champ

La classe (ex. : `TEXT`, `ID`, `NUMERIC`, `DATETIME`, `BOOLEAN`) qui
définit comment la valeur d'un champ est tokenisée, stockée, indexée, et
rendue triable/facetable.

## Filtre

Un composant d'`Analyzer` qui traite, transforme ou filtre les jetons
après la tokenisation. Exemples : `LowercaseFilter`, `StopFilter`,
`StemmerFilter`.

## Format

Un objet `Format` contrôle comment les informations de posting (fréquence
du terme, positions, décalages de caractères) sont encodées pour chaque
champ dans l'index inversé.
Exemples : `Existence`, `Frequency`, `Positions`, `Characters`.

## Fragmentation

Le processus de sélection des fragments de texte autour des termes
correspondants pour la mise en évidence.

## Mise en évidence (Highlighter)

Le module `whoosh.highlight`, qui fournit des formateurs, fragmenteurs
et évaluateurs pour mettre en évidence les termes de recherche dans les
documents.

## Index

La collection de fichiers de segment qui stockent l'index inversé, les
données de documents et les métadonnées (la table des matières, ou TOC).

## IndexWriter

La classe `IndexWriter` est utilisée pour créer et modifier l'index. Elle
met en mémoire tampon les ajouts et suppressions de documents et les
valide sur le disque.

## Index inversé (Inverted Index)

La structure de données centrale d'un moteur de recherche : pour chaque
terme unique, il stocke une liste de documents (et de positions) où ce
terme apparaît.

## Correspondance (Matcher)

Un objet qui itère sur les documents correspondants dans la liste de
postings pour une requête. Les correspondances peuvent être combinées
(union, intersection, etc.) pour des requêtes composées.

## Posting

Une entrée unique dans l'index inversé : un tuple
(ID de document, fréquence du terme, valeur) pour un terme donné.

## Schéma (Schema)

Définit les champs, leurs types et les options d'indexation. Un schéma
est passé à `Storage.create_index()`.

## Évaluateur (Scorer)

Un objet qui calcule un score de pertinence pour un document donné une
requête et des poids de termes. Les différents modèles de pondération
(BM25, TF-IDF, etc.) utilisent des évaluateurs différents.

## Segment

Une portion autonome de l'index inversé. Un index peut consiste en
plusieurs segments. Les segments sont fusionnés périodiquement (lors de
l'optimisation ou des opérations de fusion) pour améliorer les
performances.

## Clé de tri (Sort Key)

Une valeur calculée par document (via un `FacetType` et son
`Categorizer`) utilisée pour ordonner les résultats lors du tri et de la
facettisation.

## Racinement (Stemming)

Le processus de réduction des mots à leur forme racine (par ex.,
"running" → "run", "cats" → "cat") pour améliorer le rappel en
correspondant les formes inflectées.

## Mots vides (Stop Words)

Des mots à haute fréquence et faible information (ex. : "the", "a",
"and") qui sont généralement filtrés lors de l'indexation.

## Terme

Un couple unique (nom de champ, texte du jeton) dans l'index inversé.

## Vecteur de termes (Term Vector)

Structure de données optionnelle par document stockant les termes (et
optionnellement les positions et décalages de caractères) qui apparaissent
dans le champ d'un document, permettant des fonctionnalités comme la mise
en évidence et les retours de pertinence pseudo.

## Tokeniseur (Tokenizer)

Un composant d'`Analyzer` qui divise le texte d'entrée en jetons.
Exemples : `RegexTokenizer`, `PathTokenizer`, `NgramTokenizer`.

## Requête Whoosh

La syntaxe de requête propre à Whoosh, analysée par `QueryParser`.
Prend en charge la recherche sur champs, les requêtes de phrase, les
jokers, les intervalles et plus encore.


## DOCUMENT (FR): Highlight

﻿---
title: "Highlighting"
sidebar_position: 11
Module: whoosh.highlight
Version: 2.7.4
---
> **Note de traduction** : Cette page n'est pas encore traduite en francais.
> Le contenu anglais est affiche ci-dessous en attendant la traduction.

<!-- Creez une version francaise de ce fichier et supprimez ce message. -->


# Highlighting search result excerpts

## Overview

The highlighting system works as a pipeline, with four component types.

- **Fragmenters** chop up the original text into *fragments*, based on the
  locations of matched terms in the text.
- **Scorers** assign a score to each fragment, allowing the system to rank the
  best fragments by whatever criterion.
- **Order functions** control in what order the top-scoring fragments are
  presented to the user. For example, you can show the fragments in the order
  they appear in the document (`FIRST`) or show higher-scoring fragments first
  (`SCORE`).
- **Formatters** turn the fragment objects into human-readable output, such as
  an HTML string.

## Requirements

Highlighting requires that you have the text of the indexed document available.
You can keep the text in a stored field, or if the original text is available in
a file, database column, etc, just reload it on the fly. Note that you might
need to process the text to remove e.g. HTML tags, wiki markup, etc.

## How to

Get search results and use the `highlights()` method on the
`whoosh.searching.Hit` object to get highlighted snippets:

```python
results = mysearcher.search(myquery)
for hit in results:
    print(hit["title"])
    # Assume "content" field is stored
    print(hit.highlights("content"))
```

If the field is not stored, you need to retrieve the text of the field some
other way, then supply it with the `text` argument:

```python
results = mysearcher.search(myquery)
for hit in results:
    print(hit["title"])
    # Assume the "path" stored field contains a path to the original file
    with open(hit["path"]) as fileobj:
        filecontents = fileobj.read()
    print(hit.highlights("content", text=filecontents))
```

## The character limit

By default, Whoosh only pulls fragments from the first 32K characters of the
text. This prevents very long texts from bogging down the highlighting process
too much. You can change the character limit on the results object:

```python
results = mysearcher.search(myquery)
results.fragmenter.charlimit = 100000
```

To turn off the character limit:

```python
results.fragmenter.charlimit = None
```

If you instantiate a custom fragmenter, you can set the character limit directly:

```python
sf = highlight.SentenceFragmenter(charlimit=100000)
results.fragmenter = sf
```

## Customizing the highlights

### Number of fragments

Use the `top` keyword argument to control the number of fragments returned:

```python
# Show a maximum of 5 fragments from the document
print(hit.highlights("content", top=5))
```

### Fragment size

The default fragmenter has a `maxchars` attribute (default 200) controlling the
maximum length of a fragment, and a `surround` attribute (default 20)
controlling the maximum number of characters of context to add at the beginning
and end of a fragment:

```python
# Allow larger fragments
results.fragmenter.maxchars = 300
# Show more context before and after
results.fragmenter.surround = 50
```

### Fragmenter

A fragmenter controls how to extract excerpts from the original text. The
`highlight` module has the following pre-made fragmenters:

- `whoosh.highlight.ContextFragmenter` (the default) â€” a "smart" fragmenter
  that finds matched terms and pulls in surround text. Only yields fragments
  that contain matched terms.
- `whoosh.highlight.SentenceFragmenter` â€” tries to break the text into
  fragments based on sentence punctuation.
- `whoosh.highlight.WholeFragmenter` â€” returns the entire text as one
  "fragment". Useful for short bits of text.

```python
my_cf = highlight.ContextFragmenter(maxchars=100, surround=30)
results.fragmenter = my_cf
```

### Scorer

A scorer is a callable that takes a `whoosh.highlight.Fragment` object and
returns a sortable value (where higher values represent better fragments). The
default scorer adds up the number of matched terms in the fragment, and adds a
"bonus" for the number of *different* matched terms.

```python
def StandardDeviationScorer(fragment):
    """Gives higher scores to fragments where the matched terms are close together."""
    return 0 - stddev([t.pos for t in fragment.matched])

results.scorer = StandardDeviationScorer
```

### Order

The order is a function that takes a fragment and returns a sortable value used
to sort the highest-scoring fragments before presenting them to the user.

- `FIRST` (the default) â€” show fragments in document order.
- `SCORE` â€” show highest scoring fragments first.
- `LONGER` / `SHORTER` â€” longer/shorter fragments first (less generally useful).

```python
results.order = highlight.SCORE
```

### Formatter

A formatter controls how the highest scoring fragments are turned into a
formatted bit of text. The `highlight` module contains:

- `whoosh.highlight.HtmlFormatter` â€” outputs HTML with a class attribute around
  matched terms.
- `whoosh.highlight.UppercaseFormatter` â€” converts matched terms to UPPERCASE.

The easiest way to create a custom formatter is to subclass `highlight.Formatter`
and override `format_token`:

```python
class BracketFormatter(highlight.Formatter):
    """Puts square brackets around the matched terms."""

    def format_token(self, text, token, replace=False):
        tokentext = highlight.get_text(text, token, replace)
        return "[%s]" % tokentext

brf = BracketFormatter()
results.formatter = brf
```

## Highlighter object

Rather than setting attributes on the results object, you can create a reusable
`whoosh.highlight.Highlighter` object:

```python
hi = highlight.Highlighter(fragmenter=my_cf, scorer=sds)
for hit in results:
    print(hit["title"])
    print(hi.highlight_hit(hit))
```

## Speeding up highlighting

Recording which terms matched in which documents during the search may make
highlighting faster:

```python
# Record per-document term matches
results = searcher.search(myquery, terms=True)
```

### PinpointFragmenter

Instead of re-tokenizing the document text, Whoosh can look up the character
positions of the matched terms in the index. To use
`whoosh.highlight.PinpointFragmenter` and avoid re-tokenizing:

1. Index the field with character information (requires re-indexing):

   ```python
   schema = fields.Schema(content=fields.TEXT(stored=True, chars=True))
   ```

2. Record per-document term matches:

   ```python
   results = searcher.search(myquery, terms=True)
   ```

3. Set the `PinpointFragmenter` as the fragmenter:

   ```python
   results.fragmenter = highlight.PinpointFragmenter()
   ```

Use the `autotrim` option to strip whitespace before the first space and after
the last space in the fragments:

```python
results.fragmenter = highlight.PinpointFragmenter(autotrim=True)
```

## Using the low-level API

```python
from whoosh.highlight import highlight

excerpts = highlight(
    text, terms, analyzer, fragmenter, formatter, top=3,
    scorer=BasicFragmentScorer, minscore=1, order=FIRST,
)
```

| Argument | Description |
|----------|-------------|
| `text` | The original text of the document. |
| `terms` | A sequence or set containing the query words to match. |
| `analyzer` | The analyzer to use to break the document text into tokens. |
| `fragmenter` | A `Fragmenter` object. |
| `formatter` | A `Formatter` object. |
| `top` | The number of fragments to include in the output. |
| `scorer` | A `FragmentScorer` object. |
| `minscore` | The minimum score a fragment must have to be included. |
| `order` | An ordering function for the "top" fragments. |

## See also

- [Searching](/core/searching) â€” The `search()` method and `Hit` objects
- [API: highlight](../api/highlight) â€” Full `whoosh.highlight` reference


## DOCUMENT (FR): Indexing

:::info
Suite au renommage de `whoosh-reloaded` en `whoosh-ng`, les nouveaux modules spécifiques à Whoosh-NG se trouvent généralement sous `whoosh_modern`.
Les composants Whoosh core (comme `whoosh.analysis`, `whoosh.index`) restent accessibles directement sous l'espace de noms `whoosh` pour la rétrocompatibilité.
:::

# Indexation

Guide pour ajouter, mettre à jour et supprimer des documents.

## Ouvrir un writer

```python
from whoosh import index

ix = index.open_dir("indexdir")

# Writer basique
writer = ix.writer()

# Writer avec options
writer = ix.writer(
    timeout=10.0,
    delay=0.1,
    limitmb=128,
    compound=True
)
```

## Ajouter des documents

```python
with ix.writer() as writer:
    writer.add_document(
        title="Premier document",
        content="Bonjour le monde",
        path="/doc1",
        tags=["python", "recherche"]
    )
    writer.commit()
```

## Mettre à jour

```python
with ix.writer() as writer:
    writer.update_document(
        path="/doc1",
        content="Contenu mis à jour"
    )
```

## Supprimer

```python
# Par numéro de document
writer.delete_document(docnum=42)

# Par terme
writer.delete_by_term("path", "/doc1")

# Par requête
from whoosh.query import Term
q = Term("tags", "deprecated")
writer.delete_by_query(q)

writer.commit()
```

## Bonnes pratiques

- Utilisez `with ix.writer() as writer:` pour le nettoyage automatique
- Commutez par lots pour de meilleures performances
- Utilisez `BufferedWriter` en environnement multi-processus
- Libérez toujours le verrou avec `commit()` ou `cancel()`

## Valeurs stockées vs indexées

Pour les champs qui sont à la fois indexés et stockés, vous pouvez stocker une valeur différente :

```python
writer.add_document(
    title="Title to be indexed",
    _stored_title="Display title to show in results"
)
```

> **Note** : Le préfixe underscore (`_stored_<field>`, `_<field>_boost`) est une
> convention Whoosh pour les overrides par document. Il vous permet de stocker
> une valeur différente pour l'affichage (`_stored_title`) sans modifier ce qui
> est indexé, ou de booster un champ spécifique pour un seul document
> (`_title_boost`) sans affecter le boost au niveau du schéma.

## Boosts de champs

Booster des champs individuels au niveau du document :

```python
writer.add_document(
    title="Important title",
    _title_boost=2.0,   # Double weight for title terms
    content="Body content"
)
```

## Mettre à jour les documents

Utilisez `update_document` pour remplacer les documents correspondant à des champs uniques :

```python
schema = Schema(path=ID(unique=True, stored=True), content=TEXT)
ix = index.create_in("indexdir", schema)

with ix.writer() as writer:
    writer.add_document(path="/doc1", content="Original content")
    writer.commit()

with ix.writer() as writer:
    # Remplace tout document avec path="/doc1"
    writer.update_document(path="/doc1", content="Updated content")
    writer.commit()
```

## Supprimer des documents

```python
# Par numéro de document
writer.delete_document(docnum=42)

# Par terme
writer.delete_by_term("path", "/doc1")

# Par requête
from whoosh.query import Term
q = Term("tags", "deprecated")
writer.delete_by_query(q)

writer.commit()
```


## DOCUMENT (FR): Installation

# Installation

## Prérequis

- Python 3.10+
- Aucune dépendance obligatoire (pur Python)
- Extras optionnels pour les fonctionnalités avancées

## pip install

```bash
pip install whoosh-ng
```

## Extras

| Extra | Description |
|-------|-------------|
| `vector` | Providers de recherche vectorielle (NumPy, HNSW, Faiss) |
| `autocomplete` | Plugin d'autocomplétion |
| `api` | Plugin FastAPI |
| `metrics` | Intégration Prometheus |
| `all` | Installer tout |

```bash
pip install whoosh-ng[all]
```

## Installation pour le développement

```bash
git clone https://github.com/your-org/whoosh-NG.git
cd whoosh-NG
uv sync --extra dev
```

## Vérification

```bash
uv run pytest tests/ -q
uv run ruff check src/ tests/
uv run ruff format --check .
uv run mypy src/whoosh
```

## Prochaines étapes

- [Démarrage rapide](/core/quickstart)
- [Concepts fondamentaux](/core/core-concepts)


## DOCUMENT (FR): Intro

﻿---
title: "Introduction to Whoosh"
sidebar_position: 2
Module: whoosh
Version: 2.7.4
---
> **Note de traduction** : Cette page n'est pas encore traduite en francais.
> Le contenu anglais est affiche ci-dessous en attendant la traduction.

<!-- Creez une version francaise de ce fichier et supprimez ce message. -->


# Introduction to Whoosh

## About Whoosh

Whoosh was created by Matt Chaput. It started as a quick and dirty search
server for the online documentation of the Houdini 3D animation software
package. Side Effects Software generously allowed Matt to open source the code
in case it might be useful to anyone else who needs a very flexible or
pure-Python search engine (or both!).

- Whoosh is fast, but uses only pure Python, so it will run anywhere Python
  runs, without requiring a compiler.
- By default, Whoosh uses the [Okapi BM25F](https://en.wikipedia.org/wiki/Okapi_BM25)
  ranking function, but like most things the ranking function can be easily
  customized.
- Whoosh creates fairly small indexes compared to many other search libraries.
- All indexed text in Whoosh must be **unicode**.
- Whoosh lets you store arbitrary Python objects with indexed documents.

## What is Whoosh?

Whoosh is a fast, pure Python search engine library.

The primary design impetus of Whoosh is that it is pure Python. You should be
able to use Whoosh anywhere you can use Python, no compiler or Java required.

Like one of its ancestors, Lucene, Whoosh is not really a search engine, it's a
programmer library for creating a search engine.

Practically no important behavior of Whoosh is hard-coded. Indexing of text, the
level of information stored for each term in each field, parsing of search
queries, the types of queries allowed, scoring algorithms, etc. are all
customizable, replaceable, and extensible.

## What can Whoosh do for you?

Whoosh lets you index free-form or structured text and then quickly find
matching documents based on simple or complex search criteria.

## Whoosh-NG

Whoosh-NG is the maintained evolution of Whoosh. It preserves the pure-Python
core described above while adding optional, opt-in extensions (vector search,
a plugin system, a middleware pipeline, linguistics, and pluggable storage).
Classic features documented in this section remain backwards-compatible with
Whoosh 1.x/2.x.

## Getting help with Whoosh

You can view outstanding issues on the
[Whoosh-NG GitHub page](https://github.com/dorel14/whoosh-ng) and get help by
opening an issue or discussion there.


## DOCUMENT (FR): Keywords

﻿---
title: "Query Expansion & Keywords"
sidebar_position: 13
Module: whoosh.classify, whoosh.searching
Version: 2.7.4
---
> **Note de traduction** : Cette page n'est pas encore traduite en francais.
> Le contenu anglais est affiche ci-dessous en attendant la traduction.

<!-- Creez une version francaise de ce fichier et supprimez ce message. -->


# Query expansion and keyword extraction

## Overview

Whoosh provides methods for computing the "key terms" of a set of documents.
For these methods, "key terms" basically means terms that are frequent in the
given documents, but relatively infrequent in the indexed collection as a whole.

Because this is a purely statistical operation, not a natural language
processing or AI function, the quality of the results will vary based on the
content, the size of the document collection, and the number of documents for
which you extract keywords.

These methods can be useful for providing the following features to users:

- **Search term expansion.** Extract key terms for the top N results from a
  query and suggest them to the user as additional/alternate query terms.
- **Tag suggestion.** Extracting the key terms for a single document may yield
  useful suggestions for tagging the document.
- **"More like this".** Extract key terms for the top ten or so results from a
  query (and removing the original query terms), and use those key words as the
  basis for another query that may find more documents using terms the user
  didn't think of.

## Usage

### More like this

Get more documents like a certain search hit. *This requires that the field you
want to match on is vectored or stored, or that you have access to the original
text.*

```python
results = mysearcher.search(myquery)
first_hit = results[0]
more_results = first_hit.more_like_this("content")
```

### Key terms from top N results

*This requires that the field is either vectored or stored.*

```python
# Extract five key terms from the "content" field of the top ten documents
keywords = [keyword for keyword, score
            in results.key_terms("content", docs=10, numterms=5)]
```

### Key terms from an arbitrary set of documents

*This requires that the field is either vectored or stored.*

```python
with email_index.searcher() as s:
    docnums = s.document_numbers(emailto="matt@whoosh.ca")
    keywords = [keyword for keyword, score
                in s.key_terms(docnums, "body")]
```

### Key terms from arbitrary text not in the index

```python
with email_index.searcher() as s:
    keywords = [keyword for keyword, score
                in s.key_terms_from_text("body", mytext)]
```

## Expansion models

The `ExpansionModel` subclasses in the `whoosh.classify` module implement
different weighting functions for key words. These models are translated into
Python from original Java implementations in Terrier.

```python
from whoosh.classify import Bo1Model

results = mysearcher.search(myquery)
keywords = results.key_terms("content", docs=10, numterms=5, model=Bo1Model)
```

Available models include `Bo1Model`, `Bo2Model`, and `KLModel`.

## See also

- [Searching](/core/searching) â€” `Results`, `Hit`, and the `search()` method
- [API: searching](../api/searching) â€” `key_terms`, `more_like_this` reference


## DOCUMENT (FR): Legacy Cleanup

# Stratégie de nettoyage du code legacy

Ce guide explique comment Whoosh-NG sépare le code moderne typé du code legacy,
et comment le nettoyage progressif est organisé.

## Pourquoi une frontière legacy ?

`whoosh-modern` est la nouvelle surface de Whoosh-NG, entièrement typée.
Le package `whoosh` original fonctionne toujours au runtime, mais il contient
des décennies de motifs de compatibilité Python 2/3, de métaprogrammation
dynamique et d'internes non typés. Forcer des types stricts sur l'ensemble
d'un coup bloquerait le développement.

La stratégie de nettoyage est **incrémentale et opt-in** :

1. `src/whoosh_modern/` est typé et vérifié avec `pyright` et `mypy` en mode strict.
2. `src/whoosh/` est la surface legacy. Elle est divisée en :
   - **modules exclus** (documentés dans `pyrightconfig.json`) — code trop
     dynamique ou vendu pour justifier un passage de types rentable maintenant ;
   - **candidats au nettoyage** — petits fichiers isolés, faciles à annoter et
     à vérifier.
3. Chaque sprint, une vague de candidats est typée, testée, puis sortie de la
   zone de tolérance élevée.

## Seuils actuels (Sprint 2)

| Vérificateur | Portée | Seuil |
|--------------|--------|-------|
| `pyright` | `src/whoosh_modern/` | **0 erreur** (strict) |
| `pyright` | legacy | **≤ 500 erreurs** (tolérant) |
| `mypy` | `src/` | **0 erreur** (via overrides + `ignore_errors`) |

## Justification des exclusions (`pyrightconfig.json`)

La liste `exclude` de `pyrightconfig.json` regroupe les fichiers par thème :

- **Vendu / sans stubs** : `pyparsing.py`, `relativedelta.py`
- **Shims de migration** : `codec/whoosh2.py`, `codec/whoosh3.py`
- **Parsing dynamique** : `qparser/`, `query/`, `analysis/`, `automata/`
- **Stockage fichiers** : `filedb/`, `reading/`, `writing/`
- **Heuristique / data-driven** : `lang/dmetaphone.py`, `lang/lovins.py`,
  `lang/phonetic.py`, `lang/wordnet.py`
- **Objets dynamiques** : `classify.py`, `index.py`, `locking.py`,
  `formats.py`, `middleware/`
- **Bas niveau vendu** : `support/bench.py`, `support/base85.py`,
  `support/bitstream.py`, `support/bitvector.py`, `support/charset.py`,
  `support/levenshtein.py`

## Plan Sprint 2

Pour le Sprint 2, l'accent est mis sur les petits modules utilitaires et de
support, sans dépendances externes ni métaprogrammation lourde.

Vague de candidats :

- `src/whoosh/util/varints.py`
- `src/whoosh/util/text.py`
- `src/whoosh/util/loading.py`
- `src/whoosh/support/bitstream.py`
- `src/whoosh/support/levenshtein.py`

Pour chaque fichier :

1. Supprimer le `# type: ignore` global (si présent).
2. Ajouter des signatures de fonctions précises.
3. Lancer `pyright` et `mypy` pour confirmer **0 nouvelle erreur**.
4. Retirer le fichier des exclusions de `pyrightconfig.json`.
5. Ajouter un test de régression dans `tests/test_legacy_cleanup.py`.

## Objectif long terme

Chaque fichier de `src/whoosh/` doit finir par être vérifiable par `mypy` et
`pyright` sans exclusion globale. D'ici là, la liste d'exclusion est le
registre explicite de la dette, et chaque sprint la réduit.


## DOCUMENT (FR): Migration

# Guide de migration

Ce guide vous aide à migrer depuis Whoosh legacy ou Whoosh-Reloaded 3.x vers Whoosh-NG v3.0.0.
> **Prochaine version** : v4.0.0.dev0 (en développement) ajoute `SchemaBuilder`, la hiérarchie d'exceptions middleware, et plus encore — voir le [CHANGELOG](https://github.com/dorel14/whoosh-ng/blob/master/CHANGELOG.md).

## Depuis Whoosh 1.x/2.x (Legacy)

### Chemins d'import

| Legacy | Whoosh-NG |
|--------|-----------|
| `import whoosh` | `import whoosh` |
| `from whoosh.index import create_in` | `from whoosh.index import create_in` |
| `from whoosh.fields import Schema, TEXT` | `from whoosh.fields import Schema, TEXT` |

L'API core est intentionnellement stable. La plupart du code existant fonctionne sans modification.

## Depuis Whoosh-Reloaded 3.x

Aucun changement cassant. Whoosh-NG est une continuation de Whoosh-Reloaded.

### Migration optionnelle des plugins

```python
# Ancien
from whoosh_modern.vector.numpy_provider import NumpyProvider

# Nouveau (via registre)
from whoosh.vector import NumpyProvider
from whoosh.registry import VectorRegistry

VectorRegistry.register("numpy", NumpyProvider(), "mon_app")
```

### SchemaBuilder (nouveau dans v4.0.0.dev0)

```python
# Ancien
schema = Schema(title=TEXT(stored=True), content=TEXT)

# Nouveau (API fluent)
from whoosh.fields import SchemaBuilder

schema = (
    SchemaBuilder()
    .field("title", TEXT(stored=True))
    .field("content", TEXT)
    .build()
)
```

## Méthode de migration middleware (nouveau dans v4.0.0.dev0)

```python
from whoosh.middleware import Middleware, MiddlewareContext

class LoggingMiddleware(Middleware):
    def before_search(self, context: MiddlewareContext):
        print(f"Query: {context.query}")
        return context

# Envelopper le writer/searcher existant
writer = apply_middleware_to_writer(ix.writer(), [LoggingMiddleware()])
```

## Liste de vérification

1. **Mettre à jour les dépendances**:
   ```bash
   pip install --upgrade whoosh-ng
   ```

2. **Exécuter les tests**:
   ```bash
   uv run pytest tests/ -q
   ```

3. **Mettre à jour les dépendances optionnelles** (si plugins utilisés):
   ```bash
   pip install whoosh-ng[all]
   ```

4. **Revoir le middleware**: Envisagez d'ajouter du middleware pour les préoccupations transverses

## Dépréciations

| Fonctionnalité | Statut | Remplacement |
|----------------|--------|--------------|
| `whoosh_modern.vector` | Déprécié | `whoosh.vector` |
| `whoosh.store` brut | Déprécié | `whoosh.backends` |
| Utilisation directe de `SegmentWriter` | Découragé | Utilisez `IndexWriter` |

## Compatibilité

Whoosh-NG maintient la compatibilité ascendante. Si vous trouvez un changement cassant, signalez-le comme une issue.


## DOCUMENT (FR): Nested

# Documents imbriqués (Nested)

Ce guide couvre l'indexation et la recherche de structures de documents
hiérarchiques imbriqués (par exemple, un document parent contenant plusieurs
documents enfants) en utilisant les fonctionnalités de relation
parent-enfant de Whoosh.

## Définition des documents imbriqués

Vous pouvez indexer des documents parents qui contiennent des documents
enfants en utilisant un champ parent et des champs enfants :

```python
from whoosh import fields, index

schema = fields.Schema(
    type=fields.ID(sortable=True),
    title=fields.TEXT(stored=True),
    content=fields.TEXT,
    section_name=fields.ID,
    section_content=fields.TEXT,
)
```

Le champ `type` distingue les documents parents des documents enfants.

## Indexation des documents imbriqués

Utilisez `IndexWriter.add_all()` avec un générateur qui produit les
documents parents et enfants groupés ensemble :

```python
writer = ix.writer()
writer.add_all([
    parent_doc,
    child_doc_1,
    child_doc_2,
    parent_doc_2,
    child_doc_3,
])
```

Les documents parents ont `type="parent"` et les documents enfants ont
`type="child"`.

## Recherche dans les documents imbriqués

### Recherche-enfants, correspondance-parents

Recherchez dans les documents enfants et faites correspondre leurs
documents parents :

```python
from whoosh.query import Every, Term
from whoosh.sorting import NestedParent

# Correspondre tous les documents parents
parents = NestedParent(Term("type", "parent"))
q = Every("section_content", "hello")
results = searcher.search(q, sortedby=parents)
```

### Recherche-parents, correspondance-enfants

Recherchez les documents parents dont les enfants correspondent :

```python
from whoosh.sorting import NestedChildren

# Correspondre les documents parents qui ont des enfants correspondant à la requête
parent_results = searcher.search(child_query, groupedby=NestedChildren(parent_matcher, child_matcher))
```

## Relations parent-enfant lors de l'indexation

Lors de l'écriture des documents, utilisez le paramètre `parent` pour
lier les enfants aux parents :

```python
writer.add_document(type="parent", title="Chapitre 1", _key="chapter1")
writer.add_document(type="child", section_name="Section 1.1",
                    section_content="...", parent="chapter1")
writer.add_document(type="child", section_name="Section 1.2",
                    section_content="...", parent="chapter1")
```

## Accès aux résultats imbriqués

Pour récupérer les correspondances d'enfants aux côtés des résultats
parents, utilisez la méthode `expand` sur les résultats :

```python
results = searcher.search(parent_query)
expanded = results.expand_child("section")
```

## Facettisation imbriquée

Combinez les relations parent-enfant avec la facettisation en utilisant
`NestedParent` et `NestedChildren` comme facets :

```python
parent_facet = NestedParent(FieldFacet("type"))
results = searcher.search(query, groupedby=parent_facet)
```

## Considérations de performance

- Les jointures parent-enfant sont plus coûteuses que les recherches
  sur des documents plats.
- Utilisez l'option `childperm` du chercheur pour limiter le nombre de
  permutations examinées.
- Considérez si la structure hiérarchique est nécessaire lors de la
  requête, ou si les documents peuvent être aplatis lors de l'indexation.


## DOCUMENT (FR): Ngrams

# N-grammes

Ce guide couvre la tokenisation et l'analyse N-gramme pour la recherche
de sous-chaînes, les requêtes par préfixe et la fonctionnalité
d'autocomplétion.

## Qu'est-ce que les N-grammes ?

Un N-gramme est une séquence continue de N caractères (ou de jetons)
d'une chaîne. Par exemple, les 2-grammes de "hello" sont : "he", "el",
"ll", "lo".

L'analyse N-gramme est utile pour :
- Recherche de sous-chaînes (trouver "ell" dans "hello")
- Autocomplétion / suggestions en temps réel
- Correspondance floue sans calcul de distance d'édition

## NgramTokenizer

Le `NgramTokenizer` divise le texte en N-grammes au niveau des
caractères :

```python
from whoosh.analysis import NgramTokenizer
from whoosh import fields

tokenizer = NgramTokenizer(minsize=2, maxsize=4)

schema = fields.Schema(
    content=fields.TEXT(analyzer=tokenizer),
)
```

### Paramètres de NgramTokenizer

- `minsize` : Longueur minimale des N-grammes (par défaut `2`)
- `maxsize` : Longueur maximale des N-grammes (par défaut `4`)

Avec l'exemple ci-dessus, le texte "hello" produit ces 2-4-grammes :
`he, hel, hell, el, ell, ello, l, ll, llo, l, lo, o`

## NgramFilter

Le `NgramFilter` crée des N-grammes au niveau des mots à partir du
texte tokenisé :

```python
from whoosh.analysis import RegexTokenizer, NgramFilter

analyzer = RegexTokenizer() | NgramFilter(maxsize=2)
```

Cela produit des grammes au niveau des mots : pour "hello world", il
produit ("hello",) et ("hello", "world").

## NgramWordAnalyzer

Un analyseur de commodité qui combine `NgramTokenizer` avec
`LowercaseFilter` :

```python
from whoosh.analysis import NgramWordAnalyzer

analyzer = NgramWordAnalyzer(minsize=2, maxsize=4)

schema = fields.Schema(
    content=fields.TEXT(analyzer=analyzer),
)
```

## Cas d'utilisation

### Recherche de sous-chaînes

Avec l'analyse N-gramme, vous pouvez correspondre des sous-chaînes :

```python
from whoosh.qparser import QueryParser

# Index text with N-grams
# Searching for "ell" matches "hello" because "ell" is a substring
qp = QueryParser("content", schema=ix.schema)
q = qp.parse("ell")
results = searcher.search(q)
```

### Correspondance par préfixe

Définissez `maxsize` à une grande valeur pour créer efficacement des
N-grammes de préfixe :

```python
from whoosh.analysis import NgramWordAnalyzer

# Create N-grams where each word's prefixes become searchable tokens
# e.g., "hello" -> "h", "he", "hel", "hell", "hello"
analyzer = NgramWordAnalyzer(minsize=1, maxsize=10)
```

### Autocomplétion

Les index N-gramme sont couramment utilisés pour l'autocomplétion.
Pour une autocomplétion plus avancée avec des N-grammes de bord
(edge n-grams), envisrez :

```python
from whoosh.analysis import RegexTokenizer, NgramFilter
from whoosh.query import Prefix

# Index with standard tokenization, then use Prefix queries for autocomplete
analyzer = RegexTokenizer()
schema = fields.Schema(
    title=fields.TEXT(stored=True, analyzer=analyzer),
    content=fields.TEXT(analyzer=analyzer),
)

# For autocomplete, query with Prefix
from whoosh.qparser import QueryParser
qp = QueryParser("title", schema=ix.schema)
q = Prefix("title", "hel")  # Find documents where title starts with "hel"
```

## Comparaison avec les N-grammes de bord (Edge N-grams)

Certains moteurs de recherche prennent en charge les "N-grammes de bord"
(uniquement la génération de N-grammes à partir du début des mots).
C'est plus efficace en espace pour l'autocomplétion :

- N-grammes complets : "hello" → "he", "el", "ll", "lo", "hel", "ell", ...
- N-grammes de bord : "hello" → "h", "he", "hel", "hell", "hello"

Le `NgramTokenizer` de Whoosh génère des N-grammes complets (bidirectionnels).
Pour un comportement similaire aux N-grammes de bord, utilisez les
paramètres `minsize` et `maxsize` stratégiquement, ou utilisez des
requêtes `Prefix` sur un champ tokenisé standard.

## Considérations de performance

- Les index N-gramme sont généralement beaucoup plus volumineux que les
  index standard.
- Chaque jeton original produit plusieurs jetons N-gramme, augmentant
  la taille de l'index.
- Choisissez `minsize` et `maxsize` avec soin pour équilibrer la qualité
  de recherche et la taille de l'index.
- Pour l'autocomplétion, envisenez d'utiliser des requêtes `Prefix` sur
  un champ non-N-gramme pour de meilleures performances.


## DOCUMENT (FR): Query

# Langage de requête

Whoosh-NG fournit un langage de requête puissant similaire à Lucene, ainsi qu'une API de requêtes programmatique.

## Syntaxe de requête

### Termes de base

```
bonjour                    # Terme unique
bonjour monde              # Termes multiples (AND par défaut)
bonjour OU monde           # OR explicite
"bonjour monde"            # Phrase
```

### Spécification de champ

```
titre:python               # Recherche dans le champ titre
titre:"Tutoriel Python"    # Phrase dans un champ spécifique
```

### Opérateurs booléens

```
python AND whoosh
python OR whoosh
python AND NOT java
python AND (whoosh OR lucene)
```

### Préfixe et jokers

```
pyth*                     # Requête préfixe
pyth?n                    # Joker caractère unique
```

### Requêtes de plage

```
date:[2020 TO 2025]
prix:[10 TO 50]
rating:[4.0 TO *]         # Plage ouverte
```

### Recherche floue

```
python~2                  # Distance d'édition <= 2
lucene~1                  # Correspondance approximative
```

### Recherche de proximité

```
"bonjour monde"~5         # Dans un rayon de 5 termes
```

### Boost

```
python^2.0 whoosh         # Booster python par 2x
(titre:python)^3 content:python  # Booster les matches dans titre
```

## Classes de requêtes

Construisez des requêtes programmatiquement :

```python
from whoosh.query import *

# Terme simple
q = Term("content", "python")

# AND
q = And([Term("content", "python"), Term("content", "whoosh")])

# OR
q = Or([Term("content", "python"), Term("content", "lucene")])

# Phrase
q = Phrase("content", ["bonjour", "monde"])

# Plage
q = NumericRange("prix", 10, 50)
q = DateRange("date", datetime(2020,1,1), datetime(2025,1,1))

# Préfixe
q = Prefix("content", "pyth")
```

## MultifieldParser

Recherchez plusieurs champs avec des boosts différents :

```python
from whoosh.qparser import MultifieldParser

qp = MultifieldParser(
    ["titre", "content", "tags"],
    schema,
    fieldboosts={"titre": 2.0, "tags": 1.5}
)
q = qp.parse("python recherche")
```

## Échappement des caractères spéciaux

```
titre\:python              # Deux-points littéral
chemin\:\/\/exemple        # Échapper les caractères spéciaux
```


## DOCUMENT (FR): Quickstart

# Démarrage rapide

## Installation

```bash
pip install whoosh-ng
uv pip install whoosh-ng
```

## Exemple basique

```python
from whoosh import index
from whoosh.fields import Schema, TEXT, ID

schema = Schema(id=ID(stored=True), content=TEXT())
ix = index.create_in("indexdir", schema)

with ix.writer() as w:
    w.add_document(id="1", content="hello world")
    w.add_document(id="2", content="goodbye world")

with ix.searcher() as s:
    results = s.search("world")
    for hit in results:
        print(hit["id"], hit.score)
```

## Avec plugins

```bash
pip install whoosh-ng[vector,autocomplete,api]
```

```python
from whoosh.plugins.manager import PluginManager
from whoosh_modern.vector.plugin import VectorPlugin

PluginManager.load_plugins()
```

## Sources de données

```bash
pip install whoosh-ng
```

```python
import sqlite3
from whoosh_modern.data_sources import SQLSource
from whoosh_modern.views import SearchView

# Utiliser les données de benchmark existantes
conn = sqlite3.connect("benchmark/benchmark_data.db")
source = SQLSource(
    connection=conn,
    query="SELECT * FROM reuters_articles",
)

vue = SearchView(name="reuters", source=source)
ix = vue.build("indexdir")
```


## DOCUMENT (FR): Recipes

﻿---
title: "Whoosh Recipes"
sidebar_position: 17
Module: whoosh
Version: 2.7.4
---
> **Note de traduction** : Cette page n'est pas encore traduite en francais.
> Le contenu anglais est affiche ci-dessous en attendant la traduction.

<!-- Creez une version francaise de ce fichier et supprimez ce message. -->


# Whoosh recipes

A collection of small, practical code snippets for common tasks.

## General

### Get the stored fields for a document from the document number

```python
stored_fields = searcher.stored_fields(docnum)
```

## Analysis

### Eliminate words shorter/longer than N

Use a `StopFilter` and the `minsize` and `maxsize` keyword arguments. If you
just want to filter based on size and not common words, set the `stoplist` to
`None`:

```python
sf = analysis.StopFilter(stoplist=None, minsize=2, maxsize=40)
```

### Allow optional case-sensitive searches

Index both the original and lowercased versions of each word. If the user
searches for an all-lowercase word, it acts as a case-insensitive search, but
if they search for a word with any uppercase characters, it acts as a
case-sensitive search:

```python
class CaseSensitivizer(analysis.Filter):
    def __call__(self, tokens):
        for t in tokens:
            yield t
            if t.mode == "index":
                low = t.text.lower()
                if low != t.text:
                    t.text = low
                    yield t

ana = analysis.RegexTokenizer() | CaseSensitivizer()
print([t.text for t in ana("The new SuperTurbo 5000", mode="index")])
# ["The", "the", "new", "SuperTurbo", "superturbo", "5000"]
```

## Searching

### Find every document

```python
myquery = query.Every()
```

### iTunes-style search-as-you-type

Use `whoosh.analysis.NgramWordAnalyzer` as the analyzer for the field you want
to search as the user types. You can save space in the index by turning off
positions in the field using `phrase=False`:

```python
# For example, to search the "title" field as the user types
analyzer = analysis.NgramWordAnalyzer()
title_field = fields.TEXT(analyzer=analyzer, phrase=False)
schema = fields.Schema(title=title_field)
```

See the documentation for the `NgramWordAnalyzer` class for information on the
available options. Also see [N-grams](/core/ngrams).

## Shortcuts

### Look up documents by a field value

```python
# Single document (unique field value)
stored_fields = searcher.document(id="bacon")

# Multiple documents
for stored_fields in searcher.documents(tag="cake"):
    ...
```

## Sorting and scoring

See [Sorting](/core/sorting).

### Score results based on the position of the matched term

The following scoring function uses the position of the first occurrence of a
term in each document to calculate the score, so documents with the given term
earlier in the document will score higher:

```python
from whoosh import scoring

def pos_score_fn(searcher, fieldname, text, matcher):
    poses = matcher.value_as("positions")
    return 1.0 / (poses[0] + 1)

pos_weighting = scoring.FunctionWeighting(pos_score_fn)
with myindex.searcher(weighting=pos_weighting) as s:
    ...
```

## Results

### How many hits were there?

```python
# The number of scored hits
found = results.scored_length()

if results.has_exact_length():
    print("Scored", found, "of exactly", len(results), "documents")
else:
    low = results.estimated_min_length()
    high = results.estimated_length()
    print("Scored", found, "of between", low, "and", high, "documents")
```

### Which terms matched in each hit?

```python
# Use terms=True to record term matches for each hit
results = searcher.search(myquery, terms=True)

for hit in results:
    # Which terms matched in this hit?
    print("Matched:", hit.matched_terms())
    # Which terms from the query didn't match in this hit?
    print("Didn't match:", myquery.all_terms() - hit.matched_terms())
```

## Global information

### How many documents are in the index?

```python
# Including documents that are deleted but not yet optimized away
numdocs = searcher.doc_count_all()
# Not including deleted documents
numdocs = searcher.doc_count()
```

### What fields are in the index?

```python
return myindex.schema.names()
```

### Is term X in the index?

```python
return ("content", "wobble") in searcher
```

### How many times does term X occur in the index?

```python
# Number of times content:wobble appears in all documents
freq = searcher.frequency("content", "wobble")
# Number of documents containing content:wobble
docfreq = searcher.doc_frequency("content", "wobble")
```

### Is term X in document Y?

```python
# Without term vectors
postings = searcher.postings("content", "wobble")
postings.skip_to(500)
return postings.id() == 500

# If field has term vectors
vector = searcher.vector(500, "content")
vector.skip_to("wobble")
return vector.id() == "wobble"
```

## See also

- [Analysis](/core/analysis) â€” Analyzers, tokenizers, and filters
- [N-grams](/core/ngrams) â€” Search-as-you-type with N-gram analyzers
- [Searching](/core/searching) â€” The `search()` method and `Hit` objects


## DOCUMENT (FR): Schema

# Conception de schéma

Le schéma définit la structure des documents dans votre index. Il spécifie les champs existants, leur indexation et leur stockage.

## Types de champs

| Type | Description | Indexé | Stocké |
|------|-------------|--------|--------|
| `TEXT` | Texte libre, tokenisé | Oui | Optionnel |
| `ID` | Identifiant non tokenisé | Oui | Optionnel |
| `KEYWORD` | Mots-clés séparés par espace/virgule | Oui | Optionnel |
| `STORED` | Stocké uniquement, non searchable | Non | Oui |
| `NUMERIC` | Entier ou flottant | Oui | Optionnel |
| `DATETIME` | Dates et heures | Oui | Optionnel |
| `BOOLEAN` | Booléen | Oui | Optionnel |
| `NGRAM` | N-grammes de caractères | Oui | Optionnel |
| `NGRAMWORDS` | N-grammes de mots | Oui | Optionnel |
| `VectorField` | Vecteur d'embedding | Personnalisé | Optionnel |

## Créer un schéma

```python
from whoosh.fields import Schema, TEXT, ID, KEYWORD, STORED, NUMERIC

schema = Schema(
    title=TEXT(stored=True),
    path=ID(stored=True, unique=True),
    content=TEXT,
    tags=KEYWORD(lowercase=True),
    published=NUMERIC(int, stored=True),
    is_published=BOOLEAN,
    icon=STORED
)
```

## Options des champs

### TEXT

```python
content = TEXT(
    stored=False,        # Stocker le texte original ?
    unique=False,        # Utiliser pour remplacer des documents ?
    phrase=True,         # Indexer les positions pour recherche de phrases
    analyzer=None,       # Analyseur personnalisé
    field_boost=1.0      # Boost pour le scoring
)
```

### ID

```python
path = ID(
    stored=True,         # Stocker le chemin
    unique=True          # Utiliser pour remplacement de documents
)
```

### KEYWORD

```python
tags = KEYWORD(
    stored=False,
    lowercase=True,      # Minusculiser automatiquement
    commas=True,         # Séparer par virgules
    scorable=True        # Stocker la longueur pour scoring
)
```

## SchemaBuilder

Whoosh-NG v4.0.0.dev0 (en développement) introduit `SchemaBuilder` pour une API fluide :

```python
from whoosh.fields import SchemaBuilder, TEXT, ID, NUMERIC

schema = (
    SchemaBuilder()
    .field("title", TEXT(stored=True))
    .field("path", ID(stored=True, unique=True))
    .field("content", TEXT)
    .field("rating", NUMERIC(float, stored=True))
    .build()
)
```

## Champs dynamiques

Utilisez des patterns glob pour associer des types :

```python
# Tout champ finissant par "_date" est un DATETIME
schema.add("*_date", DATETIME(stored=True), glob=True)

# Tout champ finissant par "_id" est un ID
schema.add("*_id", ID(stored=True), glob=True)
```

## Modifier le schéma

Ajoutez ou supprimez des champs après création :

```python
writer = ix.writer()

# Ajouter un champ
writer.add_field("description", TEXT(stored=True))

# Supprimer un champ
writer.remove_field("legacy_field")

writer.commit()
```

> Note: Supprimer un champ ne fait que le retirer du schéma. Les données ne sont libérées qu'à l'optimisation.

## Modèles de recherche

Whoosh-NG peut mapper automatiquement des modèles Python (dataclasses, Pydantic, SQLAlchemy, SQLModel, msgspec) vers un `Schema` Whoosh via `ModelIndex`.

### Niveau 1 : Auto-mapping

```python
from dataclasses import dataclass
from whoosh_modern.models import ModelIndex

@dataclass
class Book:
    title: str
    count: int
    tag: str | None = None

idx = ModelIndex(Book)
schema = idx.schema
```

`ModelIndex` inspecte les annotations de type et les mappe vers des champs Whoosh :

| Type Python | Champ Whoosh |
|-------------|--------------|
| `str` | `TEXT` |
| `int` / `float` | `NUMERIC` |
| `bool` | `BOOLEAN` |
| `datetime` / `date` | `DATETIME` |
| `Decimal` | `NUMERIC(int, decimal_places=2)` |
| `Enum` | `KEYWORD` |
| `bytes` | `KEYWORD` (stockage hexadécimal) |
| `list[str]` | `KEYWORD` |
| `Optional[T]` | type mappé ou `STORED` |

Les champs ID sont auto-détectés : `SearchOptions(id=True)` explicite > nom `id`/`ID`/`_id` > premier champ `str`.

### Niveau 2 : Options explicites

Utilisez `SearchField` pour remplacer les valeurs par défaut :

```python
from whoosh_modern.models import SearchField, SearchOptions

class Book:
    title: str = SearchField(fulltext=True, stored=True)
    count: int = SearchField(sortable=True)
    tags: list[str] = SearchField(multi=True)
```

### Niveau 3 : Types annotés

Utilisez `Annotated` pour attacher des métadonnées directement aux annotations :

```python
from typing import Annotated
from whoosh_modern.models import SearchField

class Book:
    title: Annotated[str, SearchField(fulltext=True, stored=True)]
```

### Intégrations

#### Dataclass

```python
from dataclasses import dataclass
from whoosh_modern.models import ModelIndex

@dataclass
class Article:
    title: str
    body: str
    published: datetime.datetime

idx = ModelIndex(Article)
```

#### Pydantic v2

```python
from pydantic import BaseModel
from whoosh_modern.models import register_model

class Article(BaseModel):
    title: str
    body: str
    published: datetime.datetime

    # Métadonnées de recherche par champ via json_schema_extra
    model_config = {"json_schema_extra": {"search": {"fulltext": True}}}

idx = register_model(Article)
```

#### SQLAlchemy

```python
from sqlalchemy import Column, Integer, String, DateTime
from sqlalchemy.orm import DeclarativeBase
from whoosh_modern.models import register_model

class Base(DeclarativeBase):
    pass

class Article(Base):
    __tablename__ = "articles"
    id = Column(Integer, primary_key=True)
    title = Column(String, info={"search": {"fulltext": True, "stored": True}})
    published = Column(DateTime, info={"search": {"sortable": True}})

idx = register_model(Article)
```

#### SQLModel

```python
from sqlmodel import SQLModel, Field
from whoosh_modern.models import register_model

class Article(SQLModel, table=True):
    id: int = Field(primary_key=True)
    title: str = Field(sa_column_kwargs={"info": {"search": {"fulltext": True}}})
    published: datetime.datetime

idx = register_model(Article)
```

#### msgspec

```python
import msgspec
from whoosh_modern.models import register_model

class Article(msgspec.Struct):
    title: str = msgspec.field(metadata={"search": {"fulltext": True}})
    published: datetime.datetime

idx = register_model(Article)
```

### Conversion d'instances

```python
doc = idx.to_whoosh_document(book_instance)
writer.add_document(**doc)
```

`to_whoosh_document` gère :
- dataclass : itération via `dataclasses.fields()`
- Pydantic/SQLModel : itération via `model_fields`
- SQLAlchemy : itération via `__mapper__.columns`
- Valeurs Enum converties en `.value`
- `bytes` convertis en chaîne hexadécimale

## Bonnes pratiques

1. **Minimal** : N'indexez que ce que vous cherchez
2. **STORED avec parcimonie** : Augmente la taille de l'index
3. **Champs uniques** : Utilisez `unique=True` pour les identifiants
4. **Boost de champ** : Boostez les champs importants au niveau schéma
5. **TEXT options** : Désactivez `phrase` si vous n'avez pas besoin de recherche de phrase
6. **Champ ID** : Laissez `ModelIndex` auto-détecter ou marquez explicitement avec `SearchOptions(id=True)`


## DOCUMENT (FR): Searching

# Recherche

Guide pour exécuter des recherches, travailler avec les résultats, le scoring et le tri.

## Recherche basique

```python
from whoosh.qparser import QueryParser

with ix.searcher() as searcher:
    qp = QueryParser("content", ix.schema)
    q = qp.parse("bonjour monde")
    results = searcher.search(q)
    for hit in results:
        print(hit["title"], hit.score)
```

## Le Searcher

Le `Searcher` est l'interface principale pour lire l'index.

```python
# Toujours utiliser le context manager
with ix.searcher() as searcher:
    results = searcher.search(query)

# Ou gestion manuelle
searcher = ix.searcher()
try:
    results = searcher.search(query)
finally:
    searcher.close()
```

## QueryParser

Convertit une chaîne de requête en objet Query :

```python
from whoosh.qparser import QueryParser, OrGroup

# AND par défaut entre termes
qp = QueryParser("content", schema)
q = qp.parse("bonjour monde")  # content:bonjour AND content:monde

# Changer l'opérateur par défaut
qp = QueryParser("content", schema, group=OrGroup)
q = qp.parse("bonjour monde")  # content:bonjour OR content:monde
```

## Méthodes de recherche

### search()

```python
results = searcher.search(
    query,
    limit=10,           # Max résultats (None pour tout)
    sortedby=None,      # Clé(s) de tri
    reverse=False,      # Tri inversé
    terms=False,        # Collecter les termes matchés
    filter=None,        # Autoriser seulement ces docnums
    mask=None,          # Exclure ces docnums
    collapse=None       # Facette d'effondrement
)
```

### search_page()

```python
# Page 1, 10 résultats par page (défaut)
results = searcher.search_page(query, 1)

# Page 3, 20 résultats par page
results = searcher.search_page(query, 3, pagelen=20)
```

## Résultats

`Results` agit comme une liste de documents matchés :

```python
results = searcher.search(query)

# Support de slice
first_five = results[0:5]

# Longueur (peut déclencher un recompte)
total = len(results)

# Longueur scorée (ce qui est réellement retourné)
scored = results.scored_length()
```

### Objet Hit

```python
for hit in results:
    # Champs stockés
    title = hit["title"]
    path = hit["path"]

    # Score
    print(hit.score)

    # Surbrillance
    highlights = hit.highlights("content", top=3)
```

## Scoring

Le modèle de scoring par défaut est BM25F :

```python
from whoosh import scoring

with ix.searcher(weighting=scoring.BM25F()) as s:
    results = s.search(query)
```

### Scoring personnalisé

```python
class MyScorer(scoring.WeightingModel):
    def scorer(self, searcher, fieldname, text, qf=1):
        return MyCustomScorer(searcher, fieldname, text, qf)

with ix.searcher(weighting=MyScorer()) as s:
    results = s.search(query)
```

## Tri

```python
from whoosh import sorting

# Tri par champ unique
results = searcher.search(query, sortedby="date")

# Tri inversé
results = searcher.search(query, sortedby="date", reverse=True)

# Tri multi-champs
results = searcher.search(query, sortedby=[
    sorting.FieldFacet("category"),
    sorting.ScoreFacet()
])
```

## Facettes

```python
from whoosh import sorting

facet = sorting.FieldFacet("category")
with searcher.all_features() as features:
    facets = features.facet(facet)
    for cat, count in facets.most_common():
        print(f"{cat}: {count}")
```

## Filtrage et masquage

```python
from whoosh.query import Term

# Autoriser seulement les documents publiés
filter_q = Term("published", True)
results = searcher.search(query, filter=filter_q)

# Exclure les brouillons
mask_q = Term("draft", True)
results = searcher.search(query, mask=mask_q)
```

## Surbrillance

```python
results = searcher.search(query, terms=True)

for hit in results:
    print(hit.highlights("content", top=2))
```

## Recherches à temps limité

```python
from whoosh.collectors import TimeLimitCollector

with ix.searcher() as s:
    c = s.collector(limit=None)
    tlc = TimeLimitCollector(c, timelimit=5.0)
    try:
        s.search_with_collector(query, tlc)
    except TimeLimit:
        print("Recherche annulée: trop lente")
    results = tlc.results()
```


## DOCUMENT (FR): Sorting

> **Note de traduction** : Cette page n'est pas encore traduite en français.
> Le contenu anglais est affiché ci-dessous en attendant la traduction.

<!-- Creez une version francaise de ce fichier et supprimez ce message. -->


# Sorting

The `whoosh.sorting` module provides facets and sort-key computation for ordering and grouping search results.

## Quick start

```python
from whoosh import sorting

# Sort by a field
results = searcher.search(query, sortedby="date")

# Sort descending
results = searcher.search(query, sortedby=sorting.FieldFacet("price", reverse=True))
```

For the full API reference, see [Sorting API](/api/sorting).


## DOCUMENT (FR): Spelling

﻿---
title: "Did you mean..."
sidebar_position: 12
Module: whoosh.spelling
Version: 2.7.4
---
> **Note de traduction** : Cette page n'est pas encore traduite en francais.
> Le contenu anglais est affiche ci-dessous en attendant la traduction.

<!-- Creez une version francaise de ce fichier et supprimez ce message. -->


# "Did you mean... ?" Correcting errors in user queries

## Overview

Whoosh can quickly suggest replacements for mis-typed words by returning a list
of words from the index (or a dictionary) that are close to the mis-typed word:

```python
with ix.searcher() as s:
    corrector = s.corrector("text")
    for mistyped_word in mistyped_words:
        print(corrector.suggest(mistyped_word, limit=3))
```

See the `whoosh.spelling.Corrector.suggest()` method documentation for
information on the arguments.

Currently the suggestion engine is more like a "typo corrector" than a real
"spell checker" since it doesn't do the kind of sophisticated phonetic matching
or semantic/contextual analysis a good spell checker might. However, it is
still very useful.

There are two main strategies for correcting words:

- Use the terms from an index field.
- Use words from a word list.

## Pulling suggestions from an indexed field

In Whoosh 2.7 and later, spelling suggestions are available on all fields.
However, if you have an analyzer that modifies the indexed words (such as
stemming), you can add `spelling=True` to a field to have it store separate
unmodified versions of the terms for spelling suggestions:

```python
ana = analysis.StemmingAnalyzer()
schema = fields.Schema(text=TEXT(analyzer=ana, spelling=True))
```

You can then use the `whoosh.searching.Searcher.corrector()` method to get a
corrector for a field:

```python
corrector = searcher.corrector("content")
```

The advantage of using the contents of an index field is that when you are
spell checking queries on that index, the suggestions are tailored to the
contents of the index. The disadvantage is that if the indexed documents
contain spelling errors, then the spelling suggestions will also be erroneous.

## Pulling suggestions from a word list

There are plenty of word lists available on the internet you can use to populate
the spelling dictionary. `word_list` can be a list of unicode strings, or a
file object with one word on each line.

```python
from whoosh.spelling import ListCorrector

# word_list must be a sorted list of unicode strings
corrector = ListCorrector(word_list)
```

## Merging two or more correctors

You can combine suggestions from two sources (for example, the contents of an
index field and a word list) using a `whoosh.spelling.MultiCorrector`:

```python
c1 = searcher.corrector("content")
c2 = spelling.ListCorrector(word_list)
corrector = MultiCorrector([c1, c2])
```

## Correcting user queries

You can spell-check a user query using the
`whoosh.searching.Searcher.correct_query()` method:

```python
from whoosh import qparser

# Parse the user query string
qp = qparser.QueryParser("content", myindex.schema)
q = qp.parse(qstring)

# Try correcting the query
with myindex.searcher() as s:
    corrected = s.correct_query(q, qstring)
    if corrected.query != q:
        print("Did you mean:", corrected.string)
```

The `correct_query` method returns an object with the following attributes:

- `query` â€” A corrected `whoosh.query.Query` tree. Compare it (`==`) with the
  original parsed query to check if the corrector changed anything.
- `string` â€” A corrected version of the user's query string.
- `tokens` â€” A list of corrected token objects representing the corrected terms.

You can use a `whoosh.highlight.Formatter` object to format the corrected query
string, for example the `HtmlFormatter` to format it as HTML:

```python
from whoosh import highlight

hf = highlight.HtmlFormatter()
corrected = s.correct_query(q, qstring, formatter=hf)
```

## See also

- [Highlighting](/core/highlight) â€” Format corrected query strings with a formatter
- [Query Language](/core/query) â€” Parsing user queries
- [API: spelling](../api/spelling) â€” Full `whoosh.spelling` reference


## DOCUMENT (FR): Stemming

# Racinement (Stemming) et mots vides

Ce guide couvre l'utilisation des racines (stemmers), des filtres de
mots vides (stop words) et de l'analyse de texte spécifique à chaque
langue avec Whoosh.

## Racines (Stemmers)

Un racine (stemmer) réduit les mots à leur forme racine (par ex.,
"running" → "run", "cats" → "cat"), afin que les différentes formes
du même mot correspondent lors des recherches.

### Utilisation de StemmerFilter

```python
from whoosh.analysis import RegexTokenizer, StemmerFilter
from whoosh.lang.porter import stem
from whoosh import fields

# English Porter stemmer
stem_analyzer = RegexTokenizer() | StemmerFilter(stemfn=stem)

schema = fields.Schema(
    title=fields.TEXT(stored=True),
    content=fields.TEXT(analyzer=stem_analyzer),
)
```

### Racines Snowball

Whoosh inclut des racines Snowball pour plusieurs langues :

```python
from whoosh.analysis import StemmerFilter
from whoosh.lang.snowball import EnglishStemmer

stem_analyzer = RegexTokenizer() | StemmerFilter(stemfn=EnglishStemmer().stem)
```

### Sélection de racine selon la langue

```python
from whoosh.lang import stemmer_for_language, StemmerFilter
from whoosh.analysis import RegexTokenizer

stem = stemmer_for_language("en")
analyzer = RegexTokenizer() | StemmerFilter(stemfn=stem)

# Ou utilisez l'analyseur StemmingAnalyzer :
from whoosh.analysis import StemmingAnalyzer

analyzer = StemmingAnalyzer("en")
```

### Langues disponibles

```python
from whoosh.lang import languages, has_stemmer, has_stopwords

print(languages)  # ('ar', 'da', 'nl', 'en', 'fi', 'fr', ...)
print(has_stemmer("en"))  # True
print(has_stopwords("en"))  # True
```

## Mots vides (Stop Words)

Les mots vides sont des mots fréquents (comme "the", "a", "and") qui
sont généralement filtrés lors de l'indexation car ils apparaissent
dans trop de documents pour être utiles au classement.

### Utilisation de StopFilter

```python
from whoosh.analysis import RegexTokenizer, StopFilter
from whoosh.lang import stopwords_for_language

# English stop words
stop_words = set(stopwords_for_language("en"))
stop_analyzer = RegexTokenizer() | StopFilter(stoplist=stop_words)

schema = fields.Schema(
    content=fields.TEXT(analyzer=stop_analyzer),
)
```

### Combinaison de racinement et de mots vides

```python
from whoosh.analysis import StemmingAnalyzer

# StemmingAnalyzer charge automatiquement le racine et les mots vides pour la langue
analyzer = StemmingAnalyzer("en")

schema = fields.Schema(
    content=fields.TEXT(analyzer=analyzer),
)
```

### Mots vides personnalisés

```python
from whoosh.analysis import RegexTokenizer, StopFilter

# Liste personnalisée de mots vides
custom_stops = frozenset(["the", "a", "an", "foo", "bar"])
analyzer = RegexTokenizer() | StopFilter(stoplist=custom_stops)
```

## StemmingAnalyzer (Recommandé)

Le `StemmingAnalyzer` combine le tokeniseur, le racinement et le
filtrage des mots vides :

```python
from whoosh.analysis import StemmingAnalyzer

# Utilise automatiquement le bon racine et les mots vides pour la langue
analyzer = StemmingAnalyzer("en")

# Vous pouvez remplacer les valeurs par défaut
analyzer = StemmingAnalyzer("en",
                            use_stopwords=True,
                            use_stems=True)
```

### Options de StemmingAnalyzer

- `lang` : Code de langue (ex. : `"en"`, `"fr"`, `"de"`)
- `use_stopwords` : Charge et applique les mots vides (par défaut `True`)
- `use_stems` : Applique le racinement (par défaut `True`)
- `args` : Arguments passés au tokeniseur
- `kwargs` : Arguments du mot-clé pour le racine ou les mots vides

## Considérations spécifiques selon la langue

### Arabe (ISRI Stemmer)

```python
from whoosh.analysis import StemmerFilter
from whoosh.lang.isri import ISRIStemmer

stem_analyzer = RegexTokenizer() | StemmerFilter(stemfn=ISRIStemmer().stem)
```

### Double Métaphone pour la correspondance phonétique

```python
from whoosh.analysis import RegexTokenizer, DoubleMetaphoneFilter

analyzer = RegexTokenizer() | DoubleMetaphoneFilter()
```

## Racinement côté requête

L'analyseur est appliqué à la fois lors de l'indexation et lors de la
requête (via l'analyseur de requête), donc le racinement est automatiquement
appliqué aux termes de recherche :

```python
from whoosh.qparser import QueryParser

# Si l'index utilise le racinement, les requêtes sont racinées aussi
qp = QueryParser("content", schema=ix.schema)
q = qp.parse("running cats")  # Correspondra à "run", "cat", etc.
```

## Analyse N-gramme

Pour la correspondance de sous-chaînes et les requêtes par préfixe,
utilisez les analyseurs N-gramme :

```python
from whoosh.analysis import NgramWordAnalyzer

analyzer = NgramWordAnalyzer(minsize=2, maxsize=4)
schema = fields.Schema(content=fields.TEXT(analyzer=analyzer))
```

Voir le [Guide N-grammes](ngrams.md) pour plus de détails.

## Fournisseurs de racines modernes (Whoosh-NG 2.0)

Whoosh-NG 2.0 introduit un système de fournisseurs de racines de style
plugin avec détection automatique, support de PyStemmer et d'analyseurs
spécifiques à chaque langue. Pour plus de détails, voir le
[Guide des fournisseurs de racines](stemmers-fournisseurs.md).


## DOCUMENT (FR): Threads

﻿---
title: "Concurrency, Locking & Versioning"
sidebar_position: 14
Module: whoosh.index, whoosh.searching, whoosh.store
Version: 2.7.4
---
> **Note de traduction** : Cette page n'est pas encore traduite en francais.
> Le contenu anglais est affiche ci-dessous en attendant la traduction.

<!-- Creez une version francaise de ce fichier et supprimez ce message. -->


# Concurrency, locking, and versioning

## Concurrency

The `FileIndex` object is "stateless" and should be share-able between threads.

A `Reader` object (which underlies the `Searcher` object) wraps open files and
often individual methods rely on consistent file cursor positions (e.g. they
do two `file.read()`s in a row, so if another thread moves the cursor between
the two read calls Bad Things would happen). You should use one Reader/Searcher
per thread in your code.

Readers/Searchers tend to cache information (such as field caches for
sorting), so if you can share one across multiple search requests, it's a big
performance win.

> Whoosh-NG also provides `AsyncWriter` and `BufferedWriter` in
> `whoosh.writing` (see [Indexing](/core/indexing)) as convenient wrappers for
> multi-process write scenarios.

## Locking

Only one thread/process can write to an index at a time. When you open a
writer, it locks the index. If you try to open a writer on the same index in
another thread/process, it will raise `whoosh.store.LockError`.

In a multi-threaded or multi-process environment your code needs to be aware
that opening a writer may raise this exception if a writer is already open.
Whoosh includes a couple of example implementations
(`whoosh.writing.AsyncWriter` and `whoosh.writing.BufferedWriter`) of ways to
work around the write lock.

While the writer is open and during the commit, **the index is still available
for reading**. Existing readers are unaffected and new readers can open the
current index normally.

### Lock files

Locking the index is accomplished by acquiring an exclusive file lock on the
`<indexname>_WRITELOCK` file in the index directory. The file is not deleted
after the file lock is released, so the fact that the file exists **does not**
mean the index is locked.

## Versioning

When you open a reader/searcher, the reader represents a view of the **current
version** of the index. If someone writes changes to the index, any readers
that are already open **will not** pick up the changes automatically. A reader
always sees the index as it existed when the reader was opened.

If you are re-using a Searcher across multiple search requests, you can check
whether the Searcher is a view of the latest version of the index using
`whoosh.searching.Searcher.up_to_date()`. If the searcher is not up to date,
you can get an up-to-date copy of the searcher using
`whoosh.searching.Searcher.refresh()`:

```python
# If 'searcher' is not up-to-date, replace it
searcher = searcher.refresh()
```

If the searcher has the latest version of the index, `refresh()` simply returns
it. Calling `Searcher.refresh()` is more efficient than closing the searcher
and opening a new one, since it will re-use any underlying readers and caches
that haven't changed.

## See also

- [Indexing](/core/indexing) â€” Writers, `AsyncWriter`, `BufferedWriter`
- [API: writing](../api/writing) â€” Writer concurrency helpers


## DOCUMENT (FR): Translation Status

> **Note de traduction** : Cette page n'est pas encore traduite en français.
> Le contenu anglais est affiché ci-dessous en attendant la traduction.

<!-- Creez une version francaise de ce fichier et supprimez ce message. -->


# Translation Completion Tracking

- [x] EN quickstart
- [x] EN guides
- [x] EN API pages
- [x] EN examples
- [x] FR quickstart
- [x] FR guides
- [x] FR API pages
- [x] FR examples


## DOCUMENT (FR): Autocomplete

# Autocomplétion avec Whoosh‑NG

Cet exemple démontre la fonctionnalité **autocomplete/suggestion** avec le plugin `whoosh_modern.autocomplete`.

## 1. Installation

```bash
pip install "whoosh-ng[autocomplete]"
```

## 2. Schéma avec champ Keyword pour les termes

```python
from whoosh import index
from whoosh.fields import Schema, TEXT, KEYWORD
from whoosh_modern.autocomplete.plugin import AutocompletePlugin
from whoosh.plugins.manager import PluginManager

schema = Schema(
    title=TEXT(stored=True),
    tags=KEYWORD(stored=True, commas=True),
)

ix = index.create_in("autocomplete_index", schema)
```

## 3. Enregistrer le plugin

```python
AutocompletePlugin().register(PluginManager())
```

## 4. Indexer les documents

```python
with ix.writer() as w:
    w.add_document(title="Python Programming", tags="python,programming,language")
    w.add_document(title="JavaScript Basics", tags="javascript,programming,web")
    w.add_document(title="Machine Learning", tags="ml,ai,data-science")
    w.commit()
```

## 5. Utiliser l’index inversé pour les suggestions

```python
from whoosh_modern.autocomplete.factory import create_autocomplete
from whoosh.registry import AutocompleteRegistry

provider = AutocompleteRegistry.get("inverted")

with ix.searcher() as s:
    for term in s.lexicon("tags"):
        provider.add_term(term, s.doc_count_all())

suggestions = provider.suggest("py", maxdist=1, limit=5)
print(suggestions)  # ['python', 'programming']
```

## Points clés

- Installez avec `pip install whoosh-ng[autocomplete]`.
- Utilisez des champs `KEYWORD` pour les tags/mots-clés.
- Enregistrez `AutocompletePlugin` pour activer les suggestions.
- Le provider inverted supporte les correspondances floues (`maxdist`).


## DOCUMENT (FR): Basic Indexing

# Indexation de base

Exemples pour indexer des documents dans Whoosh‑NG. Chaque section est un script
autonome **exécutable**.

> **Scénario concret** : Vous construisez un moteur de recherche de blog. Vous avez
> un fichier CSV d'articles (`blog_posts.csv`) avec les colonnes `title`, `url`,
> `tags`, `body` et `published_at`.

## 1. Schéma de production

```python
from whoosh import index
from whoosh.fields import Schema, TEXT, ID, KEYWORD, NUMERIC, DATETIME
from datetime import datetime

schema = Schema(
    doc_id=ID(stored=True, unique=True),
    title=TEXT(stored=True),
    url=ID(stored=True),
    tags=KEYWORD(stored=True, commas=True),
    body=TEXT(stored=True, phrase=True),
    published_at=DATETIME(stored=True, sortable=True),
    word_count=NUMERIC(int, stored=True),
)
```

## 2. Créer l'index

```python
from whoosh import index
from whoosh.fields import Schema, TEXT, ID, KEYWORD, NUMERIC, DATETIME
import shutil

shutil.rmtree("blog_index", ignore_errors=True)
ix = index.create_in("blog_index", schema)
```

## 3. Indexer depuis un CSV

```python
import csv
from datetime import datetime

with open("blog_posts.csv", newline="", encoding="utf-8") as f:
    reader = csv.DictReader(f)
    with ix.writer() as writer:
        for row in reader:
            writer.add_document(
                doc_id=row["doc_id"],
                title=row["title"],
                url=row["url"],
                tags=row["tags"],
                published_at=datetime.fromisoformat(row["published_at"]),
                word_count=int(row["word_count"]),
                body=row["body"],
            )
        writer.commit()
```

## 4. Mise à jour incrémentale

```python
updated_posts = [
    {"doc_id": "1", "title": "Titre mis à jour", "body": "Nouveau contenu..."},
]

with ix.writer() as writer:
    for post in updated_posts:
        writer.update_document(
            doc_id=post["doc_id"],
            title=post["title"],
            url=f"/posts/{post['doc_id']}",
            tags="python,search",
            published_at=datetime(2024, 6, 1),
            word_count=len(post["body"].split()),
            body=post["body"],
        )
    writer.commit()
```

## 5. Suppression

```python
from whoosh.query import Term

with ix.writer() as writer:
    writer.delete_by_term("doc_id", "3")
    writer.commit()
```

## 6. Indexation en bloc (10k+ documents)

```python
from whoosh.writing import BufferedWriter

buffered = BufferedWriter(ix, period=60, limit=500)
try:
    for doc in large_dataset:
        with buffered:
            buffered.add_document(**doc)
finally:
    buffered.close()
```

## 7. Recherche sur les données indexées

```python
from whoosh.qparser import QueryParser

ix = index.open_dir("blog_index")

with ix.searcher() as s:
    qp = QueryParser("body", ix.schema)
    q = qp.parse("moteur de recherche")

    results = s.search(q, limit=10)
    for hit in results:
        print(f"{hit['title']} | {hit['url']} | score={hit.score:.3f}")
```


## DOCUMENT (FR): Data Sources

# Sources de données

Whoosh-NG fournit une couche de sources de données flexible pour l'indexation de documents à partir de bases de données SQL, d'API REST, d'API GraphQL, de fichiers et d'autres fournisseurs.

## Protocole DataSource

Toutes les sources implémentent le protocole `DataSource` :

```python
from whoosh_modern.data_sources import DataSource

class DataSource(Protocol):
    @property
    def name(self) -> str: ...

    def discover_schema(self) -> Schema: ...
    def iter_documents(self) -> Iterator[Document]: ...
    def document_count(self) -> int: ...
    def metadata(self) -> Mapping[str, Any]: ...
```

### Protocoles de capacités

| Protocole | Description |
|----------|-------------|
| `DataSource` | Protocole de base : nom, schéma, itération, métadonnées |
| `IncrementalDataSource` | Supporte `iter_changes(since)` |
| `AsyncDataSource` | Diffusion asynchrone via `aiter_documents()` |
| `RefreshableDataSource` | Support de `refresh()` |
| `CountableDataSource` | `document_count()` |
| `MetadataDataSource` | `metadata()` |
| `ObservableDataSource` | Callbacks d'observation pour les changements de documents |

---

## SQLSource

`SQLSource` se connecte aux bases de données SQL et restitue les documents depuis les résultats de requête, avec pooling de connexions automatique.

### Utilisation de base

```python
from whoosh_modern.data_sources.sql import SQLSource
import sqlite3

conn = sqlite3.connect("mydb.db")
source = SQLSource(
    connection=conn,
    query="SELECT * FROM products",
)

schema = source.discover_schema()
for doc in source.iter_documents():
    print(doc["title"], doc["price"])

count = source.document_count()
```

### Pooling de connexions

```python
from whoosh_modern.data_sources.sql import SQLSource

source = SQLSource(
    connection="sqlite:///mydb.db",
    query="SELECT * FROM products",
    pool_size=10,
    pool_recycle=3600,
)
```

### GROUP BY

```python
source = SQLSource(
    connection=conn,
    query="""
        SELECT category, COUNT(*) as doc_count,
               AVG(price) as avg_price
        FROM products GROUP BY category
    """,
)
```

### JOINs avec alias

```python
source = SQLSource(
    connection=conn,
    query="""
        SELECT p.id AS product_id, p.name AS product_name,
               c.name AS category_name
        FROM products p
        JOIN categories c ON p.category_id = c.id
    """,
)
```

### Synchronisation incrémentale

```python
from datetime import datetime

source = SQLSource(
    connection=conn,
    query="SELECT * FROM articles",
    incremental_field="updated_at",
    id_field="id",
)

for doc in source.iter_changes(since=datetime(2025, 1, 1)):
    print(doc["id"], doc["updated_at"])
```

### SQLAlchemySource

```python
from whoosh_modern.data_sources.sqlalchemy_ds import SQLAlchemySource
from sqlalchemy import create_engine

engine = create_engine("postgresql://user:pass@localhost/mydb")
source = SQLAlchemySource(
    engine=engine,
    query="SELECT * FROM articles",
    incremental_field="updated_at",
    id_field="id",
)
```

### PeeweeSource

```python
from whoosh_modern.data_sources.peewee_ds import PeeweeSource
from peewee import SqliteDatabase

db = SqliteDatabase("mydb.db")
source = PeeweeSource(
    database=db,
    model=MyArticleModel,
    fields=["id", "title", "content"],
)
```

### TortoiseSource (async)

```python
from whoosh_modern.data_sources.tortoise_ds import TortoiseSource

source = TortoiseSource(
    model="myapp.models.Article",
    fields=["id", "title", "content"],
)
```

---

## RESTSource

`RESTSource` récupère les documents depuis des API REST avec pagination et authentification.

### Utilisation de base

```python
from whoosh_modern.data_sources.rest import RESTSource

source = RESTSource(
    url="https://api.example.com/v2/products",
    method="GET",
    headers={"Authorization": "Bearer your_token"},
    pagination="page",
    page_size=50,
)

schema = source.discover_schema()
for doc in source.iter_documents():
    print(doc["name"], doc["price"])
```

### Stratégies de pagination

| Stratégie | Paramètres |
|----------|-----------|
| `page` | `?page=N&size=M` |
| `offset` | `?offset=N&limit=M` |
| `cursor` | `?cursor=XYZ&size=M` |

### Authentification

```python
# Bearer token
source = RESTSource(
    url="https://api.example.com/data",
    headers={"Authorization": "Bearer your_token"},
)

# API key
source = RESTSource(
    url="https://api.example.com/data",
    headers={"X-API-Key": "your_api_key"},
)

# Basic auth
import base64
creds = base64.b64encode(b"user:pass").decode()
source = RESTSource(
    url="https://api.example.com/data",
    headers={"Authorization": f"Basic {creds}"},
)
```

### Document Path

Pour les réponses API imbriquées :

```python
source = RESTSource(
    url="https://api.example.com/api/v2/products",
    document_path="data.results",
    pagination="page",
)
```

---

## GraphQLSource

```python
from whoosh_modern.data_sources.graphql import GraphQLSource

source = GraphQLSource(
    url="https://api.example.com/graphql",
    query="""
        query GetProducts($limit: Int!, $offset: Int!) {
            products(limit: $limit, offset: $offset) {
                id
                name
                price
            }
        }
    """,
    pagination="offset",
    page_size=100,
    headers={"Authorization": "Bearer your_token"},
)
```

---

## Sources de fichiers

### FastCSVSource

```python
from whoosh_modern.data_sources.fast_csv import FastCSVSource

source = FastCSVSource(
    file_path="data/products.csv",
    id_field="id",
    incremental_field="updated_at",
)
```

### JSONSource

```python
from whoosh_modern.data_sources.json import JSONSource

source = JSONSource(file_path="data/products.json")
# ou fichier JSONL
source = JSONSource(file_path="data/logs.jsonl", format="jsonl")
```

### ParquetSource

```python
from whoosh_modern.data_sources.parquet_ds import ParquetSource

source = ParquetSource(
    file_path="data/large_dataset.parquet",
    engine="pyarrow",
    batch_size=1000,
)
```

### PandasSource

```python
from whoosh_modern.data_sources.pandas_ds import PandasSource
import pandas as pd

df = pd.read_csv("data/products.csv")
source = PandasSource(dataframe=df)
```

### PolarsSource

```python
from whoosh_modern.data_sources.polars_ds import PolarsSource
import polars as pl

df = pl.read_csv("data/products.csv")
source = PolarsSource(dataframe=df)
```

---

## DataSourceConfig

Pour une configuration programmatique :

```python
from whoosh_modern.data_sources.config import DataSourceConfig

config = DataSourceConfig(
    source_type="sql",
    connection="sqlite:///mydb.db",
    query="SELECT * FROM products",
    id_field="id",
    incremental_field="updated_at",
    mapping={"db_title": "title"},
    exclude=["description_long"],
)

source = config.create_source()
```

### Sources de données disponibles

| Classe | Type | Dépendances |
|-------|------|-------------|
| `SQLSource` | Bases SQL | `sqlite3` (stdlib) |
| `SQLAlchemySource` | SQLAlchemy | `sqlalchemy` |
| `RESTSource` | API REST | aucune (stdlib `urllib`) |
| `GraphQLSource` | API GraphQL | aucune (stdlib `urllib`) |
| `FastCSVSource` | Fichiers CSV | aucune |
| `JSONSource` | JSON/JSONL | aucune |
| `ParquetSource` | Parquet | `pyarrow` ou `pandas` |
| `PandasSource` | DataFrames pandas | `pandas` |
| `PolarsSource` | DataFrames Polars | `polars` |
| `PeeweeSource` | ORM Peewee | `peewee` |
| `TortoiseSource` | ORM Tortoise | `tortoise-orm` |
| `PydanticSource` | Modèles Pydantic | `pydantic` |


## DOCUMENT (FR): Facets

# Gestionnaire de facettes

`FacetManager` gère la configuration des facettes pour un schéma Whoosh.

## Usage basique

```python
from whoosh.fields import Schema, TEXT, NUMERIC, BOOLEAN
from whoosh_modern.facets import FacetManager, TermsFacet, RangeFacet

schema = Schema(
    title=TEXT(stored=True),
    category=TEXT(sortable=True),
    price=NUMERIC(),
    active=BOOLEAN(),
)

manager = FacetManager(schema)
```

## Auto-découverte

| Type Whoosh | Facette auto-découverte |
|-------------|-------------------------|
| `KEYWORD`, `BOOLEAN`, `ID` | `TermsFacet` |
| `NUMERIC` | `RangeFacet` |
| `DATETIME` | `DateRangeFacet` |

```python
facets = manager.get_facets()
manager.is_facetable("category")  # True
manager.is_facetable("title")     # False
```

## Remplacement manuel

```python
manager.set_manual_override("category", {"type": "terms", "limit": 50})
manager.set_manual_override("price", {"type": "range", "buckets": ["0-10", "10-50"]})
```

## Statistiques

```python
stats = manager.get_facet_stats()
# {"total_fields": 4, "auto_discovered_facets": 2, ...}
```


## DOCUMENT (FR): Fastapi Search

# Intégration FastAPI

Un service FastAPI complet exposant la recherche Whoosh-NG via HTTP.

## 1. Installation

```bash
pip install "whoosh-ng[api]" fastapi uvicorn
```

## 2. Créer l'index

```python
# setup_index.py
import json
from whoosh import index
from whoosh.fields import Schema, TEXT, ID

schema = Schema(
    id=ID(stored=True, unique=True),
    title=TEXT(stored=True),
    content=TEXT,
)

ix = index.create_in("docs_index", schema)

with ix.writer() as w:
    for doc in json.load(open("documents.json")):
        w.add_document(
            id=doc["id"],
            title=doc["title"],
            content=doc["content"],
        )
    w.commit()
```

## 3. Service REST

```python
# main.py
from fastapi import FastAPI, Query
from typing import Optional
from whoosh import index
from whoosh.qparser import QueryParser
from whoosh_fastapi import create_app

ix = index.open_dir("docs_index")

# Option A: Utiliser l'aide
app = create_app(ix, prefix="/api/v1")

# Option B: endpoints manuels
# app = FastAPI(title="Document Search API", version="1.0.0")
#
# @app.get("/api/v1/health")
# async def health():
#     return {"status": "ok"}
#
# @app.post("/api/v1/search")
# async def search(q: str = Query(...), limit: int = 10):
#     with ix.searcher() as s:
#         parser = QueryParser("content", ix.schema)
#         results = s.search(parser.parse(q), limit=limit)
#         return {"hits": [dict(h) for h in results], "total": len(results)}

if __name__ == "__main__":
    import uvicorn
    uvicorn.run(app, host="0.0.0.0", port=8000)
```

## 4. Démarrer le serveur

```bash
uvicorn main:app --reload --port 8000
```

## 5. Tester l'API

```bash
# Vérification de santé
curl http://localhost:8000/api/v1/health

# Recherche
curl -X POST http://localhost:8000/api/v1/search \
  -H "Content-Type: application/json" \
  -d '{"q": "python recherche"}'

# Get document by ID
curl http://localhost:8000/api/v1/documents/doc1

# Autocomplétion
curl "http://localhost:8000/api/v1/autocomplete?q=py"
```

## 6. Indexation en lot

```python
# Ajouter à main.py pour l'indexation dynamique
from fastapi import FastAPI
from whoosh.writing import BufferedWriter

@app.post("/api/v1/index")
async def index_docs(docs: list[dict]):
    with BufferedWriter(ix, period=30, limit=50) as w:
        for doc in docs:
            w.add_document(**doc)
    return {"indexed": len(docs)}
```

## Points clés

- `create_app()` de `whoosh_fastapi` fournit les endpoints `/health`, `/search` et `/autocomplete`.
- Tous les appels bloquants s'exécutent hors boucle d'événements via `run_sync`.
- Utilisez `BufferedWriter` pour l'indexation en masse.
- `WhooshFastAPI` classe offre une enregistrement par endpoint pour les intégrations personnalisées.


## DOCUMENT (FR): Middleware Pipeline

# Pipeline de middleware

Le pipeline de middleware enveloppe les opérations avec des préoccupations transversales : nouvelle tentative, journalisation, etc.

## Architecture

```python
from whoosh_modern.middleware import Middleware, MiddlewarePipeline, RetryMiddleware, LoggingMiddleware

pipeline = MiddlewarePipeline(
    RetryMiddleware(attempts=3, backoff="exponential"),
    LoggingMiddleware(),
)

result = pipeline.execute(my_operation)
```

## RetryMiddleware

```python
from whoosh_modern.middleware import RetryMiddleware

retry = RetryMiddleware(attempts=3, backoff="exponential")

@retry.wrap
def operation():
    return fetch_data()
```

Stratégies de backoff :
- `"exponential"` : 1s, 2s, 4s, 8s...
- `"linear"` : 1s, 2s, 3s, 4s...

## LoggingMiddleware

```python
from whoosh_modern.middleware import LoggingMiddleware
import logging

logger = logging.getLogger("benchmark")
logging_mw = LoggingMiddleware(logger=logger)

@logging_mw.wrap
def tracked():
    return fetch_data()
```


## DOCUMENT (FR): Middleware

# Exemples de Middleware

Des exemples pratiques pour construire et utiliser les middleware de Whoosh-NG.

## 1. Middleware de Logging

```python
from whoosh.middleware.base import Middleware
from whoosh.middleware.context import MiddlewareContext

class LoggingMiddleware(Middleware):
    def before_search(self, context: MiddlewareContext) -> MiddlewareContext:
        print(f"[RECHERCHE] Requête: {context.query}")
        return context

    def after_search(self, context: MiddlewareContext) -> MiddlewareContext:
        if context.results is not None:
            print(f"[RÉSULTATS] {len(context.results)} résultats trouvés")
        return context
```

## 2. Middleware de Metrics

```python
from whoosh.middleware.base import Middleware
from whoosh.middleware.context import MiddlewareContext

class MetricsMiddleware(Middleware):
    def __init__(self) -> None:
        self._metrics = {}

    def after_index(self, context: MiddlewareContext) -> MiddlewareContext:
        self._metrics["documents_indexés"] = self._metrics.get("documents_indexés", 0) + 1
        return context

    def after_search(self, context: MiddlewareContext) -> MiddlewareContext:
        self._metrics["recherches_executées"] = self._metrics.get("recherches_executées", 0) + 1
        return context

    def get_metrics(self) -> dict:
        return dict(self._metrics)
```

## 3. Middleware de Cache

```python
from whoosh.middleware.base import Middleware
from whoosh.middleware.context import MiddlewareContext

class SearchCacheMiddleware(Middleware):
    def __init__(self) -> None:
        self._cache = {}

    def before_search(self, context: MiddlewareContext) -> MiddlewareContext:
        if context.query and str(context.query) in self._cache:
            context.metadata["_résultat_cache"] = self._cache[str(context.query)]
        return context

    def after_search(self, context: MiddlewareContext) -> MiddlewareContext:
        if context.query and context.results is not None:
            self._cache[str(context.query)] = context.results
        return context
```

## 4. Appliquer un Middleware

```python
from whoosh.middleware.chain import MiddlewareChain
from whoosh.middleware.integration import apply_middleware_to_searcher

chain = MiddlewareChain([
    LoggingMiddleware(),
    MetricsMiddleware(),
])

with ix.searcher() as base_searcher:
    searcher = apply_middleware_to_searcher(base_searcher, chain.middlewares)
    results = searcher.search(query)
```

## Points clés

| Hook | Phase | Description |
|------|-------|-------------|
| `startup` | Init | Appelé une fois à l'initialisation |
| `shutdown` | Nettoyage | Appelé à la fermeture |
| `before_index` | Indexation | Avant l'ajout d'un document |
| `after_index` | Indexation | Après l'ajout d'un document |
| `before_delete` | Suppression | Avant la suppression |
| `after_delete` | Suppression | Après la suppression |
| `before_search` | Recherche | Avant l'exécution de la requête |
| `after_search` | Recherche | Après le retour des résultats |
| `on_error` | Erreur | En cas d'exception |
| `on_commit` | Commit | Après writer.commit() |


## DOCUMENT (FR): Movie Search

# Application de Recherche de Films

Exemple complet montrant comment créer une petite **application de recherche de films** avec Whoosh‑NG : conception du schéma, indexation à partir d’un fichier JSON, recherche facettée, mise en évidence et filtrage.

## 1. Schéma

```python
from whoosh.fields import Schema, TEXT, ID, KEYWORD, NUMERIC

schema = Schema(
    id=ID(stored=True, unique=True),
    title=TEXT(stored=True),
    director=TEXT(stored=True),
    genre=KEYWORD(stored=True, commas=True, scorable=True),
    year=NUMERIC(int, stored=True),
    synopsis=TEXT,
)
```

## 2. Indexer les documents

```python
import json
import shutil
from whoosh import index

shutil.rmtree("movies", ignore_errors=True)
ix = index.create_in("movies", schema)

movies = json.load(open("movies.json"))

with ix.writer() as w:
    for m in movies:
        w.add_document(
            id=str(m["id"]),
            title=m["title"],
            director=m["director"],
            genre=",".join(m["genres"]),
            year=m["year"],
            synopsis=m["synopsis"],
        )
    w.commit()
```

Fichier `movies.json` :

```json
[
  {
    "id": 1,
    "title": "Blade Runner",
    "director": "Ridley Scott",
    "genres": ["sci-fi", "thriller"],
    "year": 1982,
    "synopsis": "Un chasseur de replicants questionne l’humanité dans un futur pluvieux."
  }
]
```

## 3. Recherche avec facettes et mise en évidence

```python
from whoosh import index
from whoosh.qparser import MultifieldParser
from whoosh.sorting import FieldFacet

ix = index.open_dir("movies")

qp = MultifieldParser(["title", "synopsis", "director"], ix.schema)

with ix.searcher() as s:
    q = qp.parse("sci-fi")

    results = s.search(
        q,
        sortedby=FieldFacet("year", reverse=True),
        groupedby=FieldFacet("genre", allow_overlap=True),
        limit=20,
    )

    for hit in results:
        print(hit["title"], hit["year"], "|", round(hit.score, 2))
        print("  ", hit.highlights("synopsis"))

    print("\nGenres:", results.groups("genre"))
```

## 4. Filtrage

Filtrer les films de science-fiction après 1990 :

```python
from whoosh import index
from whoosh.qparser import QueryParser
from whoosh.query import Term, And, NumericRange

ix = index.open_dir("movies")
qp = QueryParser("synopsis", ix.schema)

with ix.searcher() as s:
    user_q = qp.parse("future")
    filters = And([
        Term("genre", "sci-fi"),
        NumericRange("year", 1990, None),
    ])
    results = s.search(user_q, filter=filters)
    for hit in results:
        print(hit["title"], hit["year"])
```

## Points clés

- `KEYWORD(commas=True)` stocke des champs multi-valeurs facetables.
- `MultifieldParser` recherche sur plusieurs champs avec des boosts optionnels.
- `FieldFacet` permet les facettes et le tri.
- `hit.highlights()` renvoie des fragments mis en évidence prêts à afficher.


## DOCUMENT (FR): Plugin Dev

# Développement de Plugins

Guide complet pour créer, enregistrer et tester vos propres plugins Whoosh-NG.

## 1. Classe de base des Plugins

Tous les plugins héritent de `whoosh.plugins.base.Plugin` :

```python
from whoosh.plugins.base import Plugin

class MyPlugin(Plugin):
    name = "my_plugin"
    version = "1.0.0"
    depends_on = []
    conflicts_with = []
    priority = 0
    middleware = []

    def register(self, manager):
        """Appelé quand le plugin est chargé."""
        manager.register("my_handler", MyHandler())

    def register_hooks(self):
        """Enregistrer les hooks avec le décorateur hookimpl."""
        from whoosh.hooks import hookimpl, register_hook

        @hookimpl
        def on_search(request, response):
            pass

        register_hook("on_search", hookimpl(on_search))
```

## 2. Enregistrement d’un Plugin

### Enregistrement manuel

```python
from whoosh.plugins.manager import PluginManager

plugin = MyPlugin()
PluginManager.register(plugin)
```

### Auto-découverte via Entry Points

Dans `pyproject.toml` :

```toml
[project]
name = "whoosh-ng-my-plugin"

[project.entry-points."whoosh_ng.plugins"]
my_plugin = "my_package.plugin:MyPlugin"
```

Auto-chargement :

```python
from whoosh.plugins.manager import PluginManager

PluginManager.load_plugins()
```

## 3. Exemple de Plugin Provider

```python
from whoosh.plugins.base import Plugin
from whoosh.registry import VectorRegistry

class MyVectorProvider:
    def search(self, query_vector, k=10):
        return [{"doc_id": "1", "score": 0.95}]

class MyVectorPlugin(Plugin):
    name = "my_vector"
    version = "1.0.0"

    def register(self, manager):
        provider = MyVectorProvider()
        VectorRegistry.register("my_vector", provider, self.name)
```

## 4. Tester son Plugin

```python
import pytest
from whoosh.plugins.manager import PluginManager

class TestMyPlugin:
    def test_register(self):
        plugin = MyPlugin()
        manager = PluginManager()
        plugin.register(manager)
        assert "my_handler" in manager._plugins

    def test_entry_point(self):
        manager = PluginManager()
        manager.register(MyPlugin())
        assert "my_plugin" in manager.list_enabled()
```

## 5. API du PluginManager

```python
from whoosh.plugins.manager import PluginManager

manager = PluginManager()
manager.register(MyPlugin())
manager.enable("my_plugin")
manager.disable("my_plugin")
manager.list_plugins()
manager.list_enabled()
plugin = manager.get("my_plugin")
```

## 6. Plugins Intégrés

- `whoosh_modern.vector` - Recherche vectorielle (NumPy)
- `whoosh_modern.autocomplete` - Autocomplétion par index inversé
- `whoosh_fastapi` - Endpoints REST FastAPI

```python
from whoosh.plugins.manager import PluginManager
from whoosh_modern.vector.plugin import VectorPlugin
from whoosh_modern.autocomplete.plugin import AutocompletePlugin

PluginManager.load_plugins()
```


## DOCUMENT (FR): Schema Discovery

# Découverte de schéma

La découverte de schéma infère un schéma Whoosh à partir des métadonnées de résultats ou d'échantillons de documents.

## Depuis les métadonnées de colonnes

```python
from whoosh_modern.schema_discovery import SchemaDiscovery
import sqlite3

conn = sqlite3.connect("benchmark/benchmark_data.db")
cursor = conn.cursor()
cursor.execute("PRAGMA table_info(reuters_articles)")
columns = [(row[1], row[2]) for row in cursor.fetchall()]

schema = SchemaDiscovery.from_result_set(columns)
```

## Depuis des échantillons de documents

```python
from whoosh_modern.data_sources.sql import SQLSource

source = SQLSource(connection=conn, query="SELECT * FROM reuters_articles LIMIT 10")
docs = list(source.iter_documents())[:10]
schema = SchemaDiscovery.from_sample(docs)
```

## Détection du champ ID

```python
id_field = SchemaDiscovery.detect_id_field(dict(schema))
```

## Détection des doublons

`from_result_set` lève `SchemaDiscoveryError` sur les noms de colonnes dupliqués.


## DOCUMENT (FR): Search Models

# Modèles de recherche

Exemples de mapping automatique de modèles Python vers des schémas Whoosh.

## Dataclass

```python
from dataclasses import dataclass
from whoosh.fields import Schema, TEXT, NUMERIC
from whoosh_modern.models import ModelIndex
import tempfile
import shutil

@dataclass
class Book:
    title: str
    year: int
    tags: list[str] | None = None

idx = ModelIndex(Book)
print(idx.schema)
```

## Pydantic

```python
from pydantic import BaseModel
from whoosh_modern.models import register_model

class BookModel(BaseModel):
    title: str
    year: int
    tags: list[str] | None = None

idx = register_model(BookModel)
schema = idx.schema
```

## SQLAlchemy

```python
from sqlalchemy import Column, Integer, String
from whoosh_modern.models import register_model

class BookSQL:
    __tablename__ = "book"
    title = Column(String, info={"search": {"fulltext": True, "stored": True}})
    year = Column(Integer, info={"search": {"sortable": True}})

idx = register_model(BookSQL)
schema = idx.schema
```

## SQLModel

```python
from sqlmodel import SQLModel, Field
from whoosh_modern.models import register_model

class Book(SQLModel, table=True):
    id: int = Field(primary_key=True)
    title: str = Field(sa_column_kwargs={"info": {"search": {"fulltext": True}}})
    year: int

idx = register_model(Book)
schema = idx.schema
```

## msgspec

```python
import msgspec
from whoosh_modern.models import register_model

class Book(msgspec.Struct):
    title: str = msgspec.field(metadata={"search": {"fulltext": True}})
    year: int

idx = register_model(Book)
schema = idx.schema
```

## Indexation de documents

```python
from whoosh import index

tmp = tempfile.mkdtemp()
ix = index.create_in(tmp, schema)

with ix.writer() as w:
    book = Book(title="Guide Whoosh", year=2024, tags=["python", "recherche"])
    doc = idx.to_whoosh_document(book)
    w.add_document(**doc)
    w.commit()
```

## Auto-indexation avec AutoIndexer

```python
from whoosh_modern.models import AutoIndexer

auto = AutoIndexer(ix, on_error="raise")
auto.register(Book)

# Indexer une instance unique
book = Book(title="Nouveau livre", year=2024, tags=["python"])
auto.index(book)

# Supprimer par ID
auto.remove(book)

# Versions asynchrones
await auto.index_async(book)
await auto.remove_async(book)
```

Pour les modèles SQLAlchemy, `AutoIndexer` se connecte automatiquement aux événements `after_insert`, `after_update` et `after_delete`.

## Nettoyage

```python
shutil.rmtree(tmp)
```


## DOCUMENT (FR): Search View

# SearchView

`SearchView` intègre une source de données avec l'indexation Whoosh.

## Usage basique

```python
from whoosh_modern.views import SearchView
from whoosh_modern.data_sources.sql import SQLSource
import sqlite3

conn = sqlite3.connect("benchmark/benchmark_data.db")
source = SQLSource(
    connection=conn,
    query="SELECT * FROM reuters_articles",
    incremental_field="article_date",
    id_field="id",
)

view = SearchView(
    name="reuters",
    source=source,
)

# Créer l'index
ix = view.build("indexdir")
```

## Rafraîchissement incrémental

```python
# Réindexation complète
count = view.reindex()

# Rafraîchissement incrémental
count = view.refresh()
```

## Validation

```python
results = view.validate()
for result in results:
    print(f"Niveau {result.level}: {'PASS' if result.passed else 'FAIL'}")
```

## Mode strict

```python
view = SearchView(
    name="strict",
    source=source,
    strict=True,  # Lever ValidationError en cas d'échec
)
```


## DOCUMENT (FR): Search

# Recherche

Exemples concrets d'interrogation d'un index Whoosh-NG.

> **Scénario** : Vous avez indexé un catalogue de livres (voir `basic-indexing.md`).
> Les exemples ci-dessous montrent les patterns de recherche produit.

## Prérequis

L'index `book_index/` contient ce schéma :

```python
from whoosh.fields import Schema, TEXT, ID, KEYWORD, NUMERIC

schema = Schema(
    isbn=ID(stored=True, unique=True),
    title=TEXT(stored=True),
    author=TEXT(stored=True),
    content=TEXT,
    genre=KEYWORD(stored=True, commas=True),
    published_year=NUMERIC(int, stored=True, sortable=True),
    rating=NUMERIC(float, stored=True, sortable=True),
)
```

## 1. Recherche basique — « Livres sur Python »

```python
from whoosh import index
from whoosh.qparser import QueryParser

ix = index.open_dir("book_index")

with ix.searcher() as s:
    qp = QueryParser("content", ix.schema)
    q = qp.parse("python")

    results = s.search(q, limit=10)
    for hit in results:
        print(f"{hit['title']} par {hit['author']} — score={hit.score:.2f}")
```

## 2. Recherche multi-champs avec boosts

```python
from whoosh.qparser import MultifieldParser

ix = index.open_dir("book_index")
qp = MultifieldParser(
    ["title", "author", "content"],
    ix.schema,
    fieldboosts={"title": 3.0, "author": 2.0, "content": 1.0},
)

q = qp.parse("clean code")

with ix.searcher() as s:
    results = s.search(q, limit=10)
    for hit in results:
        print(f"{hit['title']} — {hit['author']}")
```

## 3. Pagination — « Page 3 des résultats »

```python
ix = index.open_dir("book_index")
qp = QueryParser("content", ix.schema)
q = qp.parse("machine learning")

with ix.searcher() as s:
    page = s.search_page(q, 3, pagelen=15)

    print(f"Page {page.number} / {page.pagecount} ({page.total} résultats)")
    for hit in page:
        print(f"  {hit['title']}")
```

## 4. Tri et filtres — « Sci-fi noté ≥4 après 2010 »

```python
from whoosh.query import Term, And, NumericRange
from whoosh.sorting import FieldFacet

ix = index.open_dir("book_index")
qp = QueryParser("content", ix.schema)
q = qp.parse("space")

with ix.searcher() as s:
    filters = And([
        Term("genre", "sci-fi"),
        NumericRange("published_year", 2010, None),
    ])

    results = s.search(
        q,
        filter=filters,
        sortedby=FieldFacet("rating", reverse=True),
        limit=20,
    )
    for hit in results:
        print(f"{hit['title']} ({hit['published_year']}) — note: {hit['rating']}")
```

## 5. Mise en évidence — « Où la requête correspond »

```python
ix = index.open_dir("book_index")
qp = QueryParser("content", ix.schema)
q = qp.parse("réseaux de neurones")

with ix.searcher() as s:
    results = s.search(q, limit=5)

    for hit in results:
        snippet = hit.highlights("content", top=2)
        print(f"{hit['title']}:")
        print(f"  {snippet}")
```

## 6. Recherche par plage — « Livres publiés en 2023 »

```python
from whoosh.query import NumericRange

ix = index.open_dir("book_index")

with ix.searcher() as s:
    q = NumericRange("published_year", 2023, 2023)
    results = s.search(q)
    print(f"{results.total} livres publiés en 2023")
```

## 7. Recherche par préfixe — « Titres commençant par "Deep" »

```python
from whoosh.query import Prefix

ix = index.open_dir("book_index")

with ix.searcher() as s:
    q = Prefix("title", "Deep")
    results = s.search(q)
    for hit in results:
        print(hit["title"])
```

## 8. Recherche facetée — « Grouper par genre »

```python
from whoosh.sorting import FieldFacet

ix = index.open_dir("book_index")
qp = QueryParser("content", ix.schema)
q = qp.parse("programming")

with ix.searcher() as s:
    results = s.search(q, groupedby=FieldFacet("genre"))

    for genre, group in results.groups("genre").items():
        print(f"{genre}: {len(group)} résultats")
```

## Points clés

- `QueryParser` analyse une chaîne en objet `Query`.
- `MultifieldParser` recherche plusieurs champs avec des boosts.
- `search_page()` gère la pagination.
- `filter` restreint les résultats sans affecter le score.
- `sortedby` trie par valeur de champ ou par score de pertinence.
- `hit.highlights()` renvoie des extraits mis en surbrillance.


## DOCUMENT (FR): Validation

# Framework de validation

Le framework de validation exécute 4 niveaux de vérifications sur une source de données avant l'indexation.

## Les 4 niveaux

| Niveau | Méthode | Objectif |
|-------|---------|---------|
| **Niveau 1** | `validate_structural(source)` | Disponibilité de la source, détection de schéma |
| **Niveau 2** | `validate_search(schema)` | Champs indexables, compatibilité des analyseurs |
| **Niveau 3** | `validate_performance(schema, source)` | Avertissements de performance (TEXT, etc.) |
| **Niveau 4** | `validate_runtime(source, sample_size)` | Itération d'échantillon, validation de types |

## Usage basique

```python
from whoosh_modern.validation import ValidationFramework, ValidationResult
from whoosh_modern.data_sources.sql import SQLSource
import sqlite3

conn = sqlite3.connect("benchmark/benchmark_data.db")
source = SQLSource(connection=conn, query="SELECT * FROM reuters_articles")

validator = ValidationFramework()
results: list[ValidationResult] = validator.validate(source)

for result in results:
    status = "PASS" if result.passed else "FAIL"
    print(f"Niveau {result.level}: {status}")
```

## Validation individuelle

```python
errors = validator.validate_structural(source)
errors = validator.validate_search(schema)
warnings = validator.validate_performance(schema, source)
errors = validator.validate_runtime(source, sample_size=100)
```


## DOCUMENT (FR): Vector Search

# Recherche Vectorielle avec Whoosh‑NG

Cet exemple montre comment activer la **recherche sémantique/vectorielle** avec l’option `vector` supplémentaire. Nous indexons des embeddings et effectuons une recherche de plus proches voisins (k-NN).

## 1. Installer les dépendances optionnelles

```bash
pip install "whoosh-ng[vector]" numpy
```

## 2. Schéma avec champ Vectoriel

```python
from whoosh.fields import Schema, TEXT, ID, VECTOR

schema = Schema(
    doc_id=ID(stored=True, unique=True),
    title=TEXT(stored=True),
    content=TEXT,
    embedding=VECTOR(stored=True, dim=128),
)
```

## 3. Indexer les vecteurs

```python
import numpy as np
from whoosh import index
import shutil

shutil.rmtree("vector_index", ignore_errors=True)
ix = index.create_in("vector_index", schema)

documents = [
    {"doc_id": "doc1", "title": "Python Basics", "content": "Learn Python programming."},
    {"doc_id": "doc2", "title": "Advanced Python", "content": "Deep dive into decorators."},
    {"doc_id": "doc3", "title": "Data Science", "content": "Pandas and NumPy."},
]

np.random.seed(42)
embeddings = {d["doc_id"]: np.random.rand(128).astype(np.float32) for d in documents}

with ix.writer() as w:
    for doc in documents:
        w.add_document(
            doc_id=doc["doc_id"],
            title=doc["title"],
            content=doc["content"],
            embedding=embeddings[doc["doc_id"]].tobytes(),
        )
    w.commit()
```

## 4. Recherche vectorielle avec NumpyProvider

```python
from whoosh_modern.vector.numpy_provider import NumpyProvider
from whoosh_modern.vector.plugin import VectorPlugin
from whoosh.plugins.manager import PluginManager

VectorPlugin().register(PluginManager())

provider = NumpyProvider()
for doc_id, vec in embeddings.items():
    provider.add([(doc_id, vec.tolist())])

query_vec = embeddings["doc1"]
hits = provider.search(query_vec, k=2)

for hit in hits:
    print(f"doc_id={hit.doc_id}, score={hit.score:.3f}")
```

## 5. Utiliser VectorField pour la sérialisation

```python
from whoosh.vector import VectorField

vf = VectorField(dimension=128, name="embedding")

values = [0.1, 0.2, 0.3, 0.4] + [0.0] * 124
raw = vf.vector_to_bytes(values)
restored = vf.bytes_to_vector(raw)
print(restored == tuple(values))  # True
```

## Points clés

- Installez avec `pip install whoosh-ng[vector]`.
- `VECTOR` stocke les octets bruts ; utilisez `VectorField` pour convertir.
- `NumpyProvider` implémente la similarité cosinus.
- Enregistrez le plugin via `VectorPlugin().register(manager)`.
- Utilisez `filter_ids` dans `provider.search()` pour restreindre les documents.


## DOCUMENT (FR): Autocomplete Fournisseurs

# Fournisseurs d'Autocomplétion

Module: `whoosh_modern.autocomplete`
Version: 2.0.0

Le module d'autocomplétion fournit plusieurs stratégies de fournisseurs pour la suggestion de requêtes et la recherche en tapant. Tous les fournisseurs implémentent une interface commune afin de pouvoir changer de stratégie à l'exécution. Les fournisseurs sont enregistrés via le `AutocompleteRegistry` et chargés via des entry points.

## Vue d'ensemble du module

```text
whoosh_modern.autocomplete
    ├── provider.py   # AutocompleteHit, AutocompleteProvider (Protocole)
    ├── ngram.py      # NGramProvider (basé sur des n-grammes de caractères)
    ├── edge_ngram.py # InvertedIndexAutocomplete (correspondance de préfixe par indice inversé)
    ├── fuzzy.py      # FuzzySuggestProvider (correspondance approximative via rapidfuzz)
    ├── factory.py    # create_autocomplete()
    └── plugin.py     # AutocompletePlugin (plugin via entry point)
```

## AutocompleteProvider (Classe de Base)

Située dans `whoosh_modern.autocomplete.provider` :

```python
from whoosh_modern.autocomplete.provider import AutocompleteProvider, AutocompleteHit

class MyProvider(AutocompleteProvider):
    def add(self, phrases: Iterable[str]) -> None:
        """Ajouter des phrases à l'index du fournisseur."""
        ...

    def search(self, prefix: str, limit: int = 10) -> list[AutocompleteHit]:
        """Retourner les suggestions d'autocomplétion pour le préfixe donné."""
        ...
```

### AutocompleteHit

Un objet de résultat simple retourné par les fournisseurs :

```python
class AutocompleteHit:
    def __init__(self, text: str, score: float) -> None:
        self.text = text    # La phrase correspondante
        self.score = score  # Score de pertinence (plus haut = mieux)
```

## Fournisseurs Intégrés

### InvertedIndexAutocomplete

Situé dans `whoosh_modern.autocomplete.edge_ngram`. Utilise une correspondance simple de préfixe contre une liste en mémoire :

```python
from whoosh_modern.autocomplete.edge_ngram import InvertedIndexAutocomplete

provider = InvertedIndexAutocomplete()
provider.add(["python", "pyramid", "pytorch", "java", "javascript"])

hits = provider.search("py", limit=5)
for hit in hits:
    print(f"{hit.text} (score: {hit.score})")
# Output:
# python (score: 0.45)
# pyramid (score: 0.43)
# pytorch (score: 0.43)
```

**Score** : Les correspondances exactes de préfixe obtiennent un bonus de 1.5x ; le score de base est `1.0 / (len(phrase) + 1)`.

### NGramProvider

Situé dans `whoosh_modern.autocomplete.ngram`. Construit un index de n-grammes de caractères pour une correspondance de sous-chaîne souple :

```python
from whoosh_modern.autocomplete.ngram import NGramProvider

provider = NGramProvider(n=3)
provider.add(["python programming", "java development", "rust language"])

hits = provider.search("pyt", limit=5)
for hit in hits:
    print(f"{hit.text} (score: {hit.score})")
```

**Paramètres :**

| Paramètre | Type | Défaut | Description                          |
|-----------|------|---------|--------------------------------------|
| `n`       | `int` | `3`     | Taille des n-grammes de caractères   |

**Fonctionnement** : Les n-grammes sont extraits de chaque phrase (en minuscules). Lors de la recherche, les n-grammes du préfixe sont comparés à l'index. Les phrases avec plus de n-grammes correspondants obtiennent des scores plus élevés.

### FuzzySuggestProvider

Situé dans `whoosh_modern.autocomplete.fuzzy`. Utilise `rapidfuzz` pour une correspondance approximative (fautes de frappe, correspondances partielles) :

```python
from whoosh_modern.autocomplete.fuzzy import FuzzySuggestProvider

# Nécessite: pip install whoosh-ng[fuzzy]
provider = FuzzySuggestProvider(max_distance=2, score_cutoff=50.0)
provider.add(["python", "pyramid", "pytorch", "java", "javascript"])

hits = provider.search("pythn", limit=5)  # Faute de frappe dans "python"
for hit in hits:
    print(f"{hit.text} (score: {hit.score})")
# Output: python (score: 0.95), ...
```

**Paramètres :**

| Paramètre       | Type  | Défaut  | Description                              |
|-----------------|-------|----------|------------------------------------------|
| `max_distance`  | `int` | `2`      | Distance d'édition maximale (réservé pour une utilisation future) |
| `score_cutoff`  | `float` | `50.0` | Score de similarité minimum (échelle 0-100)   |

**Note** : Nécessite `rapidfuzz` (`pip install whoosh-ng[fuzzy]`). Retourne `ImportError` si non installé.

## Fonction d'Usine

Située dans `whoosh_modern.autocomplete.factory` :

```python
from whoosh_modern.autocomplete import create_autocomplete

# Créer n'importe quel fournisseur par nom
provider = create_autocomplete("inverted")   # InvertedIndexAutocomplete
provider = create_autocomplete("ngram", n=3) # NGramProvider avec n personnalisé
provider = create_autocomplete("fuzzy", max_distance=2, score_cutoff=60.0)
```

**Fournisseurs disponibles :**

| Nom         | Classe                    | Dépendance Optionnelle |
|-------------|---------------------------|------------------------|
| `"inverted"`| `InvertedIndexAutocomplete` | Aucune               |
| `"ngram"`   | `NGramProvider`           | Aucune                |
| `"fuzzy"`   | `FuzzySuggestProvider`    | `rapidfuzz`           |

## Enregistrement dans AutocompleteRegistry

Les fournisseurs sont enregistrés dans `whoosh.registry.AutocompleteRegistry` (une instance de `Registry`) :

```python
from whoosh.registry import AutocompleteRegistry
from whoosh_modern.autocomplete import create_autocomplete

# Enregistrer un fournisseur
provider = create_autocomplete("ngram", n=3)
AutocompleteRegistry.register("ngram-suggester", provider, owner="my_app")

# Le récupérer plus tard
suggester = AutocompleteRegistry.get("ngram-suggester")

# Lister tous les fournisseurs enregistrés
print(AutocompleteRegistry.list_keys())
```

## AutocompletePlugin (Entry Point)

Situé dans `whoosh_modern.autocomplete.plugin`, c'est le plugin intégré enregistré via le groupe d'entry points `whoosh_ng.plugins` :

```python
from whoosh_modern.autocomplete.plugin import AutocompletePlugin

# Automatiquement chargé par PluginManager.load_plugins()
# Enregistre le fournisseur "inverted" dans AutocompleteRegistry
```

### Déclaration d'Entry Point

Dans `pyproject.toml` :

```toml
[project.entry-points."whoosh_ng.plugins"]
whoosh_autocomplete = "whoosh_modern.autocomplete.plugin:AutocompletePlugin"
```

### Détails du Plugin

```python
class AutocompletePlugin(Plugin):
    name = "whoosh_autocomplete"
    version = "3.0.0"

    def register(self, manager):
        # Enregistre InvertedIndexAutocomplete comme "inverted"
        AutocompleteRegistry.register(
            "inverted", create_autocomplete("inverted"), self.name
        )

    def register_hooks(self):
        # Enregistre un hook on_search (actuellement un no-op)
        from whoosh.hooks import hookimpl, register_hook
        register_hook("on_search", hookimpl(on_search))
```

## Exemples d'Utilisation

### Utilisation de Base

```python
from whoosh_modern.autocomplete import create_autocomplete

# Créer et peupler un fournisseur
provider = create_autocomplete("inverted")
provider.add([
    "python programming",
    "python tutorial",
    "java tutorial",
    "javascript framework",
])

# Rechercher des suggestions
hits = provider.search("py", limit=3)
for hit in hits:
    print(f"{hit.text}: {hit.score:.3f}")
```

### Correspondance Floue avec Tolérance aux Fautes

```python
from whoosh_modern.autocomplete import create_autocomplete

provider = create_autocomplete("fuzzy", score_cutoff=70.0)
provider.add(["python", "pytorch", "tensorflow", "keras"])

# Même avec une faute, les suggestions pertinentes sont retournées
hits = provider.search("pyton", limit=5)
for hit in hits:
    print(hit.text, hit.score)
```

### Correspondance par N-grammes pour les Mots Partiels

```python
from whoosh_modern.autocomplete import create_autocomplete

# Utiliser des n-grammes de taille 3 pour une meilleure correspondance de sous-chaînes
provider = create_autocomplete("ngram", n=3)
provider.add(["machine learning", "deep learning", "neural networks"])

# Trouve les phrases contenant les n-grammes de "machin"
hits = provider.search("machin", limit=5)
```

### Intégration avec la Recherche

```python
from whoosh_modern.autocomplete import create_autocomplete

# Construire le fournisseur d'autocomplétion
provider = create_autocomplete("inverted")
provider.add(["python", "java", "javascript", "go", "rust"])

# Utiliser dans un endpoint de recherche
def suggest(prefix: str, limit: int = 5):
    hits = provider.search(prefix, limit=limit)
    return [{"text": h.text, "score": h.score} for h in hits]

# Dans votre endpoint FastAPI/REST :
# GET /api/suggest?q=py&limit=5
# Response: [{"text": "python", "score": 0.45}, ...]
```

## Comparaison des Fournisseurs

| Fournisseur              | Correspondance       | Forces                    | Faiblesses                | Dépendance    |
|--------------------------|----------------------|---------------------------|---------------------------|---------------|
| `inverted`               | Préfixe              | Simple, rapide, pas de deps | Pas de tolérance aux fautes | Aucune          |
| `ngram`                  | Chevauchement n-gramme | Correspondance de sous-chaînes, flexible | Plus lent que préfixe     | Aucune        |
| `fuzzy`                  | Distance d'édition   | Tolérance aux fautes, flexible | Nécessite rapidfuzz    | `rapidfuzz`   |

## Installation

```bash
# Autocomplétion core (inverted + n-gram)
pip install whoosh-ng

# Avec correspondance floue
pip install whoosh-ng[fuzzy]

# Analyse moderne complète
pip install whoosh-ng[modern]
```

## Voir Aussi

- [Guide Système de Plugins](plugins-avances.md) — Enregistrement et découverte de plugins
- [Guide Middleware](middleware-pipeline.md) — Intégration du pipeline de middleware
- [Intégration des Providers](provider-integration.md) — Guide complet du pipeline pour tous les providers
- [API: Moderne](../api/modern.md) — Référence complète de l'API pour les extensions d'autocomplétion


## DOCUMENT (FR): Linguistique

# Synonymes & Linguistique

Module: `whoosh_modern.linguistics.synonyms`, `whoosh_modern.linguistics.stemmers`
Version: 2.0.0

Le module de linguistique fournit un moteur complet d'expansion de synonymes et des analyseurs linguistiques spécifiques à chaque langue. Il s'intègre au pipeline de middleware pour étendre les requêtes et les documents avec des synonymes à la fois à l'indexation et au moment de la recherche.

## Vue d'ensemble du module

```text
whoosh_modern.linguistics
    ├── synonyms/
    │   ├── provider.py       # Protocole SynonymProvider + StaticSynonymProvider
    │   ├── yaml_provider.py  # YAMLSynonymProvider
    │   ├── json_provider.py  # JSONSynonymProvider
    │   ├── store.py          # SQLiteSynonymStore
    │   ├── compiler.py       # SynonymCompiler
    │   ├── manager.py        # SynonymManager
    │   ├── middleware.py     # SynonymExpansionMiddleware
    │   └── languages.py      # LANG_SYNONYMS (FR/EN/DE/ES/IT)
    └── stemmers/
        └── __init__.py       # Analyseurs linguistiques (FR/EN/DE/ES/IT)
```

## Fournisseurs de Synonymes

### SynonymProvider (Protocole)

Le protocole de base que tous les fournisseurs de synonymes implémentent :

```python
from whoosh_modern.linguistics.synonyms import SynonymProvider

class MyProvider(SynonymProvider):
    def get_synonyms(self, word: str) -> list[str]:
        """Retourner les synonymes pour le mot donné."""
        ...

    def add_synonym(self, word: str, synonyms: list[str]) -> None:
        """Ajouter des synonymes pour le mot donné."""
        ...

    def remove_synonym(self, word: str, synonym: str) -> None:
        """Supprimer un synonyme pour le mot donné."""
        ...
```

### StaticSynonymProvider

Fournisseur de synonymes en mémoire basé sur un dictionnaire :

```python
from whoosh_modern.linguistics.synonyms import StaticSynonymProvider

provider = StaticSynonymProvider({
    "car": ["automobile", "vehicle", "auto"],
    "house": ["home", "residence"],
})

print(provider.get_synonyms("car"))  # ['automobile', 'vehicle', 'auto']
```

### YAMLSynonymProvider

Charge les synonymes depuis un fichier YAML :

```yaml
# synonyms.yaml
car:
  - automobile
  - vehicle
  - auto
house:
  - home
  - residence
```

```python
from whoosh_modern.linguistics.synonyms import YAMLSynonymProvider

# Nécessite: pip install pyyaml
provider = YAMLSynonymProvider("synonyms.yaml")
print(provider.get_synonyms("car"))  # ['automobile', 'vehicle', 'auto']
```

### JSONSynonymProvider

Charge les synonymes depuis un fichier JSON :

```json
{
    "car": ["automobile", "vehicle", "auto"],
    "house": ["home", "residence"]
}
```

```python
from whoosh_modern.linguistics.synonyms import JSONSynonymProvider

provider = JSONSynonymProvider("synonyms.json")
print(provider.get_synonyms("car"))
```

### SQLiteSynonymStore

Magasin de synonymes persistant basé sur SQLite :

```python
from whoosh_modern.linguistics.synonyms import SQLiteSynonymStore

store = SQLiteSynonymStore("synonyms.db")

# Opérations CRUD
store.add_synonym("car", ["automobile", "vehicle"])
print(store.get_synonyms("car"))  # ['automobile', 'vehicle']
store.remove_synonym("car", "automobile")
print(store.get_synonyms("car"))  # ['vehicle']
store.close()
```

### SynonymCompiler

Précompile les données de synonymes brutes en un format de recherche rapide :

```python
from whoosh_modern.linguistics.synonyms import SynonymCompiler

compiler = SynonymCompiler({"car": ["automobile", "vehicle"]})
compiler.add("house", ["home", "residence"])
compiler.merge({"book": ["publication", "work"]})

compiled = compiler.compile()
print(compiled)
# {'car': ['automobile', 'vehicle'], 'house': ['home', 'residence'], 'book': ['publication', 'work']}
```

## SynonymManager

Le `SynonymManager` est l'interface de haut niveau pour gérer les synonymes. Il encapsule un `StaticSynonymProvider` en interne et prend en charge l'import/export :

```python
from whoosh_modern.linguistics.synonyms import SynonymManager

manager = SynonymManager({"car": ["automobile", "vehicle"]})

# CRUD
manager.add_synonyms("house", ["home", "residence"])
print(manager.get_synonyms("house"))  # ['home', 'residence']
manager.remove_synonym("house", "home")

# Import depuis des sources externes
manager.import_yaml("synonyms.yaml")   # Nécessite PyYAML
manager.import_json("synonyms.json")

# Export
manager.export_json("output.json")
```

### Flux de Travail d'Import/Export

```python
# Import depuis YAML
manager = SynonymManager()
manager.import_yaml("my_synonyms.yaml")

# Export vers JSON (ex: pour migration ou sauvegarde)
manager.export_json("backup.json")
```

## Synonymes Linguistiques Prédéfinis

Le dictionnaire `LANG_SYNONYMS` contient des mappings de synonymes de démarrage pour cinq langues :

```python
from whoosh_modern.linguistics.synonyms import LANG_SYNONYMS

# Langues disponibles : fr, en, de, es, it
french_syns = LANG_SYNONYMS["fr"]
print(french_syns["voiture"])  # ['automobile', 'véhicule']

english_syns = LANG_SYNONYMS["en"]
print(english_syns["car"])  # ['automobile', 'vehicle']

# Initialiser un SynonymManager avec une langue
manager = SynonymManager(LANG_SYNONYMS["fr"])
```

| Langue   | Code | Exemple                               |
|----------|------|---------------------------------------|
| Français | `fr` | `"voiture": ["automobile", "véhicule"]` |
| Anglais  | `en` | `"car": ["automobile", "vehicle"]`    |
| Allemand | `de` | `"auto": ["wagen", "fahrzeug"]`       |
| Espagnol | `es` | `"coche": ["automóvil", "vehículo"]`  |
| Italien  | `it` | `"auto": ["automobile", "veicolo"]`   |

> **Note** : Ce sont des dictionnaires de démarrage minimaux pour la démonstration et les tests. Les déploiements de production devraient charger depuis des sources élaborées ou spécifiques au domaine.

## SynonymExpansionMiddleware

Intègre l'expansion de synonymes dans le pipeline de middleware. Elle étend à la fois les requêtes de recherche et les champs de documents indexés :

```python
from whoosh_modern.linguistics.synonyms import (
    SynonymManager,
    SynonymExpansionMiddleware,
)

# Créer un gestionnaire avec vos synonymes
manager = SynonymManager({
    "car": ["automobile", "vehicle"],
    "house": ["home", "residence"],
})

# Créer le middleware
middleware = SynonymExpansionMiddleware(manager)

# L'enregistrer auprès du PluginManager ou MiddlewareChain
from whoosh.plugins.manager import PluginManager
PluginManager._default.register_middleware("synonym", middleware)
```

### Fonctionnement

- **`before_search`** : Étend `context.query` en ajoutant les synonymes de chaque token
- **`before_index`** : Étend les valeurs de type chaîne dans `context.document` en ajoutant les synonymes

```python
# Avant : query = "car"
# Après :  query = "car automobile vehicle"

# Avant : document = {"title": "house for sale"}
# Après :  document = {"title": "house for sale home residence"}
```

## Analyseurs Linguistiques Spécifiques

Situés dans `whoosh_modern.linguistics.stemmers`, ces analyseurs combinent tokenisation, stemme et suppression des mots vides :

```python
from whoosh_modern.linguistics.stemmers import (
    EnglishAnalyzer,
    FrenchAnalyzer,
    GermanAnalyzer,
    SpanishAnalyzer,
    ItalianAnalyzer,
)

# Chaque analyseur est appelable et retourne une liste de tokens
analyzer = EnglishAnalyzer()
tokens = analyzer("The running cats")
# tokens sont stemmés: ["run", "cat"] (mots vides supprimés)
```

## Voir Aussi

- [Guide Stemmers](stemmers-fournisseurs.md) — Fournisseurs de stemmers et analyseurs linguistiques
- [Guide Middleware](middleware-pipeline.md) — Intégration du pipeline de middleware
- [Intégration des Providers](provider-integration.md) — Guide complet du pipeline pour tous les providers
- [API: Linguistique](../api/modern.md) — Référence complète de l'API


## DOCUMENT (FR): Plugins Avances

# Système de Plugins

Module: `whoosh.plugins.manager`
Version: 2.0.0

L'architecture à plugins de Whoosh-NG permet aux paquets externes d'étendre le moteur d'indexation, de recherche et d'analyse de texte. Les plugins sont découverts via les [entry points](https://docs.python.org/3/library/importlib.metadata.html#entry-points) Python déclarés dans `pyproject.toml` et gérés par le `PluginManager`.

## Vue d'ensemble de l'architecture

```text
PluginManager (singleton)
    ├── load_plugins(group)          # Auto-découverte depuis entry points
    ├── register(plugin)             # Enregistrement manuel
    ├── enable(name) / disable(name) # Activer/désactiver le cycle de vie
    ├── get(name) / list_plugins()   # Inspection
    ├── get_middleware_chain()       # Construire MiddlewareChain depuis middleware des plugins
    ├── register_datasource()        # Enregistrer un fournisseur de source de données
    ├── register_vector_provider()   # Enregistrer un fournisseur de vecteurs
    ├── register_middleware()        # Enregistrer une instance de middleware
    ├── register_embedding()         # Enregistrer un fournisseur d'embeddings
    ├── register_analyzer()          # Enregistrer un analyseur nommé
    └── register_query_rewriter()    # Enregistrer un réécrivant de requête
```

## Classes de Base des Plugins

### Plugin (ABC)

La classe de base pour tous les plugins. Les sous-classes définissent les attributs de classe et implémentent `register()` :

```python
from whoosh.plugins.manager import Plugin, PluginMetadata

class MyPlugin(Plugin):
    name = "my_plugin"
    version = "1.0.0"

    def register(self, manager: PluginManager) -> None:
        """Appelé quand le plugin est chargé ; enregistrer les fournisseurs ici."""
        manager.register_middleware("my_module.MyMiddleware", MyMiddleware())

    def register_hooks(self) -> None:
        """Enregistrer les hooks d'événements (optionnel)."""
        from whoosh.hooks import hookimpl, register_hook

        @hookimpl
        def on_search(request, response):
            pass
        register_hook("on_search", hookimpl(on_search))
```

### AnalyzerPlugin

Pour les plugins qui fournissent des tokenizers/analyseurs personnalisés :

```python
from whoosh.plugins.manager import AnalyzerPlugin

class MyAnalyzerPlugin(AnalyzerPlugin):
    name = "my_analyzer"

    def register(self, manager):
        manager.register_analyzer("my_analyzer", MyTokenizer())
```

### QueryRewritePlugin

Pour les plugins qui transforment les requêtes avant l'exécution :

```python
from whoosh.plugins.manager import QueryRewritePlugin

class SynonymRewriterPlugin(QueryRewritePlugin):
    name = "synonym_rewriter"

    def rewrite(self, query, searcher):
        # Retourner la requête modifiée
        return query
```

## PluginMetadata

Un dataclass décrivant les métadonnées du plugin :

| Champ         | Type              | Description                            |
|---------------|-------------------|----------------------------------------|
| `name`        | `str`             | Nom unique du plugin                   |
| `version`     | `str`             | Version sémantique                     |
| `depends_on`  | `list[str]`       | Noms des plugins requis                |
| `priority`    | `int`             | Priorité d'ordre de chargement (plus haut = plus tard) |
| `middleware`  | `list[str]`       | Chemins pointés vers les classes de middleware |

## Groupes d'Entry Points

Le `PluginManager` découvre les plugins depuis ces groupes d'entry points standards :

| Groupe                      | Utilisation                          |
|-----------------------------|--------------------------------------|
| `whoosh.plugins`            | Plugins généraux                     |
| `whoosh.datasources`        | Fournisseurs de sources de données   |
| `whoosh.vector.providers`   | Fournisseurs de similarité vectorielle |
| `whoosh.middlewares`        | Classes de middleware                 |
| `whoosh.embeddings`         | Fournisseurs de modèles d'embedding  |
| `whoosh.language`           | Analyseurs linguistiques             |
| `whoosh.apps`               | Usines d'applications (FastAPI, admin, etc.) |

## Créer et Déployer un Plugin

### Étape 1 : Définir la Classe du Plugin

```python
# my_plugin/plugin.py
from whoosh.plugins.manager import Plugin
from whoosh.registry import VectorRegistry

class MyVectorPlugin(Plugin):
    name = "my_vector"
    version = "1.0.0"
    depends_on = []
    conflicts_with = []
    priority = 0
    middleware = []

    def register(self, manager):
        """Enregistrer un fournisseur de vecteurs dans le VectorRegistry."""
        provider = MyCustomVectorProvider()
        VectorRegistry.register("my_vector", provider, owner=self.name)

    def register_hooks(self):
        """Enregistrer les hooks optionnels (ex: on_search, on_index)."""
        pass
```

### Étape 2 : Déclarer l'Entry Point

Dans votre `pyproject.toml` :

```toml
[project]
name = "whoosh-ng-my-vector"
version = "1.0.0"
dependencies = ["whoosh-ng>=2.0"]

[project.entry-points."whoosh_ng.plugins"]
my_vector = "my_plugin.plugin:MyVectorPlugin"
```

### Étape 3 : Installer et Vérifier

```bash
pip install -e .
```

```python
# Vérifier que le plugin est bien enregstré
from whoosh.plugins.manager import PluginManager

PluginManager.load_plugins()  # Découvre tous les entry points

manager = PluginManager._default
print(manager.list_plugins())
# ['whoosh_autocomplete', 'whoosh_vector', ..., 'my_vector']

# Vérifier le registre
from whoosh.registry import VectorRegistry
print(VectorRegistry.list_keys())
# ['my_vector', 'numpy']
```

## Enregistrement Manuel (Sans Entry Point)

Pour les tests ou un usage programmatique :

```python
from whoosh.plugins.manager import PluginManager

manager = PluginManager()
manager.register(MyVectorPlugin())
manager.enable("my_vector")
```

## Cycle de Vie d'un Plugin

```
1. Entry point découvert  ───►  2. register() appelé  ───►  3. register_hooks()
   │                               │                            │
   └── load_plugins(group)          └── register provider/     └── register_hook()
                                      middleware/analyzer
```

### Activation / Désactivation

```python
from whoosh.plugins.manager import PluginManager

manager = PluginManager._default

manager.enable("my_vector")    # Activer un plugin
manager.disable("my_vector")   # Désactiver un plugin
print(manager.list_enabled())  # Seuls les plugins activés
```

### Validation de Version

```python
# Vérifie si un plugin respecte une version minimale
ok = manager.validate_version("my_vector", "1.0.0")
print(ok)  # True si la version du plugin >= 1.0.0
```

### Détection de Conflits

```python
# Vérifie si deux plugins entrent en conflit
if manager.detect_conflicts("plugin_a", "plugin_b"):
    print("Ces plugins ne peuvent pas être chargés ensemble")
```

## API du PluginManager

### `PluginManager.load_plugins(group=None)`

Charge tous les plugins depuis les groupes d'entry points. Si `group` est `None`, charge tous les groupes standards (`STANDARD_GROUPS`).

### `PluginManager.register(plugin)`

Enregistre une instance de plugin. Appelle `plugin.register(self)` et `plugin.register_hooks()`. Supporte les méthodes `register()` asynchrones via `asyncio`.

### `PluginManager.get_middleware_chain()`

Construit et retourne une `MiddlewareChain` à partir de tous les plugins qui déclarent une liste `middleware`. Les classes de middleware sont importées et instanciées par chemin pointé.

### Méthodes d'Enregistrement dans les Registres

| Méthode                       | Description                          |
|-------------------------------|--------------------------------------|
| `register_analyzer(name, analyzer)` | Enregistrer un analyseur nommé   |
| `register_datasource(name, datasource)` | Enregistrer une source de données  |
| `register_vector_provider(name, provider)` | Enregistrer un fournisseur de vecteurs |
| `register_middleware(name, middleware)` | Enregistrer une instance de middleware |
| `register_embedding(name, embedding)` | Enregistrer un fournisseur d'embeddings |
| `register_query_rewriter(plugin)` | Enregistrer un plugin réécrivant des requêtes |

### Méthodes de Recherche

| Méthode                       | Retourne                          |
|-------------------------------|----------------------------------|
| `get(name)`                   | Instance `Plugin`                |
| `list_plugins()`              | Noms de tous les plugins enregistrés |
| `list_enabled()`              | Noms des plugins activés         |
| `get_analyzer(name)`          | Analyseur callable               |
| `list_analyzers()`            | Noms des analyseurs enregistrés  |
| `list_datasources()`          | Noms des sources de données enregistrées |
| `list_vector_providers()`     | Noms des fournisseurs de vecteurs enregistrés |
| `list_middlewares()`          | Noms des middleware enregistrés  |
| `list_embeddings()`           | Noms des fournisseurs d'embeddings enregistrés |
| `list_query_rewriters()`      | Noms des réécrivants enregistrés |

## Plugins Intégrés

| Plugin            | Module                  | Groupe d'Entry Point       |
|-------------------|-------------------------|----------------------------|
| `whoosh_autocomplete` | `whoosh_modern.autocomplete.plugin` | `whoosh.plugins` |
| `whoosh_vector`   | `whoosh_modern.vector.plugin`      | `whoosh.plugins` |
| `whoosh_fastapi`  | `whoosh_fastapi`                  | `whoosh.apps` |
| `whoosh_observability` | `whoosh.middleware.metrics`  | `whoosh.middlewares` |
| `whoosh_admin`    | `whoosh_admin`                   | `whoosh.apps` |

## Bonnes Pratiques

1. **Responsabilité unique** : Un plugin, une fonctionnalité
2. **Déclarez les dépendances** : Utilisez `depends_on` pour les plugins requis
3. **Version sémantique** : Incrémentez la version pour les changements d'API
4. **Degradation gracieuse** : Vérifiez les dépendances optionnelles dans `register()`
5. **Pas d'effets de bord dans `__init__`** : Toute l'initialisation dans `register()`
6. **Nettoyage** : Si applicable, fournissez une logique de teardown

## Voir Aussi

- [Guide Middleware](middleware-pipeline.md) — Pipeline hooks et middleware personnalisé
- [Intégration des Providers](provider-integration.md) — Guide complet du pipeline pour tous les providers
- [Exemple: Développement de Plugin](../examples/plugin-dev.md) — Tutoriel pas à pas
- [API: Plugins](../api/plugins.md) — Référence complète de l'API


## DOCUMENT (FR): Stemmers Fournisseurs

# Fournisseurs de Stemmers

Module: `whoosh_modern.analysis.stemmer_providers`, `whoosh_modern.analysis.stemming_analyzer`, `whoosh_modern.linguistics.stemmers`
Version: 2.0.0

Le système de fournisseurs de stemmers donne un contrôle flexible sur le backend de stemming utilisé pour l'analyse de texte. Il prend en charge la détection automatique, la sélection explicite du backend et l'enregistrement de stemmers personnalisés — le tout avec une API propre de type plugin.

## Vue d'ensemble du module

```text
whoosh_modern.analysis
    ├── stemmer_providers.py   # Protocole StemmerProvider, fournisseurs Internal/PyStemmer, register_stemmer, get_stemmer
    └── stemming_analyzer.py   # StemmingAnalyzer amélioré avec support plugin

whoosh_modern.linguistics.stemmers
    └── __init__.py            # Analyseurs linguistiques (FR/EN/DE/ES/IT)
```

## Protocole StemmerProvider

Situé dans `whoosh_modern.analysis.stemmer_providers` :

```python
from whoosh_modern.analysis.stemmer_providers import StemmerProvider

class MyStemmer(StemmerProvider):
    def stem(self, word: str) -> str:
        """Réduire un mot à sa racine."""
        ...

    @property
    def name(self) -> str:
        """Retourner le nom du stemmer."""
        return "my_stemmer"

    @property
    def language(self) -> str:
        """Retourner le code de langue."""
        return "english"
```

## Obtenir un Stemmer

### Détection Automatique (Recommandé)

La fonction `get_stemmer("auto", language)` sélectionne automatiquement le meilleur backend disponible :

```python
from whoosh_modern.analysis.stemmer_providers import get_stemmer

# Détection automatique : préfère PyStemmer si installé, sinon fallback interne
stemmer = get_stemmer("auto", "english")
print(stemmer.stem("running"))  # "run"
print(stemmer.name)             # "pystemmer" ou "internal"
```

**Ordre de priorité :**
1. **PyStemmer** (le plus rapide, nécessite `pip install whoosh-ng[fast-stemming]`)
2. **Stemmer interne** (Porter, toujours disponible)

### Sélection Explicite du Backend

```python
from whoosh_modern.analysis.stemmer_providers import get_stemmer

# Forcer le stemmer interne
stemmer = get_stemmer("internal", "english")

# Forcer PyStemmer (nécessite l'installation)
stemmer = get_stemmer("pystemmer", "english")
```

### Lister les Backends Disponibles

```python
from whoosh_modern.analysis.stemmer_providers import list_available_backends

backends = list_available_backends()
print(backends)
# {'internal': 'available', 'pystemmer': 'available', 'my_custom': 'registered'}
```

| Backend       | Chaîne de statut    | Nécessite                          |
|---------------|---------------------|-------------------------------------|
| `internal`    | `"available"`       | Aucun (toujours inclus)             |
| `pystemmer`   | `"available"` / `"not installed"` | `pip install whoosh-ng[fast-stemming]` |
| Personnalisé  | `"registered"`      | Enregistré via `@register_stemmer` |

## Fournisseurs de Stemmers Intégrés

### InternalStemmerProvider

Enveloppe le stemmer Porter intégré de Whoosh. Toujours disponible (aucune dépendance externe) :

```python
from whoosh_modern.analysis.stemmer_providers import InternalStemmerProvider

stemmer = InternalStemmerProvider("english")
print(stemmer.stem("cats"))    # "cat"
print(stemmer.stem("running")) # "run"
```

### PyStemmerProvider

Enveloppe la bibliothèque `Stemmer` pour un stemming haute performance. Supporte toutes les langues Snowball :

```python
from whoosh_modern.analysis.stemmer_providers import PyStemmerProvider

# Nécessite: pip install whoosh-ng[fast-stemming]
stemmer = PyStemmerProvider("english")
print(stemmer.stem("cats"))    # "cat"
```

**Note** : Ce fournisseur appelle `self._stemmer.stemWord(word)` pour réduire les mots. Assurez-vous que PyStemmer est installé ou la détection automatique basculera vers le stemmer interne.

### IdentityStemmerProvider

Un stemmer sans opération pour les tests ou lorsque le stemming n'est pas souhaité :

```python
from whoosh_modern.analysis.stemmer_providers import IdentityStemmerProvider

stemmer = IdentityStemmerProvider()
print(stemmer.stem("anything"))  # "anything"
```

## Enregistrer un Stemmer Personnalisé

Utilisez le décorateur `@register_stemmer` :

```python
from whoosh_modern.analysis.stemmer_providers import register_stemmer

@register_stemmer("simple")
class SimpleStemmer:
    def stem(self, word: str) -> str:
        # Suppression simple de suffixe
        if word.endswith("s") and len(word) > 3:
            return word[:-1]
        return word

    @property
    def name(self) -> str:
        return "simple"

    @property
    def language(self) -> str:
        return "english"

# Maintenant l'utiliser
from whoosh_modern.analysis.stemmer_providers import get_stemmer

stemmer = get_stemmer("simple", "english")
print(stemmer.stem("cats"))  # "cat"
```

## StemmingAnalyzer (Amélioré)

Situé dans `whoosh_modern.analysis.stemming_analyzer`, c'est le point d'entrée principal pour créer des analyseurs linguistiques :

```python
from whoosh_modern.analysis import StemmingAnalyzer

# Détection automatique du meilleur stemmer pour l'anglais
analyzer = StemmingAnalyzer(stemmer="auto", language="english")

# Stemmer interne explicite
analyzer = StemmingAnalyzer(stemmer="internal", language="english")

# Backend PyStemmer (si installé)
analyzer = StemmingAnalyzer(stemmer="pystemmer", language="french")

# Instance de fournisseur de stemmer personnalisé
analyzer = StemmingAnalyzer(stemmer=my_stemmer_instance)
```

### Paramètres de StemmingAnalyzer

| Paramètre   | Type                          | Défaut                   | Description                      |
|-------------|-------------------------------|--------------------------|----------------------------------|
| `expression`| Motif regex                   | motif de token par défaut | Tokenisation regex             |
| `stoplist`  | Itérable de mots vides        | `whoosh.analysis.STOP_WORDS` | Mots vides à filtrer         |
| `minsize`   | `int`                         | `2`                      | Longueur minimale du token       |
| `maxsize`   | `int \| None`                 | `None`                   | Longueur maximale du token       |
| `gaps`      | `bool`                        | `False`                  | Diviser sur l'expression vs correspondre |
| `stemmer`   | `str \| StemmerProvider`      | `"auto"`                 | Backend de stemmer               |
| `language`  | `str`                         | `"english"`              | Code de langue                   |
| `ignore`    | `set[str] \| None`            | `None`                   | Mots à ignorer                   |
| `cachesize` | `int`                         | `50000`                  | Taille du cache de stemming      |

### Utilisation avec les Types de Champs

```python
from whoosh_modern.analysis import StemmingAnalyzer
from whoosh.fields import Schema, TEXT

# Stemmer anglais avec mots vides
en_analyzer = StemmingAnalyzer("auto", language="english")

# Stemmer français
fr_analyzer = StemmingAnalyzer("auto", language="french")

schema = Schema(
    title=TEXT(stored=True),
    content_en=TEXT(analyzer=en_analyzer),
    content_fr=TEXT(analyzer=fr_analyzer),
)
```

## Analyseurs Linguistiques Spécifiques

Des analyseurs prêts à l'emploi pour cinq langues, disponibles dans `whoosh_modern.linguistics.stemmers` :

```python
from whoosh_modern.linguistics.stemmers import (
    EnglishAnalyzer,
    FrenchAnalyzer,
    GermanAnalyzer,
    SpanishAnalyzer,
    ItalianAnalyzer,
)

# Chaque analyseur est appelable et retourne une liste de tokens
en = EnglishAnalyzer()
tokens = en("The quick brown foxes")
# les tokens sont stemmés: ["quick", "brown", "fox"] (mots vides comme "the" supprimés)
```

### Analyseurs Linguistiques Disponibles

| Classe             | Langue    | Module                              |
|-------------------|-----------|-------------------------------------|
| `EnglishAnalyzer` | Anglais   | `whoosh_modern.linguistics.stemmers` |
| `FrenchAnalyzer`  | Français  | `whoosh_modern.linguistics.stemmers` |
| `GermanAnalyzer`  | Allemand  | `whoosh_modern.linguistics.stemmers` |
| `SpanishAnalyzer` | Espagnol  | `whoosh_modern.linguistics.stemmers` |
| `ItalianAnalyzer` | Italien   | `whoosh_modern.linguistics.stemmers` |

Chaque analyseur utilise en interne `get_stemmer("auto", language)` pour sélectionner le meilleur backend disponible et applique des mots vides spécifiques à la langue.

## Validation de la Compatibilité des Stemmers

Validez qu'un fournisseur de stemmer fonctionne correctement avec un ensemble de mots de test :

```python
from whoosh_modern.analysis.stemmer_providers import (
    get_stemmer,
    validate_stemmer_compatibility,
)

stemmer = get_stemmer("auto", "english")
report = validate_stemmer_compatibility(stemmer, ["running", "cats", "jumps", "houses"])

print(report["total_words"])   # 4
print(report["successful"])    # 4 (ou moins si erreurs)
print(report["failed"])        # 0
print(report["results"])       # [{'word': 'running', 'stemmed': 'run', 'success': True}, ...]
```

### Structure du Rapport de Compatibilité

| Champ          | Type       | Description                          |
|----------------|------------|--------------------------------------|
| `provider`     | `str`      | Nom du fournisseur de stemmer        |
| `language`     | `str`      | Code de langue                       |
| `total_words`  | `int`      | Nombre total de mots de test         |
| `successful`   | `int`      | Mots stemmés avec succès             |
| `failed`       | `int`      | Mots qui ont échoué                  |
| `results`      | `list[dict]` | Résultats par mot avec `word`, `stemmed`, `success` |

## Intégration avec StemmingMiddleware

Les fournisseurs de stemmers peuvent être utilisés avec le `StemmingMiddleware` de `whoosh_modern.middleware.analyzer` :

```python
from whoosh_modern.analysis.stemmer_providers import get_stemmer
from whoosh_modern.middleware.analyzer import StemmingMiddleware

stemmer = get_stemmer("auto", "english")
middleware = StemmingMiddleware(
    stemmer=stemmer.stem,
    fields=["title", "content"],  # Ne stemmer que ces champs
    stem_query=True,              # Also stemmer la requête de recherche
)
```

## Migration depuis Whoosh Classique

### Ancienne API (Whoosh 1.x/2.x)

```python
from whoosh.analysis import StemmingAnalyzer as OldAnalyzer
analyzer = OldAnalyzer("en")  # Codé en dur sur "english"
```

### Nouvelle API (Whoosh-NG 2.0)

```python
from whoosh_modern.analysis import StemmingAnalyzer

# Détection automatique du backend (recommandé)
analyzer = StemmingAnalyzer("auto", language="en")

# Ou utiliser un analyseur linguistique
from whoosh_modern.linguistics.stemmers import EnglishAnalyzer
analyzer = EnglishAnalyzer()
```

> **Note** : L'ancienne `StemmingAnalyzer("en")` était codée en dur sur la langue `"english"`. La nouvelle `StemmingAnalyzer(stemmer, language)` est explicite et prend en charge toutes les langues Snowball via PyStemmer.

## Installation

```bash
# Sans PyStemmer (utilise le stemmer interne, plus lent)
pip install whoosh-ng

# Avec PyStemmer (recommandé, plus rapide)
pip install whoosh-ng[fast-stemming]

# Analyse moderne complète
pip install whoosh-ng[modern]
```

## Intégration des Fournisseurs de Stemmers dans le Pipeline

Le système `StemmerProvider` s'intègre à **deux niveaux** : les analyseurs de niveau champ et le middleware de pipeline. Comprendre les deux est essentiel pour éviter le double-stemming.

### Architecture

```text
┌─────────────────────────────────────────────────────────────────┐
│  StemmingAnalyzer (niveau champ, dans Schema)                   │
│  ┌───────────────────────────────────────────────────────────┐  │
│  │ RegexTokenizer() │ StopFilter │ StemmingAnalyzer          │  │
│  │                    (mots vides)    │                       │  │
│  │                                   ▼                       │  │
│  │                         stemfn = provider.stem            │  │
│  │                                   │                       │  │
│  │                                   ▼                       │  │
│  │                         Token(stemmed=True)                │  │
│  └───────────────────────────────────────────────────────────┘  │
│                                                                 │
│  Appliqué par Whoosh core à l'indexation ET à la recherche     │
│  (via QueryParser). Automatique, aucun middleware nécessaire.   │
└─────────────────────────────────────────────────────────────────┘

┌─────────────────────────────────────────────────────────────────┐
│  StemmingMiddleware (niveau pipeline)                           │
│  ┌───────────────────────────────────────────────────────────┐  │
│  │ before_index(context)                                     │  │
│  │   └── stemmer toutes les valeurs str dans context.document  │  │
│  │                                                             │  │
│  │ before_search(context)                                     │  │
│  │   └── stemmer context.query si stem_query=True             │  │
│  └───────────────────────────────────────────────────────────┘  │
│                                                                 │
│  Intégré dans MiddlewareChain. Activation manuelle.              │
└─────────────────────────────────────────────────────────────────┘
```

### Niveau 1 : Niveau champ (automatique)

Le `StemmingAnalyzer` encapsule le `StemmingAnalyzer` intégré de Whoosh et injecte
la méthode `.stem` d'un `StemmerProvider` comme `stemfn`. Whoosh core l'applique
automatiquement au champ à la fois à l'indexation et à la recherche.

```python
from whoosh.fields import Schema, TEXT
from whoosh_modern.analysis import StemmingAnalyzer, get_stemmer

# Détection automatique du meilleur stemmer (PyStemmer préféré)
stemmer = get_stemmer("auto", "english")

# Créer un analyseur avec la fonction de stem du provider
analyzer = StemmingAnalyzer(stemmer=stemmer)

schema = Schema(
    title=TEXT(stored=True),
    content=TEXT(analyzer=analyzer),
)

# À l'indexation : "running cats" → ["run", "cat"]
# À la recherche : QueryParser utilise le même analyseur
# donc "running cats" correspond aux documents contenant "run cat"
```

**Avantages** : Automatique, pas de configuration de middleware nécessaire, comportement cohérent index/recherche.

**Inconvénients** : Nécessite que l'analyseur soit défini sur chaque champ TEXT. Plus difficile à changer à l'exécution.

### Niveau 2 : Niveau middleware (activé manuellement)

`StemmingMiddleware` applique le stemming au niveau du pipeline, opérant sur les
valeurs de chaîne brutes dans `context.document` et `context.query` avant que
Whoosh's analyzeurs ne les voient.

```python
from whoosh_modern.middleware import StemmingMiddleware
from whoosh_modern.analysis import get_stemmer
from whoosh.middleware.chain import MiddlewareChain
from whoosh.middleware.wrappers import MiddlewareWriter

stemmer = get_stemmer("auto", "english")

chain = MiddlewareChain([
    StemmingMiddleware(
        stemmer=stemmer.stem,
        fields=["title", "content"],  # None = tous les champs str
        stem_query=True,
    ),
])

with MiddlewareWriter(ix.writer(), chain) as writer:
    writer.add_document(title="Running cats", content="Fast dogs")
    # before_index stemme : "Running cats" → "run cat"
    writer.commit()
```

**Avantages** : Fonctionne avec n'importe quel champ sans modifier le schéma. Peut être activé/désactivé à l'exécution.

**Inconvénients** : Doit être connecté manuellement au pipeline. Risque de double-stemming si le champ utilise aussi `StemmingAnalyzer`.

### Exemple de pipeline complet : indexation + recherche

```python
from whoosh import index, fields
from whoosh.qparser import QueryParser
from whoosh_modern.analysis import StemmingAnalyzer, get_stemmer
from whoosh_modern.middleware import StemmingMiddleware
from whoosh.middleware.chain import MiddlewareChain
from whoosh_modern.analysis import StemmingAnalyzer

# 1. Schéma avec analyseur de niveau champ
stemmer = get_stemmer("auto", "english")
schema = fields.Schema(
    title=fields.TEXT(stored=True, analyzer=StemmingAnalyzer(stemmer=stemmer)),
    content=fields.TEXT(analyzer=StemmingAnalyzer(stemmer=stemmer)),
)

ix = index.create_in("indexdir", schema)

# 2. Indexation (pas de double-stemming car
#    on n'utilise pas StemmingMiddleware quand les champs ont StemmingAnalyzer)
with ix.writer() as writer:
    writer.add_document(title="Running cats", content="Fast dogs")
    writer.commit()

# 3. Recherche : QueryParser applique le même analyseur à la requête
with ix.searcher() as searcher:
    qp = QueryParser("content", schema)
    q = qp.parse("running cats")
    results = searcher.search(q)
    # "running" est stemmé en "run" par l'analyseur
    # "cats" est stemmé en "cat" par l'analyseur
    # Correspond au document avec "run" et "cat"
```

### Éviter le double-stemming

```python
# FAUX : double stemming
schema = Schema(
    content=TEXT(analyzer=StemmingAnalyzer(stemmer="auto")),
)
chain = MiddlewareChain([
    StemmingMiddleware(stemmer=get_stemmer("auto").stem),  # Ne pas faire ça !
])
# Résultat : "running" → "run" (analyseur) → "run" (middleware) — inoffensif mais gaspilleux

# CORRECT : choisir UN niveau
# Option A : niveau champ uniquement (recommandé pour schémas statiques)
schema = Schema(content=TEXT(analyzer=StemmingAnalyzer(stemmer="auto")))
# Aucun StemmingMiddleware nécessaire

# Option B : middleware uniquement (pour champs dynamiques)
schema = Schema(content=TEXT)  # Pas d'analyseur
chain = MiddlewareChain([StemmingMiddleware(stemmer=get_stemmer("auto").stem)])
```

### Provider de stemmer personnalisé

```python
from whoosh_modern.analysis import register_stemmer, get_stemmer

@register_stemmer("my_stemmer")
class MyStemmer:
    def stem(self, word: str) -> str:
        return word.lower().rstrip("s")

# Utilisez-le comme n'importe quel backend intégré
stemmer = get_stemmer("my_stemmer", "english")
analyzer = StemmingAnalyzer(stemmer=stemmer)
```

## Voir Aussi

- [Guide Stemming et Mots Vides](../core/stemming.md) — Guide classique de stemming de Whoosh
- [Guide Synonymes & Linguistique](linguistique.md) — Moteur d'expansion de synonymes
- [Intégration des Providers](provider-integration.md) — Guide complet du pipeline pour tous les providers
- [API: Moderne](../api/modern.md) — Référence complète de l'API pour les extensions d'analyse


## DOCUMENT (FR): Auto Indexing

> **Note de traduction** : Cette page n'est pas encore traduite en français.
> Le contenu anglais est affiché ci-dessous en attendant la traduction.

<!-- Creez une version francaise de ce fichier et supprimez ce message. -->


# Auto-Indexing

Whoosh-NG provides utilities for automatic schema discovery and data-source driven indexing.

## Schema Discovery

The `SchemaDiscovery` utility inspects a data source and auto-generates a Whoosh schema:

```python
from whoosh_modern.discovery import SchemaDiscovery

discovery = SchemaDiscovery(source=data_source)
schema = discovery.discover()
```

See [SearchView](/examples/search-view) and [Data Sources](/examples/data-sources) for usage examples.


## DOCUMENT (FR): Autocomplete Providers

# Providers d'Autocomplétion

Module : `whoosh_modern.autocomplete`
Version : 2.0.0

Le module d'autocomplétion fournit plusieurs stratégies de provider pour les
suggestions de requêtes et la recherche en taper-à-mesure (type-ahead). Tous
les providers implémentent une interface commune afin que vous puissiez
intervertir les stratégies à l'exécution. Les providers sont enregistrés via
l'`AutocompleteRegistry` et chargés par points d'entrée.

## Aperçu du module

```text
whoosh_modern.autocomplete
    ├── provider.py   # AutocompleteHit, AutocompleteProvider (Protocole)
    ├── ngram.py      # NGramProvider (basé sur les n-grammes de caractères)
    ├── edge_ngram.py # InvertedIndexAutocomplete (correspondance de préfixe par index inversé)
    ├── fuzzy.py      # FuzzySuggestProvider (correspondance approximative via rapidfuzz)
    ├── factory.py    # factory create_autocomplete()
    └── plugin.py     # AutocompletePlugin (plugin de point d'entrée)
```

## AutocompleteProvider (classe de base)

Localisé dans `whoosh_modern.autocomplete.provider` :

```python
from whoosh_modern.autocomplete.provider import AutocompleteProvider, AutocompleteHit

class MyProvider(AutocompleteProvider):
    def add(self, phrases: Iterable[str]) -> None:
        """Ajoute des phrases à l'index du provider."""
        ...

    def search(self, prefix: str, limit: int = 10) -> list[AutocompleteHit]:
        """Renvoie les suggestions d'autocomplétion pour le préfixe donné."""
        ...
```

### AutocompleteHit

Un objet résultat simple renvoyé par les providers :

```python
class AutocompleteHit:
    def __init__(self, text: str, score: float) -> None:
        self.text = text    # La phrase correspondante
        self.score = score  # Score de pertinence (plus élevé = meilleur)
```

## Providers intégrés

### InvertedIndexAutocomplete

Localisé dans `whoosh_modern.autocomplete.edge_ngram`. Utilise une
correspondance de préfixe simple contre une liste en mémoire :

```python
from whoosh_modern.autocomplete.edge_ngram import InvertedIndexAutocomplete

provider = InvertedIndexAutocomplete()
provider.add(["python", "pyramid", "pytorch", "java", "javascript"])

hits = provider.search("py", limit=5)
for hit in hits:
    print(f"{hit.text} (score: {hit.score})")
# Sortie :
# python (score: 0.45)
# pyramid (score: 0.43)
# pytorch (score: 0.43)
```

**Scoring** : les correspondances de préfixe exactes reçoivent un bonus 1.5x ;
le score de base est `1.0 / (len(phrase) + 1)`.

### NGramProvider

Localisé dans `whoosh_modern.autocomplete.ngram`. Construit un index de
n-grammes de caractères pour la correspondance approximative de sous-chaînes :

```python
from whoosh_modern.autocomplete.ngram import NGramProvider

provider = NGramProvider(n=3)
provider.add(["python programming", "java development", "rust language"])

hits = provider.search("pyt", limit=5)
for hit in hits:
    print(f"{hit.text} (score: {hit.score})")
```

**Paramètres :**

| Paramètre | Type | Défaut | Description                  |
|-----------|------|--------|------------------------------|
| `n`       | `int` | `3`    | Taille des n-grammes de caractères |

**Fonctionnement** : les n-grammes sont extraits de chaque phrase (en
minuscules). Lors de la recherche, les n-grammes du préfixe sont appariés avec
l'index. Les phrases avec le plus de n-grammes correspondants reçoivent les
scores les plus élevés.

### FuzzySuggestProvider

Localisé dans `whoosh_modern.autocomplete.fuzzy`. Utilise `rapidfuzz` pour la
correspondance approximative de chaînes (fautes de frappe, correspondances
partielles) :

```python
from whoosh_modern.autocomplete.fuzzy import FuzzySuggestProvider

# Nécessite : pip install whoosh-ng[fuzzy]
provider = FuzzySuggestProvider(max_distance=2, score_cutoff=50.0)
provider.add(["python", "pyramid", "pytorch", "java", "javascript"])

hits = provider.search("pythn", limit=5)  # Faute de frappe dans "python"
for hit in hits:
    print(f"{hit.text} (score: {hit.score})")
# Sortie : python (score: 0.95), ...
```

**Paramètres :**

| Paramètre       | Type  | Défaut  | Description                              |
|-----------------|-------|---------|------------------------------------------|
| `max_distance`  | `int` | `2`     | Distance d'édition max (non utilisé directement par rapidfuzz, réservé) |
| `score_cutoff`  | `float` | `50.0` | Score de similarité minimum (échelle 0-100) |

**Note** : nécessite `rapidfuzz` (`pip install whoosh-ng[fuzzy]`). Lève
`ImportError` si non installé.

## Fonction factory

Localisée dans `whoosh_modern.autocomplete.factory` :

```python
from whoosh_modern.autocomplete import create_autocomplete

# Crée n'importe quel provider par nom
provider = create_autocomplete("inverted")   # InvertedIndexAutocomplete
provider = create_autocomplete("ngram", n=3) # NGramProvider avec n personnalisé
provider = create_autocomplete("fuzzy", max_distance=2, score_cutoff=60.0)
```

**Providers disponibles :**

| Nom        | Classe                    | Dépendance optionnelle |
|------------|---------------------------|------------------------|
| `"inverted"`| `InvertedIndexAutocomplete` | Aucune              |
| `"ngram"`   | `NGramProvider`          | Aucune                |
| `"fuzzy"`   | `FuzzySuggestProvider`   | `rapidfuzz`           |

## Enregistrement dans l'AutocompleteRegistry

Les providers sont enregistrés dans `whoosh.registry.AutocompleteRegistry`
(une instance `Registry`) :

```python
from whoosh.registry import AutocompleteRegistry
from whoosh_modern.autocomplete import create_autocomplete

# Enregistre un provider
provider = create_autocomplete("ngram", n=3)
AutocompleteRegistry.register("ngram-suggester", provider, owner="my_app")

# Le récupère plus tard
suggester = AutocompleteRegistry.get("ngram-suggester")

# Liste tous les providers enregistrés
print(AutocompleteRegistry.list_keys())
```

## AutocompletePlugin (point d'entrée)

Localisé dans `whoosh_modern.autocomplete.plugin`, c'est le plugin intégré
enregistré via le groupe de points d'entrée `whoosh_ng.plugins` :

```python
from whoosh_modern.autocomplete.plugin import AutocompletePlugin

# Chargé automatiquement par PluginManager.load_plugins()
# Enregistre le provider "inverted" dans AutocompleteRegistry
```

### Déclaration du point d'entrée

Dans `pyproject.toml` :

```toml
[project.entry-points."whoosh_ng.plugins"]
whoosh_autocomplete = "whoosh_modern.autocomplete.plugin:AutocompletePlugin"
```

### Détails du plugin

```python
class AutocompletePlugin(Plugin):
    name = "whoosh_autocomplete"
    version = "3.0.0"

    def register(self, manager):
        # Enregistre InvertedIndexAutocomplete en tant que "inverted"
        AutocompleteRegistry.register(
            "inverted", create_autocomplete("inverted"), self.name
        )

    def register_hooks(self):
        # Enregistre un hook on_search (actuellement sans effet)
        from whoosh.hooks import hookimpl, register_hook
        register_hook("on_search", hookimpl(on_search))
```

## Exemples d'utilisation

### Utilisation de base

```python
from whoosh_modern.autocomplete import create_autocomplete

# Crée et remplit un provider
provider = create_autocomplete("inverted")
provider.add([
    "python programming",
    "python tutorial",
    "java tutorial",
    "javascript framework",
])

# Recherche des suggestions
hits = provider.search("py", limit=3)
for hit in hits:
    print(f"{hit.text}: {hit.score:.3f}")
```

### Utilisation de la correspondance floue avec tolérance aux fautes

```python
from whoosh_modern.autocomplete import create_autocomplete

provider = create_autocomplete("fuzzy", score_cutoff=70.0)
provider.add(["python", "pytorch", "tensorflow", "keras"])

# Même avec une faute de frappe, des suggestions pertinentes sont renvoyées
hits = provider.search("pyton", limit=5)
for hit in hits:
    print(hit.text, hit.score)
```

### Utilisation des n-grammes pour les mots partiels

```python
from whoosh_modern.autocomplete import create_autocomplete

# Utilise des 3-grammes pour une meilleure correspondance de sous-chaînes
provider = create_autocomplete("ngram", n=3)
provider.add(["machine learning", "deep learning", "neural networks"])

# Trouve les phrases contenant les n-grammes de "machin"
hits = provider.search("machin", limit=5)
```

### Intégration avec la recherche

```python
from whoosh_modern.autocomplete import create_autocomplete

# Construit le provider d'autocomplétion
provider = create_autocomplete("inverted")
provider is None  # (exemple conceptuel)
provider = create_autocomplete("inverted")
provider.add(["python", "java", "javascript", "go", "rust"])

# Utilise dans un endpoint de recherche
def suggest(prefix: str, limit: int = 5):
    hits = provider.search(prefix, limit=limit)
    return [{"text": h.text, "score": h.score} for h in hits]

# Dans votre endpoint FastAPI/REST :
# GET /api/suggest?q=py&limit=5
# Réponse : [{"text": "python", "score": 0.45}, ...]
```

## Comparaison des providers

| Provider              | Correspondance  | Forces                    | Faiblesses               | Dépendance    |
|-----------------------|----------------|---------------------------|--------------------------|---------------|
| `inverted`            | Préfixe         | Simple, rapide, sans deps | Pas de tolérance aux fautes | Aucune     |
| `ngram`               | Recouvrement n-grammes | Correspondance de sous-chaînes, flexible | Plus lent que le préfixe | Aucune |
| `fuzzy`               | Distance d'édition | Tolérance aux fautes, flexible | Nécessite rapidfuzz | `rapidfuzz` |

## Installation

```bash
# Autocomplétion de base (inverted + n-gram)
pip install whoosh-ng

# Avec correspondance floue
pip install whoosh-ng[fuzzy]

# Analyse moderne complète
pip install whoosh-ng[modern]
```

## Voir aussi

- [Guide du Système de Plugins](plugins-advanced.md) — Enregistrement et découverte de plugins
- [Guide du Middleware](middleware-pipeline.md) — Intégration du pipeline de middleware
- [Guide d'Intégration des Providers](provider-integration.md) — Guide complet du pipeline pour tous les providers
- [API : Linguistique](../api/modern.md) — Référence complète de l'API pour les extensions d'autocomplétion


## DOCUMENT (FR): Autocomplete

# Autocomplétion

Couche optionnelle d'autocomplétion par edge-ngram pour Whoosh-NG.

## Installer

```bash
pip install whoosh-ng[autocomplete]
```

## Index minimal

```python
from whoosh.fields import Schema, TEXT, AutocompleteField

schema = Schema(
    titre=TEXT(stored=True),
    query=AutocompleteField()
)

with ix.writer() as writer:
    writer.add_document(titre="Démarrage Python", query="demarrage python")
    writer.commit()
```

## Requête d'autocomplétion

```python
from whoosh_modern.autocomplete import AutocompleteProvider

provider = AutocompleteProvider(ix, "query")
suggestions = provider.suggest("de", limit=5)
print(suggestions)  # ["demarrage python", ...]
```


## DOCUMENT (FR): Configuration Engine

# Moteur de Configuration

Whoosh-NG intègre un **Moteur de Configuration** (`ConfigEngine`) qui charge,
valide et fusionne la configuration applicative depuis des fichiers YAML ou
JSON. Il repose sur des modèles Pydantic et prend en charge une organisation
hiérarchique par couches, permettant à des surcharges spécifiques à
l'environnement d'étendre proprement les réglages de base.

## Concepts de base

### Modèles Pydantic

Toute la configuration est exprimée via des modèles Pydantic typés :

- `WhooshNGConfig` — configuration applicative de plus haut niveau
- `FieldConfig` — options d'indexation par champ
- `SearchConfig` / `FuzzyConfig` / `RankingConfig` / `AIConfig`
- `DataSourceConfigModel` — connexion et synchronisation de la source de données
- `StorageConfigModel` — sélection du backend de stockage

### Chargeurs (loaders)

Deux chargeurs sont fournis :

- `load_yaml(path)` — analyse un fichier YAML en `dict`
- `load_json(path)` — analyse un fichier JSON en `dict`
- `load_config(path)` — détecte automatiquement le format depuis l'extension et
  renvoie un `WhooshNGConfig` validé

### Fusion hiérarchique

`ConfigEngine.load(path, priority=...)` et `ConfigEngine.merge(overrides, priority=...)`
empilent les sources de configuration selon l'ordre de priorité suivant (le plus
haut l'emporte) :

1. `runtime`
2. `instance`
3. `application`
4. `language`

Une valeur de ``priority`` invalide lève immédiatement ``ValueError``, afin
qu'une couche mal configurée ne puisse pas affecter silencieusement l'ordre de
fusion.

La fusion est profonde : les dictionnaires imbriqués sont fusionnés
récursivement. Les valeurs scalaires et les listes sont **remplacées
entièrement** par les valeurs de surcharge ; les listes ne sont PAS ajoutées ni
combinées. Par exemple, une configuration de base ``{"plugins": ["a", "b"]}``
surchargée par ``{"plugins": ["c"]}`` produit ``{"plugins": ["c"]}``, et non
``{"plugins": ["a", "b", "c"]}``. Si une fusion additive de listes est requise,
traitez-la au niveau applicatif avant d'appeler :meth:`ConfigEngine.merge`.

> [!WARNING]
> **Comportement de remplacement des listes** : lors de la fusion de
> configurations, les listes sont **completement écrasées** par les couches de
> priorité supérieure. Elles ne sont ni ajoutées, ni concaténées, ni
> dédupliquées. Il s'agit d'un choix de conception volontaire qui garantit un
> contrôle explicite du contenu des listes entre les couches et évite des états
> fusionnés imprévisibles. Si vous avez besoin d'un comportement additif (par
> exemple étendre une liste de plugins ou de middlewares), effectuez la logique
> de fusion dans votre code applicatif avant de passer le dictionnaire final à
> :meth:`ConfigEngine.merge`.

## Démarrage rapide

```python
from whoosh_modern.config import ConfigEngine

engine = ConfigEngine()
engine.load("whoosh-ng.yml", priority="application")
engine.load("whoosh-ng.local.yml", priority="instance")
engine.merge({"search": {"fuzzy": {"distance": 5}}}, priority="runtime")

config = engine.get_config()
print(config.index)
print(config.fields["title"].stemming)
print(config.search.fuzzy.distance)
```

## Exemple YAML

```yaml
# whoosh-ng.yml
index: products
languages:
  default: fr
fields:
  title:
    type: text
    language: fr
    stemming: true
    stored: true
  price:
    type: numeric
    sortable: true
search:
  fuzzy:
    enabled: true
    distance: 2
storage:
  type: file
  path: ./index
```

## Exemple JSON

```json
{
  "index": "products",
  "languages": {"default": "en"},
  "fields": {
    "title": {"type": "text", "language": "en", "stemming": true},
    "price": {"type": "numeric", "sortable": true}
  },
  "search": {"fuzzy": {"enabled": true, "distance": 2}},
  "storage": {"type": "file", "path": "./index"}
}
```

## Exemples YAML complets

### Configuration minimale

```yaml
# whoosh-ng.yml
index: my_index
fields:
  title:
    type: text
    stored: true
storage:
  type: file
  path: ./index
```

### Catalogue e-commerce avec source CSV

```yaml
# whoosh-ng.yml
index: products
fields:
  sku:
    type: text
    stored: true
    unique: true
  name:
    type: text
    language: fr
    stemming: true
    stored: true
  description:
    type: text
    language: fr
    stemming: true
  price:
    type: numeric
    sortable: true
    faceted: true
  category:
    type: text
    faceted: true
  published_at:
    type: datetime
    faceted: true
search:
  fuzzy:
    enabled: true
    distance: 2
data_source:
  type: csv
  path: Datas/products.csv
  delimiter: ","
  encoding: utf-8
  id_field: sku
storage:
  type: file
  path: ./index
```

### Configuration par couches (base + instance + runtime)

```yaml
# whoosh-ng.yml  (couche application)
index: app
fields:
  title:
    type: text
    stemming: true
search:
  fuzzy:
    enabled: true
    distance: 2
storage:
  type: file
  path: ./index
```

```yaml
# whoosh-ng.local.yml  (couche instance)
index: app-staging
storage:
  type: file
  path: ./index-staging
```

```python
# surcharge runtime dans le code
engine = ConfigEngine()
engine.load("whoosh-ng.yml", priority="application")
engine.load("whoosh-ng.local.yml", priority="instance")
engine.merge({"search": {"fuzzy": {"distance": 3}}}, priority="runtime")
app = engine.build()
```

## Exemples JSON complets

### Configuration minimale

```json
{
  "index": "my_index",
  "fields": {
    "title": {"type": "text", "stored": true}
  },
  "storage": {"type": "file", "path": "./index"}
}
```

### Configuration full-stack avec source SQL et stockage hybride

```json
{
  "index": "customers",
  "fields": {
    "customer_id": {"type": "numeric", "stored": true, "sortable": true},
    "first_name": {"type": "text", "language": "en", "stemming": true, "stored": true},
    "last_name": {"type": "text", "language": "en", "stemming": true, "stored": true},
    "city": {"type": "text", "language": "en", "stemming": true, "stored": true},
    "country": {"type": "text", "stored": true},
    "signup_date": {"type": "datetime", "faceted": true}
  },
  "search": {
    "fuzzy": {"enabled": true, "distance": 2},
    "highlight": {"enabled": true, "fragment_size": 200}
  },
  "data_source": {
    "type": "sql",
    "connection_string": "sqlite:///benchmark_data.db",
    "query": "SELECT * FROM customers",
    "id_field": "customer_id"
  },
  "storage": {
    "type": "hybrid",
    "local_path": "./index-cache",
    "remote": {
      "type": "s3",
      "bucket": "my-bucket",
      "prefix": "whoosh-indexes/"
    }
  }
}
```

## Configuration sans code avec ConfigEngine.build()

```python
from whoosh_modern.config import ConfigEngine

engine = ConfigEngine()
engine.load("whoosh-ng.yml")
app = engine.build()
app.build()

# Ajouter des documents via l'index writer
writer = app.index.writer()
writer.add_document(title="Premier cours de Python", body="...")
writer.add_document(title="Whoosh-NG avancé", body="...")
writer.commit()

# Ou utiliser la source directement pour un indexage en flux/lot
for doc in app._source.iter_documents():
    with app.index.writer() as writer:
        writer.add_document(**doc)

results = app.search("python")
```

## Référence des modules

| Module | Rôle |
|---|---|
| `whoosh_modern.config.models` | Modèles Pydantic de validation |
| `whoosh_modern.config.loader` | Chargeurs de fichiers YAML / JSON |
| `whoosh_modern.config.engine` | `ConfigEngine` avec fusion hiérarchique |

## Voir aussi

- [Providers de Stockage](storage-providers.md) — Backends configurables via `StorageConfigModel`
- [Sources de Données](data-sources.md) — `DataSourceConfigModel` et configuration des providers


## DOCUMENT (FR): Linguistics

# Synonymes & Linguistique

Module : `whoosh_modern.linguistics.synonyms`, `whoosh_modern.linguistics.stemmers`
Version : 2.0.0

Le module de linguistique fournit un moteur complet d'expansion de synonymes
et des analyseurs de texte spécifiques à une langue. Il s'intègre au pipeline de
middleware pour étendre les requêtes et les documents avec des synonymes aussi
bien à l'indexation qu'à la recherche.

## Aperçu du module

```text
whoosh_modern.linguistics
    ├── synonyms/
    │   ├── provider.py       # Protocole SynonymProvider + StaticSynonymProvider
    │   ├── yaml_provider.py  # YAMLSynonymProvider
    │   ├── json_provider.py  # JSONSynonymProvider
    │   ├── store.py          # SQLiteSynonymStore
    │   ├── compiler.py       # SynonymCompiler
    │   ├── manager.py        # SynonymManager
    │   ├── middleware.py      # SynonymExpansionMiddleware
    │   └── languages.py      # LANG_SYNONYMS (FR/EN/DE/ES/IT)
    └── stemmers/
        └── __init__.py       # Analyseurs spécifiques à une langue (FR/EN/DE/ES/IT)
```

## Providers de synonymes

### SynonymProvider (Protocole)

Le protocole de base implémenté par tous les providers de synonymes :

```python
from whoosh_modern.linguistics.synonyms import SynonymProvider

class MyProvider(SynonymProvider):
    def get_synonyms(self, word: str) -> list[str]:
        """Renvoie les synonymes du mot donné."""
        ...

    def add_synonym(self, word: str, synonyms: list[str]) -> None:
        """Ajoute des synonymes au mot donné."""
        ...

    def remove_synonym(self, word: str, synonym: str) -> None:
        """Retire un synonyme du mot donné."""
        ...
```

### StaticSynonymProvider

Provider en mémoire appuyé sur un dictionnaire :

```python
from whoosh_modern.linguistics.synonyms import StaticSynonymProvider

provider = StaticSynonymProvider({
    "car": ["automobile", "vehicle", "auto"],
    "house": ["home", "residence"],
})

print(provider.get_synonyms("car"))  # ['automobile', 'vehicle', 'auto']
```

### YAMLSynonymProvider

Charge les synonymes depuis un fichier YAML :

```yaml
# synonyms.yaml
car:
  - automobile
  - vehicle
  - auto
house:
  - home
  - residence
```

```python
from whoosh_modern.linguistics.synonyms import YAMLSynonymProvider

# Nécessite : pip install pyyaml
provider = YAMLSynonymProvider("synonyms.yaml")
print(provider.get_synonyms("car"))  # ['automobile', 'vehicle', 'auto']
```

### JSONSynonymProvider

Charge les synonymes depuis un fichier JSON :

```json
{
    "car": ["automobile", "vehicle", "auto"],
    "house": ["home", "residence"]
}
```

```python
from whoosh_modern.linguistics.synonyms import JSONSynonymProvider

provider = JSONSynonymProvider("synonyms.json")
print(provider.get_synonyms("car"))
```

### SQLiteSynonymStore

Stockage persistant de synonymes appuyé sur SQLite :

```python
from whoosh_modern.linguistics.synonyms import SQLiteSynonymStore

store = SQLiteSynonymStore("synonyms.db")

# Opérations CRUD
store.add_synonym("car", ["automobile", "vehicle"])
print(store.get_synonyms("car"))  # ['automobile', 'vehicle']
store.remove_synonym("car", "automobile")
print(store.get_synonyms("car"))  # ['vehicle']
store.close()
```

### SynonymCompiler

Précompile les données de synonymes brutes dans un format de recherche rapide :

```python
from whoosh_modern.linguistics.synonyms import SynonymCompiler

compiler = SynonymCompiler({"car": ["automobile", "vehicle"]})
compiler.add("house", ["home", "residence"])
compiler.merge({"book": ["publication", "work"]})

compiled = compiler.compile()
print(compiled)
# {'car': ['automobile', 'vehicle'], 'house': ['home', 'residence'], 'book': ['publication', 'work']}
```

## SynonymManager

Le `SynonymManager` est l'interface de haut niveau pour gérer les synonymes. Il
encapsule en interne un `StaticSynonymProvider` et prend en charge
l'import/export :

```python
from whoosh_modern.linguistics.synonyms import SynonymManager

manager = SynonymManager({"car": ["automobile", "vehicle"]})

# CRUD
manager.add_synonyms("house", ["home", "residence"])
print(manager.get_synonyms("house"))  # ['home', 'residence']
manager.remove_synonym("house", "home")

# Import depuis des sources externes
manager.import_yaml("synonyms.yaml")   # Nécessite PyYAML
manager.import_json("synonyms.json")

# Export
manager.export_json("output.json")
```

### Workflow d'import/export

```python
# Import depuis YAML
manager = SynonymManager()
manager.import_yaml("my_synonyms.yaml")

# Export vers JSON (ex : pour migration ou sauvegarde)
manager.export_json("backup.json")
```

## Dictionnaires de synonymes préconstruits

Le dictionnaire `LANG_SYNONYMS` contient des correspondances de synonymes de
démarrage pour cinq langues :

```python
from whoosh_modern.linguistics.synonyms import LANG_SYNONYMS

# Langues disponibles : fr, en, de, es, it
french_syns = LANG_SYNONYMS["fr"]
print(french_syns["voiture"])  # ['automobile', 'véhicule']

english_syns = LANG_SYNONYMS["en"]
print(english_syns["car"])  # ['automobile', 'vehicle']

# Amorce un SynonymManager avec une langue
manager = SynonymManager(LANG_SYNONYMS["fr"])
```

| Langue  | Code | Entrée d'exemple                          |
|---------|------|-------------------------------------------|
| Français | `fr` | `"voiture": ["automobile", "véhicule"]`    |
| Anglais | `en` | `"car": ["automobile", "vehicle"]`        |
| Allemand | `de` | `"auto": ["wagen", "fahrzeug"]`           |
| Espagnol | `es` | `"coche": ["automóvil", "vehículo"]`      |
| Italien | `it` | `"auto": ["automobile", "veicolo"]`       |

> **Note** : il s'agit de dictionnaires de démarrage minimaux destinés à la
> démonstration. Les déploiements en production devraient charger des sources
> organisées ou spécifiques à un domaine.

## SynonymExpansionMiddleware

Intègre l'expansion de synonymes dans le pipeline de middleware. Il étend à la
fois les requêtes de recherche et les champs de documents indexés :

```python
from whoosh_modern.linguistics.synonyms import (
    SynonymManager,
    SynonymExpansionMiddleware,
)

# Crée un manager avec vos synonymes
manager = SynonymManager({
    "car": ["automobile", "vehicle"],
    "house": ["home", "residence"],
})

# Crée le middleware
middleware = SynonymExpansionMiddleware(manager)

# Enregistre auprès du PluginManager ou de la MiddlewareChain
from whoosh.plugins.manager import PluginManager
PluginManager._default.register_middleware("synonym", middleware)
```

### Fonctionnement

- **`before_search`** : étend `context.query` en ajoutant les synonymes de
  chaque token
- **`before_index`** : étend les valeurs chaîne dans `context.document` en
  ajoutant les synonymes

```python
# Avant : query = "car"
# Après  : query = "car automobile vehicle"

# Avant : document = {"title": "house for sale"}
# Après : document = {"title": "house for sale home residence"}
```

## Analyseurs de stemming spécifiques à une langue

Localisés dans `whoosh_modern.linguistics.stemmers`, ces analyseurs combinent
tokenization, stemming et suppression des mots vides :

```python
from whoosh_modern.linguistics.stemmers import (
    EnglishAnalyzer,
    FrenchAnalyzer,
    GermanAnalyzer,
    SpanishAnalyzer,
    ItalianAnalyzer,
)

# Chaque analyseur est une instance de LanguageAnalyzer et est appelable :
# il renvoie une liste de tokens
analyzer = EnglishAnalyzer
tokens = analyzer("The running cats")
# tokens sont stemmés : ["run", "cat"] (mots vides supprimés)

# L'usage "style classe" rétro-compatible fonctionne aussi : appeler l'analyseur
# sans argument renvoie une nouvelle instance, donc le code historique
# écrit comme EnglishAnalyzer()(text) continue de fonctionner inchangé.
tokens = EnglishAnalyzer()("The running cats")
```

### Sélection du backend de stemming

Sous le capot, les stemmers utilisent `whoosh_modern.analysis.stemmer_providers`
:

```python
from whoosh_modern.analysis.stemmer_providers import (
    get_stemmer,
    register_stemmer,
    list_available_backends,
)

# Auto-détecte le meilleur stemmer disponible (PyStemmer privilégié)
stemmer = get_stemmer("auto", "english")

# Backend explicite
stemmer = get_stemmer("internal", "english")   # Stemmer intégré de Whoosh
stemmer = get_stemmer("pystemmer", "english")   # PyStemmer (plus rapide)

# Liste les backends disponibles
print(list_available_backends())
# {'internal': 'available', 'pystemmer': 'available', ...}

# Enregistre un stemmer personnalisé
@register_stemmer("my_stemmer")
class MyStemmer:
    def stem(self, word: str) -> str:
        return word.lower()
```

| Backend     | Nécessite                       | Vitesse                |
|-------------|---------------------------------|------------------------|
| `auto`      | Aucun (repli automatique)       | Le plus rapide dispo.  |
| `internal`  | Aucun (Porter stemmer intégré)   | Moyenne                |
| `pystemmer` | `pip install whoosh-ng[fast-stemming]` | Rapide         |

## Exemple d'intégration : pipeline complet

```python
from whoosh_modern.linguistics import (
    EnglishAnalyzer,
    LANG_SYNONYMS,
    SynonymExpansionMiddleware,
    SynonymManager,
)
from whoosh.middleware.chain import MiddlewareChain
from whoosh.middleware.wrappers import MiddlewareWriter, MiddlewareSearcher

# 1. Construit le manager de synonymes avec les synonymes anglais
syn_manager = SynonymManager(LANG_SYNONYMS["en"])
syn_manager.add_synonyms("search", ["query", "find", "lookup"])

# 2. Crée le middleware d'expansion de synonymes
syn_middleware = SynonymExpansionMiddleware(syn_manager)

# 3. Construit la chaîne de middleware
chain = MiddlewareChain([syn_middleware])

# 4. Enveloppe le writer et le searcher
with MiddlewareWriter(ix.writer(), chain) as writer:
    writer.add_document(title="How to search in Whoosh")

with MiddlewareSearcher(ix.searcher(), chain) as searcher:
    # La requête "search" est étendue en "search query find lookup"
    results = searcher.search("search")
```

## Registre de langues

`LanguageRegistry` mappe les codes de langue vers des instances `LanguageProfile`, centralisant la résolution d'analyzer, de stemmer, de provider de synonymes et de détecteur de langue.

```python
from whoosh_modern.linguistics.registry import (
    LanguageRegistry,
    LanguageProfile,
    StemmerRegistry,
    get_default_registry,
)

# Utilise le registry pré-peuplé par défaut (FR/EN/DE/ES/IT)
registry = get_default_registry()

# Résout un profil de langue
profile = registry.resolve("fr")
print(profile.language)   # "fr"
print(profile.analyzer)   # instance de FrenchAnalyzer

# Enregistre un profil de langue personnalisé
custom = LanguageProfile(
    language="pt",
    analyzer=...,  # votre analyzer
    stemmer=...,   # votre stemmer
)
registry.register(custom)

# StemmerRegistry ajoute des helpers spécifiques aux stemmers
stem_registry = StemmerRegistry(registry._profiles.values())
stemmer = stem_registry.get_stemmer("fr")
```

## Analyseur multilingue

`MultiLanguageAnalyzer` applique plusieurs analyseurs de langue simultanément pour l'indexation multilingue.

```python
from whoosh_modern.linguistics.analyzers import MultiLanguageAnalyzer

# Par défaut : FR/EN/DE/ES/IT
analyzer = MultiLanguageAnalyzer()

# Ensemble de langues personnalisé
analyzer = MultiLanguageAnalyzer(languages=["fr", "en"])

tokens = analyzer("hello bonjour")
# Retourne les tokens combinés de tous les analyseurs configurés
```

## Auto-détection de langue

`StopwordDetector` et `LangDetectProvider` permettent la détection automatique de langue :

```python
from whoosh_modern.linguistics.detection import StopwordDetector

detector = StopwordDetector(supported_languages=["fr", "en", "de"])
lang = detector.detect("Ceci est un texte en français")
print(lang)  # "fr"
```

Utilisation avec `SearchApplication` pour la résolution automatique de langue :

```python
from whoosh_modern import SearchApplication
from whoosh_modern.linguistics.detection import StopwordDetector

app = SearchApplication(
    source=my_source,
    language_detector=StopwordDetector(),
)

# FieldConfig supporte language="auto"
# Le détecteur résout la langue par document
```

## Analyseur Explain

`ExplainAnalyzer` expose le pipeline de tokenization/stemming pour Search Studio :

```python
from whoosh_modern.linguistics.explain import ExplainAnalyzer

explainer = ExplainAnalyzer(EnglishAnalyzer)
result = explainer.explain("The running cats")

print(result.text)       # "The running cats"
print(result.tokens)     # ["run", "cat"]
```

## Débogage d'analyse avec ExplainAnalyzer

`ExplainAnalyzer` encapsule n'importe quel analyseur existant et retourne une
`AnalysisExplanation` décrivant comment un texte est transformé. C'est un outil
utile pour déboguer des chaînes d'analyseurs complexes, surtout lors de l'utilisation
d'analyseurs multilingues, de filtres stopwords ou de remplacements de stemming
par dictionnaire.

```python
from whoosh_modern.linguistics.explain import ExplainAnalyzer
from whoosh.analysis import StandardAnalyzer

explainer = ExplainAnalyzer(StandardAnalyzer())
explanation = explainer.explain(
    "A quick brown fox jumps over the lazy dog"
)

print(f"Texte original : {explanation.text}")
print(f"Tokens finaux : {explanation.tokens}")

print("\nExplications étape par étape :")
for step in explanation.explanations:
    print(
        f"  - {step.step} : '{step.original}' -> '{step.result}'"
    )
```

Exemple de sortie :

```text
Texte original : A quick brown fox jumps over the lazy dog
Tokens finaux : ['quick', 'brown', 'fox', 'jumps', 'lazy', 'dog']

Explications étape par étape :
  - tokenize : 'A' -> 'A'
  - lowercase : 'A' -> 'a'
  - stop : 'a' -> ''
  - tokenize : 'quick' -> 'quick'
  - lowercase : 'quick' -> 'quick'
  ...
```

### Interprétation de la sortie

- `explanation.text` — le texte d'entrée original.
- `explanation.tokens` — la liste finale de tokens après toutes les étapes de l'analyseur.
- `explanation.explanations` — une liste chronologique d'objets
  `TokenExplanation` montrant chaque transformation.

Utilisez ceci lorsque :
- une chaîne d'analyseurs se comporte différemment de ce qui est attendu,
- vous avez besoin de vérifier quels stopwords ou règles de stemming sont appliqués,
- vous voulez comparer le comportement entre langues avec `MultiLanguageAnalyzer`.

## Override de stem par dictionnaire

Remplace le stemming Snowball par des dictionnaires métier :

```python
from whoosh_modern.linguistics.dictionary_stem_override import DictionaryStemOverride

override = DictionaryStemOverride({
    "voiture": "voitur",
    "maison": "maison",
})

print(override.stem("voiture"))  # "voitur"
print(override.stem("maison"))   # "maison"

# Ajoute des règles dynamiquement
override.add_rule("chien", "chien")
```

Utilisation avec `SearchApplication` :

```python
from whoosh_modern import SearchApplication

app = SearchApplication(
    source=my_source,
    dictionary_stem_overrides={"voiture": "voitur"},
)
```

## Analyseur de stemming avec cache

`CachedStemmingAnalyzer` encapsule les analyseurs de langue avec un cache LRU :

```python
from whoosh_modern.analysis.cached_stemming_analyzer import CachedStemmingAnalyzer
from whoosh_modern.linguistics.stemmers import FrenchAnalyzer

cached = CachedStemmingAnalyzer(FrenchAnalyzer, cache_size=50000)
tokens = cached("les maisons")
```

## Profileur de stemmer

Mesure l'impact du stemming sur le vocabulaire et les performances :

```python
from whoosh_modern.profiling.stemmer_profiler import StemmerProfiler

profiler = StemmerProfiler(stemmer=my_stemmer)
report = profiler.profile(["document 1", "document 2", ...])

print(report.original_tokens)        # Tokens totaux avant stemming
print(report.stemmed_tokens)         # Tokens uniques après stemming
print(report.reduction_ratio)        # Ratio de réduction du vocabulaire
print(report.estimated_size_reduction)  # Réduction estimée de la taille d'index %
print(report.avg_stem_time_ms)       # Temps moyen de stemming par token
```

## Préréglages d'analyseurs

Analyseurs préconfigurés pour des scénarios de recherche courants :

```python
from whoosh_modern.analysis.stemmer_presets import AnalyzerPresets

# Autocomplétion
autocomplete_analyzer = AnalyzerPresets.autocomplete()

# Correspondance partielle
partial_analyzer = AnalyzerPresets.partial_match()

# E-commerce
ecommerce_analyzer = AnalyzerPresets.ecommerce()

# Blog
blog_analyzer = AnalyzerPresets.blog()

# Multilingue
multilingual_analyzer = AnalyzerPresets.multilingual()

# Accès par nom
analyzer = AnalyzerPresets.get("autocomplete")
```

## Voir aussi

- [Synonymes](synonyms.md) — Providers de synonymes, manager et dictionnaires Wiktionary
- [Guide des Stemmers](stemming-providers.md) — Providers de stemming et analyseurs de langue
- [Guide du Middleware](middleware-pipeline.md) — Intégration du pipeline de middleware
- [Guide d'Intégration des Providers](provider-integration.md) — Guide complet du pipeline pour tous les providers
- [API : Linguistique](../api/modern.md) — Référence complète de l'API


## DOCUMENT (FR): Middleware Pipeline

# Middleware & Pipeline de Plugins

Module: `whoosh.middleware`, `whoosh.middleware.chain`, `whoosh.middleware.context`, `whoosh_modern.middleware`
Version: 2.0.0

Le pipeline de middleware permet d'intercepter et de modifier les opérations d'indexation et de recherche. C'est le mécanisme d'extension principal pour les préoccupations transverses comme la journalisation, la mise en cache, les métriques, la réécriture de requêtes et la sécurité. Le middleware peut provenir à la fois du package core `whoosh.middleware` et des plugins chargés via le `PluginManager`.

## Vue d'ensemble de l'architecture

```text
Writer/Searcher  ───►  MiddlewareChain
                           ├── Middleware 1 (hook before)
                           ├── Middleware 2 (hook before)
                           ├── ─── opération core ───
                           ├── Middleware 2 (hook after, inverse)
                           └── Middleware 1 (hook after, inverse)
```

- Les **hooks `before_*`** s'exécutent dans l'ordre d'enregistrement
- Les **hooks `after_*`** s'exécutent dans l'ordre inverse (comme une pile / oignon)
- Si un hook lève `StopOperation`, le pipeline s'arrête gracieusement
- Si `fail_open=False` (défaut), les exceptions se propagent immédiatement

## Classes de Base du Middleware

### Middleware (Classe de Base)

Localisée dans `whoosh.middleware.base`. Les sous-classes implémentent les hooks du cycle de vie :

```python
from whoosh.middleware.base import Middleware
from whoosh.middleware.context import MiddlewareContext

class MyMiddleware(Middleware):
    def startup(self, context: MiddlewareContext) -> None:
        """Appelé une fois quand le middleware est initialisé."""
        pass

    def shutdown(self, context: MiddlewareContext) -> None:
        """Appelé une fois quand le middleware est détruit."""
        pass

    def before_index(self, context: MiddlewareContext) -> MiddlewareContext:
        """Appelé avant qu'un document soit indexé. Modifier context.document."""
        return context

    def after_index(self, context: MiddlewareContext) -> MiddlewareContext:
        """Appelé après qu'un document a été indexé."""
        return context

    def before_delete(self, context: MiddlewareContext) -> MiddlewareContext:
        """Appelé avant la suppression d'un document."""
        return context

    def after_delete(self, context: MiddlewareContext) -> MiddlewareContext:
        """Appelé après la suppression d'un document."""
        return context

    def before_search(self, context: MiddlewareContext) -> MiddlewareContext:
        """Appelé avant l'exécution d'une requête. Modifier context.query."""
        return context

    def after_search(self, context: MiddlewareContext) -> MiddlewareContext:
        """Appelé après le retour des résultats. Accéder à context.results."""
        return context

    def on_error(self, context: MiddlewareContext, exc: Exception) -> None:
        """Appelé quand une exception survient. Re-raise par défaut."""
        raise exc

    def on_commit(self, context: MiddlewareContext) -> None:
        """Appelé après une opération de commit."""
        pass
```

### MiddlewareContext

Localisé dans `whoosh.middleware.context`. L'objet contexte passé à chaque hook :

```python
class MiddlewareContext:
    def __init__(self, operation: str) -> None:
        self.operation: str           # ex: "add_document", "search"
        self.index: Any = None        # L'instance Index
        self.backend: Any = None       # Le backend de stockage
        self.writer: Any = None        # L'IndexWriter (si applicable)
        self.searcher: Any = None      # Le Searcher (si applicable)
        self.document: dict[str, Any] | None  # Document à indexer
        self.query: str = ""           # La chaîne de requête de recherche
        self.collector: Any = None     # Le collecteur (si applicable)
        self.results: Any = None       # Résultats de recherche
        self.labels: dict[str, Any] = {}    # Paires clé-valeur arbitraires
        self.metadata: dict[str, Any] = {} # Métadonnées par requête
```

Utilisez `context.copy()` pour créer une copie superficielle si vous devez préserver l'état.

### MiddlewareChain

Localisé dans `whoosh.middleware.chain`. Ordonnance l'exécution des middleware :

```python
from whoosh.middleware.chain import MiddlewareChain
from whoosh.middleware.context import MiddlewareContext

chain = MiddlewareChain([
    MetricsMiddleware(),
    CacheMiddleware(),
])

# Hooks before (dans l'ordre)
context = MiddlewareContext("search")
context.query = "hello world"
context = chain.run_before("before_search", context)

# ... opération de recherche core ...

# Hooks after (dans l'ordre inverse)
context = chain.run_after("after_search", context)
print(context.results)
```

**Support asynchrone** : Utilisez `async_run_before()`, `async_run_after()`, `async_run_on_error()` et `run_hook()` pour un middleware asynchrone.

### MiddlewareRegistry

Localisé dans `whoosh.middleware.registry`. Un registre au niveau de la classe pour les middleware nommés :

```python
from whoosh.middleware.registry import MiddlewareRegistry

MiddlewareRegistry.register("my_mw", MyMiddleware(), owner="my_plugin")
mw = MiddlewareRegistry.get("my_mw")
MiddlewareRegistry.unregister("my_mw")
print(MiddlewareRegistry.list_all())  # ['my_mw', ...]
```

## Intégration du Middleware

### Wrappers: MiddlewareWriter & MiddlewareSearcher

Localisés dans `whoosh.middleware.wrappers`. Ces wrappers enveloppent le writer/searcher core pour exécuter automatiquement les hooks de middleware :

```python
from whoosh.middleware.wrappers import MiddlewareWriter, MiddlewareSearcher
from whoosh.middleware.chain import MiddlewareChain

chain = MiddlewareChain([MetricsMiddleware(), CacheMiddleware()])

# Envelopper un writer
with MiddlewareWriter(ix.writer(), chain) as writer:
    writer.add_document(title="Hello", content="World")

# Envelopper un searcher
with MiddlewareSearcher(ix.searcher(), chain) as searcher:
    results = searcher.search(query)
```

### Assistants d'Intégration

Localisés dans `whoosh.middleware.integration` :

```python
from whoosh.middleware.integration import apply_middleware_to_writer, apply_middleware_to_searcher

# Charge automatiquement le middleware depuis PluginManager si chain non fournie
writer = apply_middleware_to_writer(ix.writer())
searcher = apply_middleware_to_searcher(ix.searcher())
```

## Middleware Intégrés

### Middleware Core (`whoosh.middleware.base`)

| Classe                  | Hooks              | Description                              |
|------------------------|--------------------|------------------------------------------|
| `CompressionMiddleware` | `before_index`    | Marque les documents avec `_compressed = True` |
| `EncryptionMiddleware`  | `before_index`    | Marque les documents avec `_encrypted = True`  |
| `MetricsMiddleware`     | `after_index`, `after_search` | Compte les documents indexés et les recherches |
| `CacheMiddleware`       | `before_search`, `after_search` | Mise en cache en mémoire des résultats |

### Observabilité (`whoosh.middleware.metrics`)

`PrometheusMiddleware` — exporte des métriques vers Prometheus (nécessite `prometheus-client`) :

```python
from whoosh.middleware.metrics import PrometheusMiddleware

# Nécessite: pip install whoosh-ng[metrics]
prom = PrometheusMiddleware()
# Exporte: whoosh_searches_total, whoosh_documents_indexed_total, whoosh_search_duration_seconds
```

### Middleware Moderne (`whoosh_modern.middleware`)

#### Middleware de Résilience (sous-classes du core)

`RetryMiddleware`, `LoggingMiddleware` et `CacheMiddleware` sont désormais des **sous-classes
du core `whoosh.middleware.base.Middleware`** (la même classe de base ré-exportée sous
`whoosh_modern.middleware.Middleware`). Ils participent au pipeline de hooks standard
(`before_index` / `after_index` / `before_search` / `after_search` / `on_error` /
`on_commit`) et conservent en plus un helper `wrap(operation)` permettant de décorer de
simples callables.

`MiddlewarePipeline` est un fin wrapper autour de `whoosh.middleware.chain.MiddlewareChain`
qui exécute un callable à travers les hooks de la chaîne et renvoie son résultat. L'ancien
module `whoosh_modern.middleware.pipeline` a été supprimé — importez ces noms directement
depuis `whoosh_modern.middleware`.

| Classe                  | Description                              |
|------------------------|------------------------------------------|
| `RetryMiddleware`      | Réessaie les opérations échouées avec backoff exponentiel |
| `LoggingMiddleware`    | Journalise le temps d'exécution et les erreurs |
| `CacheMiddleware`      | Met en cache les résultats d'opérations (éviction LRU) |
| `MiddlewarePipeline`   | Enchaîne plusieurs middleware via `MiddlewareChain` |

```python
from whoosh_modern.middleware import MiddlewarePipeline, RetryMiddleware, LoggingMiddleware

pipeline = MiddlewarePipeline(
    LoggingMiddleware(),
    RetryMiddleware(attempts=3, backoff="exponential", jitter=True),
)

result = pipeline.execute(lambda: my_index_operation())
```

#### Middleware de Stockage (`whoosh_modern.middleware.storage`)

| Classe                  | Description                              |
|------------------------|------------------------------------------|
| `StorageMiddleware`    | Redirige la persistance vers des fournisseurs de stockage pluginables |
| `FileStorageProvider`  | Stockage sur système de fichiers local   |
| `SQLiteStorageProvider`| Stockage blob SQLite                    |
| `S3StorageProvider`    | Stockage cloud S3 / S3-compatible       |

```python
from whoosh_modern.middleware.storage import StorageMiddleware, FileStorageProvider

storage = StorageMiddleware(FileStorageProvider("/data/index"), name="primary")
```

#### Middleware de Recherche (`whoosh_modern.middleware.search`)

| Classe                      | Description                              |
|----------------------------|------------------------------------------|
| `QueryRewriteMiddleware`   | Réécrit `context.query` avant la recherche |
| `RankingMiddleware`        | Re-classe `context.results` après la recherche |

```python
from whoosh_modern.middleware.search import QueryRewriteMiddleware

def add_synonyms(query: str) -> str:
    # Étendre la requête avec des synonymes avant l'exécution
    return query + " " + get_synonyms(query)

rewriter = QueryRewriteMiddleware(rewriter=add_synonyms)
```

#### Middleware d'Analyse (`whoosh_modern.middleware.analyzer`)

| Classe                  | Description                              |
|------------------------|------------------------------------------|
| `StemmingMiddleware`   | Applique un stemmer aux champs de document et à la requête |
| `SynonymMiddleware`    | Étend le texte avec des synonymes (placeholder) |

## Créer un Middleware Personnalisé

### Middleware Basé sur des Hooks

```python
from whoosh.middleware.base import Middleware
from whoosh.middleware.context import MiddlewareContext

class RequestLoggingMiddleware(Middleware):
    """Journaliser toutes les recherches avec le timing."""

    def before_search(self, context: MiddlewareContext) -> MiddlewareContext:
        import time
        context.metadata["_start_time"] = time.time()
        logger.info(f"[RECHERCHE] Requête: {context.query}")
        return context

    def after_search(self, context: MiddlewareContext) -> MiddlewareContext:
        elapsed = time.time() - context.metadata.get("_start_time", time.time())
        result_count = len(context.results) if context.results is not None else 0
        logger.info(f"[RÉSULTATS] Trouvé {result_count} résultats en {elapsed:.3f}s")
        return context
```

### Middleware Personnalisé (Basé sur des Hooks)

La classe de base `Middleware` est `whoosh.middleware.base.Middleware`, ré-exportée depuis
`whoosh_modern.middleware`. Sous-classez-la et implémentez les hooks du cycle de vie :

```python
from whoosh_modern.middleware import Middleware
from whoosh.middleware.context import MiddlewareContext

class RetryMiddleware(Middleware):
    """Réessaie les opérations échouées avec backoff (illustratif)."""

    def __init__(self, attempts: int = 3) -> None:
        self._attempts = attempts

    def on_error(self, context: MiddlewareContext, exc: Exception) -> None:
        # Les middleware de résilience intégrés implémentent déjà ce pattern.
        context.metadata.setdefault("retry_errors", 0)
        context.metadata["retry_errors"] += 1
        raise exc
```

> Note : les `RetryMiddleware`, `LoggingMiddleware` et `CacheMiddleware` intégrés exposent
> aussi un helper `wrap(operation)` (préservé pour compatibilité ascendante) leur permettant
> de décorer de simples callables, mais leur mécanisme principal reste le pipeline de hooks
> ci-dessus.

### Middleware avec Intégration de Plugin

Enregistrer du middleware via un plugin pour qu'il soit automatiquement découvert :

```python
from whoosh.plugins.manager import Plugin

class LoggingPlugin(Plugin):
    name = "logging"
    version = "1.0.0"
    middleware = ["whoosh_modern.middleware.LoggingMiddleware"]

    def register(self, manager):
        manager.register_middleware(
            "logging",
            LoggingMiddleware(),
        )
```

## Gestion des Erreurs

### StopOperation

Abandonner une opération de pipeline gracieusement :

```python
from whoosh.middleware.exceptions import StopOperation

class RateLimitMiddleware(Middleware):
    def before_search(self, context: MiddlewareContext) -> MiddlewareContext:
        if not rate_limiter.allow(context):
            raise StopOperation("Limite de taux dépassée")
        return context
```

### Comportement fail_open

```python
class ResilientMiddleware(Middleware):
    def on_error(self, context: MiddlewareContext, exc: Exception) -> None:
        try:
            send_to_analytics(context.results)
        except Exception:
            # Journaliser mais ne pas échouer la recherche
            logger.warning("Analytics failed", exc_info=True)
        # La chaîne de middleware continue
```

## Découverte de Middleware depuis les Plugins

Quand `PluginManager.load_plugins()` est appelé, tous les plugins qui déclarent une liste `middleware` auront leurs classes de middleware importées et instanciées. La méthode `get_middleware_chain()` construit une `MiddlewareChain` à partir de tous les middleware enregistrés :

```python
from whoosh.plugins.manager import PluginManager

PluginManager.load_plugins()  # Découvre les plugins et leurs middleware

manager = PluginManager._default
chain = manager.get_middleware_chain()
# chain est une MiddlewareChain prête à l'emploi
```

## Bonnes Pratiques

1. **Sans état** : Utilisez `context.metadata` pour les données par requête, pas les attributs d'instance
2. **Hooks légers** : Gardez les hooks `before_*` et `after_*` rapides ; utilisez async pour les E/S
3. **L'ordre compte** : Placez le cache avant les métriques, l'authentification avant le routage
4. **Fail fast** : N'utilisez `fail_open=True` que pour les middleware non critiques
5. **Testabilité** : Mockez le `MiddlewareContext` pour tester le middleware indépendamment
6. **Nettoyage** : Implémentez `shutdown()` pour les ressources comme les connexions et les minuteurs

## Voir Aussi

- [Guide Système de Plugins](plugins-avances.md) — Enregistrement et entry points des plugins
- [Intégration des Providers](provider-integration.md) — Guide complet du pipeline pour tous les providers
- [Exemples: Middleware](../examples/middleware.md) — Patterns de middleware pratiques
- [API: Middleware](../api/middleware.md) — Référence complète de l'API
- [API: Middleware Pipeline (moderne)](../api/modern.md) — Extensions middleware modernes


## DOCUMENT (FR): Middleware

# Middleware

Le pipeline de middleware permet d'intercepter et modifier les opérations d'indexation et de recherche. C'est le mécanisme d'extension principal pour les préoccupations transverses comme le logging, le cache, les métriques et la sécurité.

## Concepts de base

Un middleware est une classe qui implémente des hooks dans le cycle de vie :

```python
from whoosh.middleware.base import Middleware
from whoosh.middleware.context import MiddlewareContext

class MonMiddleware(Middleware):
    def before_search(self, context: MiddlewareContext) -> MiddlewareContext:
        # Modifier context.query ou context.metadata
        return context

    def after_search(self, context: MiddlewareContext) -> MiddlewareContext:
        # Accéder à context.results
        return context
```

## Hooks disponibles

| Hook | Quand | Utilisations courantes |
|------|------|------------------------|
| `startup(context)` | Initialisation | Ouvrir connexions, remplir caches |
| `shutdown(context)` | Nettoyage | Fermer connexions, flush buffers |
| `before_index(context)` | Avant indexation | Validation, enrichissement, flags compression |
| `after_index(context)` | Après indexation | Métriques, événements, invalidation cache |
| `before_delete(context)` | Avant suppression | Journalisation audit, contrôle d'accès |
| `after_delete(context)` | Après suppression | Métriques, invalidation cache |
| `before_search(context)` | Avant recherche | Réécriture de requête, cache, auth |
| `after_search(context)` | Après résultats | Logging, métriques, modification résultats |
| `on_error(context, exc)` | Sur exception | Gestion d'erreur, fallbacks |
| `on_commit(context)` | Après commit | Métriques, notifications |

## Classes intégrées

### MetricsMiddleware

```python
from whoosh.middleware import MetricsMiddleware

metrics = MetricsMiddleware()
# Après opérations:
stats = metrics.get_metrics()
# Retourne: {"documents_indexed": N, "searches_executed": N}
```

### CacheMiddleware

```python
from whoosh.middleware import CacheMiddleware

cache = CacheMiddleware()
cached = cache.get_cached("requête utilisateur")
cache.set_cached("requête utilisateur", results)
```

## MiddlewareChain

```python
from whoosh.middleware import MiddlewareChain

chain = MiddlewareChain([
    MetricsMiddleware(),
    CacheMiddleware()
])

# Exécuter un hook before
context = MiddlewareContext("search")
context.query = "test"
context = chain.run_before("before_search", context)

# ... opération core ...

# Exécuter un hook after
context = chain.run_after("after_search", context)
```

## Intégration

### Avec Writer

```python
from whoosh.middleware.integration import apply_middleware_to_writer

writer = apply_middleware_to_writer(ix.writer(), chain.middlewares)

with writer:
    writer.add_document(title="Bonjour", content="Monde")
```

### Avec Searcher

```python
from whoosh.middleware.integration import apply_middleware_to_searcher

searcher = apply_middleware_to_searcher(ix.searcher(), chain.middlewares)
results = searcher.search("query")
```

## Exemple: middleware personnalisé

```python
class RequestLoggingMiddleware(Middleware):
    """Journaliser toutes les recherches."""

    def before_search(self, context: MiddlewareContext):
        context.metadata["request_id"] = generate_request_id()
        logger.info(f"Recherche: {context.query}")
        return context

    def after_search(self, context: MiddlewareContext):
        logger.info(f"Trouvé: {len(context.results)} résultats")
        return context

class RateLimitMiddleware(Middleware):
    """Abandonner les recherches dépassant la limite."""

    def before_search(self, context: MiddlewareContext):
        if not rate_limiter.allow(context):
            raise StopOperation("Limite de taux dépassée")
        return context
```

## Gestion des erreurs

```python
class ResilientMiddleware(Middleware):
    """Continuer malgré les erreurs non critiques."""

    def after_search(self, context: MiddlewareContext) -> MiddlewareContext:
        try:
            send_to_analytics(context.results)
        except Exception:
            logger.warning("Analytics failed", exc_info=True)
        return context
```

## Bonnes pratiques

1. **Sans état**: Utilisez `context.metadata` pour les données par requête
2. **Fail fast**: Utilisez `fail_open=True` uniquement pour middleware non critique
3. **L'ordre compte**: Placez le cache avant les métriques, l'auth avant le routage
4. **Performance**: Gardez les hooks légers; utilisez async pour les I/O
5. **Testabilité**: Mockez le contexte pour tester le middleware isolément

## Middleware Moderne (Whoosh-NG 2.0)

Whoosh-NG 2.0 ajoute un package middleware moderne (`whoosh_modern.middleware`) avec un middleware de résilience de type wrapper (réessaissance, cache, journalisation) et un middleware basé sur des hooks pour le stockage, la recherche et l'analyse. Pour plus de détails sur l'architecture moderne du middleware, l'intégration de plugins et le déploiement, consultez le [Guide Middleware & Pipeline de Plugins](middleware-pipeline.md).


## DOCUMENT (FR): Modern Indexing

# Indexation Moderne

Whoosh-NG fournit une couche d'indexation optimisée dans `whoosh_modern.indexing` pour l'absorption de grands volumes de documents. Ces utilitaires encapsulent l'écrivain Whoosh de base sans modifier les internals de la bibliothèque.

## BatchIndexWriter

`BatchIndexWriter` encapsule un écrivain Whoosh avec les optimisations suivantes pour le traitement par lots de grands ensembles de données.

### Optimisations clés

- Pré-calcule les noms de champs du schéma pour un filtrage rapide (O(1) par champ)
- Ignore les champs non présents dans le schéma (évitant le surcoût par document)
- Utilise `multisegment=True` pour reporter les fusions pendant l'indexation
- Supporte des commits par lots configurables pour réduire la pression I/O
- Accepte un callback pour les hooks post-commit

### Utilisation de base

```python
from whoosh_modern.indexing import BatchIndexWriter
from whoosh import index

ix = index.open_dir("indexdir")

writer = BatchIndexWriter(ix, batch_size=5000, commit_every=10)

for batch in source.stream_batches(batch_size=5000):
    writer.add_batch(batch)

writer.close()
```

### Context Manager

```python
with BatchIndexWriter(ix, batch_size=10000) as writer:
    for doc in documents:
        writer.add_document(doc)
```

### Paramètres

| Paramètre | Par défaut | Description |
|-----------|---------|-------------|
| `batch_size` | 5000 | Nombre de documents par lot |
| `limitmb` | 512 | Limite mémoire pour l'écrivain (Mo) |
| `commit_every` | None | Commit après N lots (None = pas de commit auto) |
| `multisegment` | True | Utiliser le mode multisegment |
| `callback` | None | Callback invoqué après chaque commit |
| `**writer_kwargs` | None | Arguments supplémentaires pour `index.writer()` |

### Avec Commit Profiler

```python
from whoosh_modern.indexing import BatchIndexWriter
from whoosh_modern.profiling import CommitProfilerV2

profiler = CommitProfilerV2()
with BatchIndexWriter(ix, batch_size=5000, commit_every=5, commit_profiler=profiler) as writer:
    for batch in source.stream_batches(batch_size=5000):
        writer.add_batch(batch)

print(profiler.report())
```

---

## AnalyzerCache

`AnalyzerCache` fournit un cache LRU pour les résultats d'analyse, évitant les travaux d'analyse redondants sur des valeurs de champ répétées.

### Utilisation de base

```python
from whoosh_modern.indexing import BatchIndexWriter
from whoosh_modern.profiling import AnalyzerCache

cache = AnalyzerCache(maxsize=50000)
analyzer = StandardAnalyzer()

for doc in docs:
    cache_key = f"title:{doc['title']}"
    tokens = cache.get(cache_key)
    if tokens is None:
        tokens = list(analyzer(doc['title']))
        cache.put(cache_key, tokens)
```

### Avec get_or_compute

```python
from whoosh_modern.profiling import AnalyzerCache

cache = AnalyzerCache(maxsize=50000)
analyzer = StandardAnalyzer()

for doc in docs:
    tokens = cache.get_or_compute(
        f"title:{doc['title']}",
        lambda: list(analyzer(doc['title']))
    )
```

### Statistiques du cache

```python
print(f"Hit rate: {cache.hit_rate:.1%}")
print(f"Size: {cache.size}/{cache.maxsize}")
print(cache.report())
```

### Dimensionnement à partir de données de profiling

```python
from whoosh_modern.profiling import AnalyzerCache, CacheAnalyzer

analyzer = CacheAnalyzer()
analysis = analyzer.analyze(source.iter_documents())

cache = AnalyzerCache.from_profiling(analysis.to_dict())
```

---

## FieldAnalyzerCache

`FieldAnalyzerCache` encapsule un analyseur et met en cache les résultats par champ.

### Utilisation de base

```python
from whoosh_modern.profiling import FieldAnalyzerCache

field_cache = FieldAnalyzerCache(
    analyzer=StandardAnalyzer(),
    fields=["Country", "City"],
    cache_size=50000,
)

for doc in docs:
    for field in ["Country", "City"]:
        tokens = field_cache.analyze(field, doc[field])
```

### Invalidation du cache

```python
# Invalider une entrée spécifique
field_cache.invalidate("Country", "USA")

# Vider le cache entier
field_cache.clear()
```

### Statistiques du cache

```python
print(f"Hit rate: {field_cache.hit_rate:.1%}")
print(field_cache.report())
```

---

## Sources de données disponibles

| Classe | Type | Dépendances |
|-------|------|-------------|
| `SQLSource` | Bases SQL | `sqlite3` (stdlib) |
| `SQLAlchemySource` | SQLAlchemy | `sqlalchemy` |
| `RESTSource` | API REST | aucune (stdlib `urllib`) |
| `GraphQLSource` | API GraphQL | aucune (stdlib `urllib`) |
| `FastCSVSource` | Fichiers CSV | aucune |
| `JSONSource` | JSON/JSONL | aucune |
| `ParquetSource` | Parquet | `pyarrow` ou `pandas` |
| `PandasSource` | DataFrames pandas | `pandas` |
| `PolarsSource` | DataFrames Polars | `polars` |
| `PeeweeSource` | ORM Peewee | `peewee` |
| `TortoiseSource` | ORM Tortoise | `tortoise-orm` |
| `PydanticSource` | Modèles Pydantic | `pydantic` |


## DOCUMENT (FR): Monitoring

# Monitoring

Whoosh-NG inclut des hooks d'observabilité intégrés et un plugin Prometheus pour le monitoring en production.

## Métriques intégrées

### MetricsMiddleware

```python
from whoosh.middleware import MetricsMiddleware, MiddlewareChain
from whoosh.middleware.integration import apply_middleware_to_writer, apply_middleware_to_searcher

chain = MiddlewareChain([MetricsMiddleware()])

writer = apply_middleware_to_writer(ix.writer(), chain.middlewares)
searcher = apply_middleware_to_searcher(ix.searcher(), chain.middlewares)

# Obtenir les métriques
metrics = chain.get_metrics()
```

## Plugin Prometheus

### Installation

```bash
pip install whoosh-ng[metrics]
```

### Métriques exposées

| Métrique | Type | Description |
|----------|------|-------------|
| `whoosh_documents_indexed_total` | Counter | Total documents indexés |
| `whoosh_searches_executed_total` | Counter | Total recherches exécutées |
| `whoosh_indexing_duration_seconds` | Histogram | Temps d'indexation |
| `whoosh_search_duration_seconds` | Histogram | Temps de recherche |
| `whoosh_index_size_bytes` | Gauge | Taille actuelle de l'index |
| `whoosh_cache_hits_total` | Counter | Cache hits |
| `whoosh_cache_misses_total` | Counter | Cache misses |

## Event Bus pour monitoring

```python
from whoosh.event_bus import EventBus, DocumentIndexed, SearchExecuted

bus = EventBus()

@bus.subscribe
def on_indexed(event: DocumentIndexed):
    stats.increment("documents.indexed")

@bus.subscribe
def on_searched(event: SearchExecuted):
    stats.timing("search.duration", event.duration)
```

## Bonnes pratiques

1. **Ajoutez MetricsMiddleware tôt**: Incluez-le dans votre chaîne de base
2. **Exportez via Prometheus**: En production, exposez l'endpoint `/metrics`
3. **Endpoint health**: Utilisez `/health` pour les health checks load balancer
4. **Logging structuré**: Corrélez les événements search/index avec des request IDs
5. **Alerting**: Définissez des alertes sur les taux d'erreur et les latences


## DOCUMENT (FR): Performance

# Performance et Benchmarking

Whoosh-NG inclut un ensemble complet d'outils de benchmarking dans `whoosh_modern.profiling` pour mesurer et comparer les performances des analyseurs. Ce guide explique comment utiliser ces outils et documente les optimisations livrées dans la version 2.0.0.

## Démarrage rapide

```python
from whoosh_modern.profiling.benchmarks.regex_tokenizer import run_p5_1
from whoosh_modern.profiling.benchmarks.token_optimization import run_p5_2
from whoosh_modern.profiling.synthetic_datasets import SyntheticDatasetGenerator
from whoosh_modern.profiling.stemmer_benchmark import StemmerBenchmark

# Générer des datasets synthétiques pour des benchmarks cohérents
gen = SyntheticDatasetGenerator(seed=42)
datasets = gen.generate_all(count=5000)

# Exécuter le benchmark du tokeniseur (P5.1)
run_p5_1(datasets)

# Exécuter le benchmark de création de tokens (P5.2)
run_p5_2(token_count=100_000)

# Exécuter le benchmark du stemmer
bench = StemmerBenchmark()
bench.run(gen.generate_dataset("A", 5000))
print(bench.report())
```

## Outils de benchmarking

### SyntheticDatasetGenerator

Génère des datasets de texte déterministes de complexité variable :

```python
from whoosh_modern.profiling.synthetic_datasets import SyntheticDatasetGenerator

gen = SyntheticDatasetGenerator(seed=42)
datasets = gen.generate_all(count=5000)

# Dataset A: 2 tokens/doc (court)
# Dataset B: 50 tokens/doc (moyen)
# Dataset C: 500 tokens/doc (grand)
# Dataset D: 1200 tokens/doc (très grand)
for name, texts in datasets.items():
    print(f"{name}: {len(texts)} documents")
```

### P5.1 : Benchmark du RegexTokenizer

Compare différentes implémentations de tokeniseur :

```python
from whoosh_modern.profiling.benchmarks.regex_tokenizer import run_p5_1

results = run_p5_1(datasets)

# Compare :
# - Current Regex (whoosh par défaut)
# - Compiled Global regex
# - Manual Python tokenizer
# - C extension (re2, si disponible)
```

### P5.2 : Benchmark d'optimisation des tokens

Compare différentes implémentations d'objets Token :

```python
from whoosh_modern.profiling.benchmarks.token_optimization import run_p5_2

# Compare :
# - Current Token (dict-based)
# - __slots__ optimization
# - namedtuple
# - dataclass(slots=True)
results = run_p5_2(token_count=100_000)
```

### StemmerBenchmark

Compare les backends de stemming :

```python
from whoosh_modern.profiling.stemmer_benchmark import StemmerBenchmark

bench = StemmerBenchmark()
bench.run(texts, warmup=True)
print(bench.report())
# Résultats :
# Stemmer         Tokens/s        Time (s)    Tokens
# ------------------------------------------------------
# StemFilter      1,004,172       0.1503      150,983
# PyStemmer       ~2,100,000+     0.0719+     150,983
```

## Optimisations de performance

### Résumé des gains 2.0.0

| Optimisation | Composant | Gain mesurable |
|---|---|---|
| `__slots__` sur Token | `whoosh.analysis.acore` | +35% création de tokens |
| Regex globale compilée | `RegexTokenizer` | +50% débit regex |
| Postings compactés (1 posting) | `W3TermInfo` / `W3PostingsWriter` | +35% vitesse de commit |
| Postings compactés (2-8 postings) | `W3TermInfo` / `W3PostingsWriter` | +35% vitesse de commit |
| Cache de champ dans add_postings | `whoosh.codec.base` | -93% appels write_block |
| Encodage varint des positions | `whoosh.formats` | réduction de l'overhead par terme |
| Cache de stemmer | `whoosh.analysis.morph` | taux de hit 96,5 %, 4,12x sur champs répétitifs |
| Cache d'analyseur | `whoosh_modern.profiling.analyzer_cache` | 4,12x sur champs hautement répétitifs |
| Écrivain par lots | `whoosh_modern.indexing.batch_writer` | lots filtrés optimisés |
| Optimisation setdefault stopwords | `whoosh.formats` | réduction de l'overhead dict |

### Résultats de benchmark : 20 000 documents (`customers_csv`)

```
Avant :
  commit total      : 18,653 s
  analyzing         : 8,641 s  (51,5 %)
  committing        : 10,012 s  (27,1 %)
  write_postings    : 6,5 s
  write_block calls : ~72 612

Après :
  commit total      : 6,806 s  (-63,5 %)
  analyzing         : ~2,7 s   (-68 %)
  committing        : 6,806 s  (-32 %)
  write_postings    : 6,5 s -> allouations réduites
  write_block calls : 7 565   (-93 %)
  débit             : 1 275 docs/s
```

### Résultats de benchmark : Backends de stemming (1,5M de tokens)

| Stemmer | Débit | Relatif |
|---|---|---|
| StemFilter (interne) | 1 004 172 tokens/s | 1,0x |
| PyStemmer | ~2 100 000 tokens/s | ~2,1x |

### Résultats de benchmark : Tokeniseur regex

| Tokeniseur | Débit | Relatif |
|---|---|---|
| Current regex | ~1 000 000 tokens/s | 1,0x |
| Compiled global | ~2 300 000 tokens/s | 2,3x |

### Résultats de benchmark : Objet Token

| Implémentation | Tokens/s | Relatif |
|---|---|---|
| Current (dict) | 1 000 000 | 1,0x |
| `__slots__` | ~1 350 000 | 1,35x |

## Outils de profiling

### IndexingPipelineProfiler

Profile le pipeline d'indexation complet :

```python
from whoosh_modern.profiling.indexing_pipeline_profiler import IndexingPipelineProfiler

profiler = IndexingPipelineProfiler()
for doc in documents:
    profiler.before_tokenize(doc, analyzer)
    analyzer(doc)
    profiler.after_tokenize()

report = profiler.report()
print(report)
```

### CommitProfiler

Profile les performances de commit :

```python
from whoosh_modern.profiling.commit_profiler_v2 import CommitProfiler

profiler = CommitProfiler()
# ... indexer des documents ...
ix.commit()

report = profiler.report()
# Affiche : analyze, convert_fields, write_postings, flush, commit
```

### FieldIndexProfiler

Profile les coûts de conversion des champs :

```python
from whoosh_modern.profiling.field_index_profiler import FieldIndexProfiler

profiler = FieldIndexProfiler()
# ... indexer des documents ...
report = profiler.report()
```

### IndexQualityAnalyzer

Analyse les métriques de qualité de l'index :

```python
from whoosh_modern.profiling.index_quality_analyzer import IndexQualityAnalyzer

analyzer = IndexQualityAnalyzer(index_reader)
report = analyzer.analyze()
print(f"Termes singletons: {report['singleton_terms']}/{report['total_terms']} ({report['singleton_percent']}%)")
```

## Système de fournisseurs de stemmer

Whoosh-NG propose un système de fournisseurs de stemmer :

```python
from whoosh_modern.analysis import get_stemmer, StemmingAnalyzer, list_available_backends

# Vérifier les backends disponibles
print(list_available_backends())
# {'internal': 'available', 'pystemmer': 'not installed'}

# Utiliser la détection automatique (par défaut)
analyzer = StemmingAnalyzer(stemmer="auto")

# Stemmer interne explicite
analyzer = StemmingAnalyzer(stemmer="internal")

# PyStemmer (nécessite: pip install whoosh-ng[fast-stemming])
analyzer = StemmingAnalyzer(stemmer="pystemmer")
```

## Recommandations de performance

1. **Utilisez** `StemmingAnalyzer` de `whoosh_modern.analysis` pour une sélection automatique de PyStemmer
2. **Activez le cache de stemmer** pour le contenu répétitif (`cachesize=50000` par défaut)
3. **Minimisez les champs TEXT** — utilisez KEYWORD ou ID pour les champs à faible cardinalité
4. **Évitez les positions/chars stockés** sauf si la mise en évidence l'exige
5. **Utilisez l'indexation par lots** avec des segments plus grands pour de meilleurs débits
6. **Surveillez les termes singletons** — réduisez les termes rares via des listes de stopwords

## Exécution de la suite complète de benchmarks

```bash
cd whoosh-ng

# Exécuter tous les tests P5
uv run python -m pytest tests/test_regex_tokenizer_unicode.py tests/test_token_slots.py tests/test_stemmer_compatibility.py -v

# Suite de tests complète
uv run python -m pytest -q
```


## DOCUMENT (FR): Plugins Advanced

# Système de Plugins

Module : `whoosh.plugins.manager`
Version : 2.0.0

L'architecture à plugins de Whoosh-NG permet à des paquets externes d'étendre
le pipeline central d'indexation, de recherche et d'analyse. Les plugins sont
découverts via des [points d'entrée](https://docs.python.org/3/library/importlib.metadata.html#entry-points)
Python déclarés dans `pyproject.toml` et gérés par le `PluginManager`.

## Aperçu de l'architecture

```text
PluginManager (singleton)
    ├── load_plugins(group)          # Auto-découverte via les points d'entrée
    ├── register(plugin)             # Enregistrement manuel
    ├── enable(name) / disable(name) # Bascule du cycle de vie
    ├── get(name) / list_plugins()   # Inspection
    ├── get_middleware_chain()       # Construit la MiddlewareChain depuis les middlewares des plugins
    ├── register_datasource()        # Enregistre un provider de source de données
    ├── register_vector_provider()   # Enregistre un provider vectoriel
    ├── register_middleware()        # Enregistre une instance de middleware
    ├── register_embedding()         # Enregistre un provider d'embeddings
    ├── register_analyzer()          # Enregistre un analyseur nommé
    └── register_analyzer()          # Enregistre un rewriteur de requêtes
```

## Classes de base des plugins

### Plugin (ABC)

La classe de plugin racine. Les sous-classes définissent des attributs au niveau
classe et implémentent `register()`.

```python
from whoosh.plugins.manager import Plugin, PluginMetadata

class MyPlugin(Plugin):
    name = "my_plugin"
    version = "1.0.0"

    def register(self, manager: PluginManager) -> None:
        """Appelé au chargement du plugin ; enregistrez les providers ici."""
        manager.register_middleware("my_module.MyMiddleware", MyMiddleware())

    def register_hooks(self) -> None:
        """Enregistre les hooks d'événements (optionnel)."""
        from whoosh.hooks import hookimpl, register_hook

        @hookimpl
        def on_search(request, response):
            pass
        register_hook("on_search", hookimpl(on_search))
```

### AnalyzerPlugin

Pour les plugins fournissant des tokenizers/analyseurs personnalisés :

```python
from whoosh.plugins.manager import AnalyzerPlugin

class MyAnalyzerPlugin(AnalyzerPlugin):
    name = "my_analyzer"

    def register(self, manager):
        manager.register_analyzer("my_analyzer", MyTokenizer())
```

### QueryRewritePlugin

Pour les plugins transformant les requêtes avant exécution :

```python
from whoosh.plugins.manager import QueryRewritePlugin

class SynonymRewriterPlugin(QueryRewritePlugin):
    name = "synonym_rewriter"

    def rewrite(self, query, searcher):
        # Renvoie la requête modifiée
        return query
```

## PluginMetadata

Une dataclass décrivant les métadonnées du plugin :

| Champ         | Type              | Description                            |
|---------------|-------------------|----------------------------------------|
| `name`        | `str`             | Nom de plugin unique                   |
| `version`     | `str`             | Chaîne de version SemVer               |
| `depends_on`  | `list[str]`       | Noms des plugins requis                |
| `priority`    | `int`             | Priorité d'ordre de chargement (plus élevé = plus tard) |
| `middleware`  | `list[str]`       | Chemins pointés vers les classes de middleware |

## Groupes de points d'entrée

Le `PluginManager` découvre les plugins depuis ces groupes de points d'entrée
standard :

| Groupe                | Rôle                                 |
|----------------------|--------------------------------------|
| `whoosh.plugins`     | Plugins généraux                     |
| `whoosh.datasources` | Providers de source de données       |
| `whoosh.vector.providers` | Providers de similarité vectorielle |
| `whoosh.middlewares` | Classes de middleware                 |
| `whoosh.embeddings`  | Providers de modèles d'embeddings     |
| `whoosh.language`    | Analyseurs spécifiques à une langue  |
| `whoosh.apps`        | Usines d'applications (FastAPI, admin, etc.) |

## Créer et déployer un plugin

### Étape 1 : Définir la classe du plugin

```python
# my_plugin/plugin.py
from whoosh.plugins.manager import Plugin
from whoosh.registry import VectorRegistry

class MyVectorPlugin(Plugin):
    name = "my_vector"
    version = "1.0.0"
    depends_on = []
    conflicts_with = []
    priority = 0
    middleware = []

    def register(self, manager):
        """Enregistre un provider vectoriel dans le VectorRegistry."""
        provider = MyCustomVectorProvider()
        VectorRegistry.register("my_vector", provider, owner=self.name)

    def register_hooks(self):
        """Enregistre des hooks optionnels (ex : on_search, on_index)."""
        pass
```

### Étape 2 : Déclarer le point d'entrée

Dans votre `pyproject.toml` :

```toml
[project]
name = "whoosh-ng-my-vector"
version = "1.0.0"
dependencies = ["whoosh-ng>=2.0"]

[project.entry-points."whoosh_ng.plugins"]
my_vector = "my_plugin.plugin:MyVectorPlugin"
```

### Étape 3 : Installer et vérifier

```bash
pip install -e .
```

```python
# Vérifie que le plugin est enregistré
from whoosh.plugins.manager import PluginManager

PluginManager.load_plugins()  # Auto-découvre tous les points d'entrée

manager = PluginManager._default
print(manager.list_plugins())
# ['whoosh_autocomplete', 'whoosh_vector', ..., 'my_vector']

# Vérifie le registre
from whoosh.registry import VectorRegistry
print(VectorRegistry.list_keys())
# ['my_vector', 'numpy']
```

## Enregistrement manuel (sans point d'entrée)

Pour les tests ou un usage programmatique :

```python
from whoosh.plugins.manager import PluginManager

manager = PluginManager()
manager.register(MyVectorPlugin())
manager.enable("my_vector")
```

## Cycle de vie des plugins

```
1. Point d'entrée découvert  ───►  2. register() appelé  ───►  3. register_hooks()
   │                               │                            │
   └── load_plugins(group)          └── enregistre provider/     └── register_hook()
                                       middleware/analyzer
```

### Activation / Désactivation

```python
from whoosh.plugins.manager import PluginManager

manager = PluginManager._default

manager.enable("my_vector")    # Active un plugin
manager.disable("my_vector")   # Désactive un plugin
print(manager.list_enabled())  # Uniquement les plugins activés
```

### Validation de version

```python
# Vérifie si un plugin respecte une version minimale
ok = manager.validate_version("my_vector", "1.0.0")
print(ok)  # True si la version du plugin >= 1.0.0
```

### Détection de conflits

```python
# Vérifie si deux plugins sont en conflit
if manager.detect_conflicts("plugin_a", "plugin_b"):
    print("Ces plugins ne peuvent pas être chargés ensemble")
```

## Référence de l'API du Plugin Manager

### `PluginManager.load_plugins(group=None)`

Charge tous les plugins depuis les groupes de points d'entrée. Si `group` est
`None`, charge depuis tous les groupes standards (`STANDARD_GROUPS`).

### `PluginManager.register(plugin)`

Enregistre une instance de plugin. Appelle `plugin.register(self)` et
`plugin.register_hooks()`. Supporte un `register()` asynchrone via `asyncio`.

### `PluginManager.get_middleware_chain()`

Construit et renvoie une `MiddlewareChain` à partir de tous les plugins
déclarant des entrées `middleware`. Les classes de middleware sont importées et
instanciées par chemin pointé.

### Méthodes d'enregistrement dans les registres

| Méthode                       | Description                          |
|------------------------------|--------------------------------------|
| `register_analyzer(name, analyzer)` | Enregistre un analyseur nommé   |
| `register_datasource(name, datasource)` | Enregistre une source de données |
| `register_vector_provider(name, provider)` | Enregistre un provider vectoriel |
| `register_middleware(name, middleware)` | Enregistre une instance de middleware |
| `register_embedding(name, embedding)` | Enregistre un provider d'embeddings |
| `register_query_rewriter(plugin)` | Enregistre un plugin de rewriteur de requêtes |

### Méthodes de recherche

| Méthode                       | Renvoie                          |
|------------------------------|----------------------------------|
| `get(name)`                  | Instance `Plugin`                |
| `list_plugins()`             | Tous les noms de plugins enregistrés |
| `list_enabled()`             | Noms des plugins activés         |
| `get_analyzer(name)`         | Analyseur appelable              |
| `list_analyzers()`           | Noms des analyseurs enregistrés  |
| `list_datasources()`         | Noms des sources de données enregistrées |
| `list_vector_providers()`    | Noms des providers vectoriels enregistrés |
| `list_middlewares()`         | Noms des middlewares enregistrés |
| `list_embeddings()`          | Noms des embeddings enregistrés  |
| `list_query_rewriters()`     | Noms des rewriteurs de requêtes enregistrés |

## Plugins intégrés

| Plugin            | Module                  | Groupe de point d'entrée |
|-------------------|-------------------------|--------------------------|
| `whoosh_autocomplete` | `whoosh_modern.autocomplete.plugin` | `whoosh.plugins` |
| `whoosh_vector`   | `whoosh_modern.vector.plugin`      | `whoosh.plugins` |
| `whoosh_fastapi`  | `whoosh_fastapi`                  | `whoosh.apps` |
| `whoosh_observability` | `whoosh.middleware.metrics`  | `whoosh.middlewares` |
| `whoosh_admin`    | `whoosh_admin`                   | `whoosh.apps` |

## Bonnes pratiques

1. **Responsabilité unique** : un plugin, une fonctionnalité
2. **Déclarer les dépendances** : utilisez `depends_on` pour les plugins requis
3. **Versionnement sémantique** : incrémentez la version pour les changements d'API
4. **Dégradation gracieuse** : vérifiez les dépendances optionnelles dans `register()`
5. **Éviter les effets de bord dans `__init__`** : toute l'initialisation dans `register()`
6. **Nettoyage** : si applicable, fournissez une logique de teardown

## Voir aussi

- [Guide du Middleware](middleware-pipeline.md) — Hooks du pipeline et middleware personnalisés
- [Guide d'Intégration des Providers](provider-integration.md) — Guide complet du pipeline pour tous les providers
- [Exemple de Développement de Plugin](../examples/plugin-dev.md) — Tutoriel pas à pas d'un plugin
- [API : Plugins](../api/plugins.md) — Référence complète de l'API


## DOCUMENT (FR): Plugins

# Plugins

Whoosh-NG utilise une architecture à plugins pour garder le core léger tout en permettant des fonctionnalités avancées. Les plugins sont chargés via des entry points et gérés par le `PluginManager`.

## Architecture des plugins

```text
PluginManager
    ├── load_plugins()           # Auto-découvrir depuis entry points
    ├── register(name, plugin)   # Enregistrement manuel
    ├── enable(name)            # Activer un plugin
    ├── disable(name)           # Désactiver un plugin
    ├── get(name)               # Récupérer un plugin
    └── list_plugins()          # Lister tous les plugins
```

## Plugins intégrés

| Plugin | Description |
|--------|-------------|
| whoosh-ng-vector | Recherche vectorielle (NumPy, HNSW, Faiss) |
| whoosh-ng-autocomplete | Autocomplétion par edge n-gram |
| whoosh-ng-fastapi | Factory d'app FastAPI |
| whoosh-ng-observability | Métriques Prometheus |
| whoosh-ng-admin | Interface d'administration |

## Créer un plugin

Tout plugin hérite de `BasePlugin` :

```python
from whoosh.plugins.base import BasePlugin

class MonPlugin(BasePlugin):
    name = "mon_plugin"
    version = "1.0.0"
    dependencies = []

    def setup(self, registry):
        """Appelé quand le plugin est activé."""
        registry.register("mon_provider", MonProvider())

    def teardown(self, registry):
        """Appelé quand le plugin est désactivé."""
        registry.unregister("mon_provider")

    def middleware(self):
        """Middleware à injecter dans le pipeline."""
        return [MonMiddleware()]

    def on_startup(self):
        """Appelé une fois au démarrage."""
        pass

    def on_shutdown(self):
        """Appelé une fois à l'arrêt."""
        pass
```

## Enregistrement de plugin

### Via entry_points (pyproject.toml)

```toml
[project.entry-points."whoosh_ng.plugins"]
mon_plugin = "mon_package.plugin:MonPlugin"
```

### Programmatique

```python
from whoosh.plugins.manager import PluginManager

plugin = MonPlugin()
PluginManager.register("mon_plugin", plugin)
PluginManager.enable("mon_plugin")
```

## Cycle de vie d'un plugin

```
register() -> setup() -> enable() -> hooks middleware -> teardown() -> disable()
```

## Dépendances entre plugins

```python
class VectorPlugin(BasePlugin):
    name = "vector"
    dependencies = ["metrics"]  # Requiert le plugin metrics
```

Le `PluginManager` résout l'ordre de chargement et détecte les conflits.

## Bonnes pratiques

1. **Un plugin, une responsabilité**: Gardez les plugins petits et focalisés
2. **Déclarez les dépendances**: Aidez PluginManager à résoudre l'ordre
3. **Nettoyez bien**: Implémentez `teardown()` pour supprimer les registres

## Système de Plugins Moderne (Whoosh-NG 2.0)

Whoosh-NG 2.0 introduit un `PluginManager` amélioré avec support de registres pour les sources de données, les fournisseurs de vecteurs, les embeddings et le middleware. Pour plus de détails sur l'architecture moderne des plugins, les groupes d'entry points et le déploiement, consultez le [Guide Système de Plugins](plugins-avances.md).


## DOCUMENT (FR): Provider Integration

# Intégration des Providers : Guide Complet du Pipeline

Module: `whoosh_modern.storage`, `whoosh_modern.analysis.stemmer_providers`, `whoosh_modern.linguistics.synonyms`, `whoosh_modern.vector`, `whoosh_modern.autocomplete`
Version: 2.0.0

Ce guide explique comment tous les providers de Whoosh-NG s'intègrent dans le
pipeline d'indexation et de recherche. Il est la référence définitive pour
comprendre le flux de données des documents bruts aux résultats de recherche.

## Vue d'ensemble

Whoosh-NG utilise un **pattern de provider** pour garder le moteur de recherche
léger tout en activant un comportement pluggable pour le stockage, l'analyse de
texte, la recherche vectorielle et l'autocomplétion.

```
┌──────────────────────────────────────────────────────────────────────┐
│                        Pile de Providers Whoosh-NG                   │
│                                                                      │
│  ┌─────────────┐  ┌──────────────┐  ┌─────────────┐  ┌───────────┐ │
│  │ Stockage    │  │ Stemmer      │  │ Synonyme    │  │ Vecteur   │ │
│  │ Providers   │  │ Providers    │  │ Providers   │  │ Providers │ │
│  │             │  │              │  │             │  │           │ │
│  │ FileStorage │  │ Internal     │  │ Static      │  │ Numpy     │ │
│  │ S3Storage   │  │ PyStemmer    │  │ YAML        │  │ HNSW      │ │
│  │ Hybride     │  │ Identity     │  │ JSON        │  │ Faiss     │ │
│  │ SQLite      │  │ Custom       │  │ SQLite      │  │ Qdrant    │ │
│  └──────┬──────┘  └──────┬──────┘  └──────┬──────┘  └─────┬─────┘ │
│         │                │                │                │       │
│         ▼                ▼                ▼                ▼       │
│  ┌─────────────────────────────────────────────────────────────────┐│
│  │              Pipeline de Middleware (hooks)                     ││
│  │  ┌─────────────┐  ┌──────────────┐  ┌─────────────────────┐    ││
│  │  │Stockage     │  │Stemming      │  │Synonyme             │    ││
│  │  │Middleware   │  │Middleware    │  │ExpansionMiddleware  │    ││
│  │  └─────────────┘  └──────────────┘  └─────────────────────┘    ││
│  └─────────────────────────────────────────────────────────────────┘│
│                              │                                       │
│                              ▼                                       │
│  ┌─────────────────────────────────────────────────────────────────┐│
│  │              Moteur Core Whoosh                                 ││
│  │  Index │ Writer │ Searcher │ QueryParser │ Fichiers segment     ││
│  └─────────────────────────────────────────────────────────────────┘│
└──────────────────────────────────────────────────────────────────────┘
```

## Pipeline d'Indexation Complet

### Flux étape par étape

```text
┌─────────────────┐
│   DataSource    │  (SQL, JSON, REST, CSV, DataFrame, etc.)
│   .stream_batches() │
└────────┬────────┘
         │ lots de documents
         ▼
┌─────────────────┐
│  SchemaDiscovery │  Infère le schéma Whoosh depuis les colonnes de la source
│  .discover_schema() │
└────────┬────────┘
         │ Schema(TEXT, ID, NUMERIC, VECTOR, ...)
         ▼
┌─────────────────────────────────────────┐
│  Résolution du Provider de Stockage      │
│                                         │
│  storage._root (si présent)             │
│    └──► whoosh.index.create_in(root)    │
│  Pas de root                             │
│    └──► tempfile.mkdtemp() → create_in() │
└────────┬────────────────────────────────┘
         │ Instance Index
         ▼
┌─────────────────────────────────────────┐
│  Writer + MiddlewareChain                │
│                                         │
│  chain.run_before("before_index")        │
│    ├── StorageMiddleware                 │
│    │   └── marque le contexte            │
│    ├── StemmingMiddleware                │
│    │   └── stemme les champs             │
│    └── SynonymExpansionMiddleware        │
│        └── expande les champs            │
│                                         │
│  writer.add_document(**doc)              │
│    └── Whoosh core applique les analyzeurs│
│        (TEXT.analyzer)                    │
│        et écrit le segment               │
│                                         │
│  writer.commit()                         │
│    └── chain.run_after("on_commit")      │
│        └── StorageMiddleware             │
│            └── écrit un point de commit  │
└────────┬────────────────────────────────┘
         │ Fichiers segment sur le disque/S3/cache
         ▼
┌─────────────────┐
│  Index Whoosh    │
│  (segments)      │
└─────────────────┘
```

### Exemple concret

```python
from whoosh import index, fields
from whoosh_modern import (
    SearchApplication,
    SQLSource,
    HybridStorage,
    S3Storage,
    StemmingAnalyzer,
    get_stemmer,
    SynonymManager,
    SynonymExpansionMiddleware,
    StorageMiddleware,
    StemmingMiddleware,
)
from sqlalchemy import create_engine

# 1. Source de données
engine = create_engine("sqlite:///products.db")
source = SQLSource(query="SELECT id, name, description FROM products", connection=engine)

# 2. Stockage
remote = S3Storage(bucket="my-index-bucket", prefix="segments")
storage = HybridStorage(local_cache="./cache", remote=remote)

# 3. Schéma (auto-découvert des colonnes SQL)
#    Mais on personnalise l'analyseur
stemmer = get_stemmer("auto", "english")
schema = fields.Schema(
    name=fields.TEXT(analyzer=StemmingAnalyzer(stemmer=stemmer), stored=True),
    description=fields.TEXT(analyzer=StemmingAnalyzer(stemmer=stemmer)),
    id=fields.ID(stored=True, unique=True),
)

# 4. Créer l'index dans la racine du stockage
ix = index.create_in(storage._cache_root, schema)

# 5. Construire la chaîne de middleware
syn_manager = SynonymManager({"laptop": ["notebook", "portable"]})
chain = MiddlewareChain([
    StorageMiddleware(storage, name="products"),
    StemmingMiddleware(stemmer=stemmer.stem),
    SynonymExpansionMiddleware(syn_manager),
])

# 6. Indexer avec le middleware
with MiddlewareWriter(ix.writer(), chain) as writer:
    for batch in source.stream_batches():
        for doc in batch:
            writer.add_document(**doc)
    writer.commit()
```

## Pipeline de Recherche Complet

### Flux étape par étape

```text
┌─────────────────┐
│   Requête Utilisateur │  "running cats"
└────────┬────────┘
         │
         ▼
┌─────────────────────────────────────────┐
│  MiddlewareChain.run_before("search")    │
│                                         │
│  ├── StemmingMiddleware                  │
│  │   └── "running cats" → "run cat"      │
│  ├── SynonymExpansionMiddleware          │
│  │   └── "run cat" → "run cat running feline" │
│  └── QueryRewriteMiddleware              │
│      └── réécritures personnalisées      │
└────────┬────────────────────────────────┘
         │ Requête modifiée
         ▼
┌─────────────────────────────────────────┐
│  QueryParser.parse(query)                │
│    └── Objet Query (Term, And, Or...)    │
└────────┬────────────────────────────────┘
         │ Objet Query
         ▼
┌─────────────────────────────────────────┐
│  Searcher.search(query)                  │
│                                         │
│  ┌───────────────────────────────────┐  │
│  │  Chemin de recherche par mot-clé  │  │
│  │  └── lit les listes de postings │  │
│  │      depuis les fichiers segment   │  │
│  └───────────────────────────────────┘  │
│                                         │
│  ┌───────────────────────────────────┐  │
│  │  Chemin de recherche vectorielle  │  │
│  │  └── VectorRegistry.get(provider) │  │
│  │      └── NumpyProvider.search()   │  │
│  │          └── similarité cosinus   │  │
│  └───────────────────────────────────┘  │
│                                         │
│  ┌───────────────────────────────────┐  │
│  │  Chemin d'autocomplétion            │  │
│  │  └── AutocompleteRegistry.get()   │  │
│  │      └── provider.suggest()       │  │
│  └───────────────────────────────────┘  │
└────────┬────────────────────────────────┘
         │ Résultats bruts
         ▼
┌─────────────────────────────────────────┐
│  MiddlewareChain.run_after("search")     │
│                                         │
│  └── RankingMiddleware                   │
│      └── réclasse les résultats          │
└────────┬────────────────────────────────┘
         │ Résultats finaux
         ▼
┌─────────────────┐
│  Hits retournés   │
└─────────────────┘
```

### Exemple concret

```python
from whoosh.qparser import QueryParser
from whoosh_modern.middleware import (
    StemmingMiddleware,
    RankingMiddleware,
    QueryRewriteMiddleware,
)
from whoosh_modern.analysis import get_stemmer
from whoosh_modern.vector import NumpyProvider
from whoosh_modern.vector.plugin import VectorPlugin
from whoosh.plugins.manager import PluginManager
import numpy as np

# 1. Configurer les plugins au démarrage
manager = PluginManager()
VectorPlugin().register(manager)

# 2. Ouvrir l'index
ix = index.open_dir("indexdir")

# 3. Construire la chaîne de middleware
stemmer = get_stemmer("auto", "english")
chain = MiddlewareChain([
    StemmingMiddleware(stemmer=stemmer.stem),
    QueryRewriteMiddleware(rewriter=lambda q: q + " portable"),  # ajouter synonyme
    RankingMiddleware(ranker=lambda r: sorted(r, key=lambda h: h.score, reverse=True)),
])

# 4. Rechercher avec le middleware
with MiddlewareSearcher(ix.searcher(), chain) as searcher:
    # La requête est transformée par le middleware avant l'exécution
    results = searcher.search("laptop")
    for hit in results:
        print(f"{hit['name']}: {hit.score:.4f}")

    # 5. Recherche vectorielle (parallèle)
    query_vec = np.random.rand(384).tolist()
    vector_results = searcher.vector_search("embedding", query_vec, limit=10)
    for hit in vector_results:
        print(f"doc_id={hit.doc_id}, score={hit.score:.4f}")
```

## Matrice de Comparaison des Providers

| Aspect | Stockage | Stemmer | Synonyme | Vecteur | Autocomplétion |
|--------|----------|---------|----------|---------|----------------|
| **Point d'intégration** | `StorageMiddleware` + `create_in()` | `StemmingAnalyzer` (champ) + `StemmingMiddleware` | `SynonymExpansionMiddleware` | `VectorRegistry` + format de segment | `AutocompleteRegistry` + autonome |
| **Enregistrement** | Manuel ou `__getattr__` | Décorateur `register_stemmer()` | CRUD `SynonymManager` | `VectorPlugin.register()` | `AutocompletePlugin.register()` |
| **Utilisé à l'indexation** | Oui (points de commit) | Oui (analyseur de champ + middleware) | Oui (before_index) | Oui (champ VECTOR) | Non (autonome ou post-indexation) |
| **Utilisé à la recherche** | Oui (lectures de segments via le système de fichiers) | Oui (analyseur de champ + middleware) | Oui (before_search) | Oui (vector_search) | Oui (suggest/search) |
| **Persistance** | Fichiers segment / S3 / SQLite | En mémoire (sans état) | En mémoire / YAML / JSON / SQLite | Fichiers segment (métadonnées) | En mémoire (liste de phrases) |
| **Configuration** | Classe provider + kwargs | Nom du backend + langue | Dict de mapping ou fichier | Nom du provider + métrique | Type de provider + paramètres |

## Patterns Communs

### Pattern 1 : Provider comme Analyseur de Champ

Utilisé par: Stemmer providers, analyzeurs linguistiques

```python
schema = Schema(
    content=TEXT(analyzer=StemmingAnalyzer(stemmer="auto"))
)
```

Le provider est encapsulé dans un analyzeur Whoosh et appliqué automatiquement.

### Pattern 2 : Provider comme Middleware

Utilisé par: Stockage, Stemmer, Synonymes

```python
chain = MiddlewareChain([
    StorageMiddleware(storage),
    StemmingMiddleware(stemmer=stemmer.stem),
    SynonymExpansionMiddleware(manager),
])
```

Le provider est consommé par des hooks de middleware dans le pipeline.

### Pattern 3 : Provider comme Entrée du Registre

Utilisé par: Vecteur, Autocomplétion

```python
VectorRegistry.register("numpy", NumpyProvider(), owner="my_app")
provider = VectorRegistry.get("numpy", "my_app")
```

Le provider est stocké dans un registre global et résolu par nom à l'exécution.

### Pattern 4 : Provider comme Service Autonome

Utilisé par: Autocomplétion, Vecteur (mode manuel)

```python
provider = NumpyProvider()
provider.add([(doc_id, vec)])
results = provider.search(query_vec)
```

Le provider fonctionne indépendamment de l'index/searcher Whoosh.

## Bonnes Pratiques

1. **Choisir le bon pattern d'intégration** : Les analyseurs de champs pour les schémas statiques, le middleware pour le comportement dynamique, le registre pour les backends interchangeables.
2. **Éviter les double-applications** : Ne pas utiliser à la fois les analyseurs de niveau champ et le middleware pour la même transformation (ex: stemming).
3. **Enregistrer les providers au démarrage** : Appeler `VectorPlugin().register(manager)` et `AutocompletePlugin().register(manager)` avant de créer les index.
4. **Utiliser l'API de plus haut niveau quand c'est possible** : `SearchApplication` pour le bout en bout, `create_autocomplete()` pour les suggestions, `get_stemmer()` pour le stemming.
5. **Garder les providers sans état** : Les providers ne devraient pas contenir d'état spécifique à l'index; utilisez le contexte de middleware pour les données par requête.
6. **Tester les providers en isolation** : Chaque provider devrait être testable sans Whoosh core (tests unitaires pour `provider.search()`, `provider.add()`).
7. **Documenter les dépendances des providers** : Noter les dépendances optionnelles (boto3, PyStemmer, PyYAML) dans les exigences de votre projet.

## Voir Aussi

- [Guide des Fournisseurs de Stockage](storage-providers.md) — Intégration des backends de stockage
- [Guide des Stemmers](stemmers-fournisseurs.md) — Intégration des fournisseurs de stemmers
- [Guide de Recherche Vectorielle](vector.md) — Intégration des fournisseurs de vecteurs
- [Guide d'Autocomplétion](autocomplete-providers.md) — Intégration des fournisseurs d'autocomplétion
- [Guide Middleware](middleware-pipeline.md) — Pipeline hooks et adaptateurs de providers
- [Guide Plugins](plugins-advanced.md) — Enregistrement et entry points des plugins
- [API: Moderne](../api/modern.md) — Référence complète de l'API pour tous les providers


## DOCUMENT (FR): Stemming Providers

# Providers de Stemmers

Module : `whoosh_modern.analysis.stemmer_providers`, `whoosh_modern.analysis.stemming_analyzer`, `whoosh_modern.linguistics.stemmers`
Version : 2.0.0

Le système de providers de stemmers vous donne un contrôle flexible sur le
backend de stemming utilisé pour l'analyse de texte. Il prend en charge
l'auto-détection, la sélection explicite de backend et l'enregistrement de
stemmers personnalisés — le tout avec une API propre de style plugin.

## Aperçu du module

```text
whoosh_modern.analysis
    ├── stemmer_providers.py   # Protocole StemmerProvider, backends Internal/PyStemmer, register_stemmer, get_stemmer
    └── stemming_analyzer.py   # StemmingAnalyzer enrichi avec support de plugins

whoosh_modern.linguistics.stemmers
    └── __init__.py            # Analyseurs spécifiques à une langue (FR/EN/DE/ES/IT)
```

## Protocole StemmerProvider

Localisé dans `whoosh_modern.analysis.stemmer_providers` :

```python
from whoosh_modern.analysis.stemmer_providers import StemmerProvider

class MyStemmer(StemmerProvider):
    def stem(self, word: str) -> str:
        """Stem un seul mot."""
        ...

    @property
    def name(self) -> str:
        """Renvoie le nom du stemmer."""
        return "my_stemmer"

    @property
    def language(self) -> str:
        """Renvoie le code de langue."""
        return "english"
```

## Obtenir un stemmer

### Auto-détection (recommandée)

La fonction `get_stemmer("auto", language)` sélectionne automatiquement le
meilleur backend disponible :

```python
from whoosh_modern.analysis.stemmer_providers import get_stemmer

# Auto-détecte : privilégie PyStemmer si installé, repli sur le stemmer interne
stemmer = get_stemmer("auto", "english")
print(stemmer.stem("running"))  # "run"
print(stemmer.name)             # "pystemmer" ou "internal"
```

**Ordre de priorité :**
1. **PyStemmer** (le plus rapide, nécessite `pip install whoosh-ng[fast-stemming]`)
2. **Stemmer interne** (Porter stemmer intégré, toujours disponible)

### Sélection explicite du backend

```python
from whoosh_modern.analysis.stemmer_providers import get_stemmer

# Force le stemmer interne
stemmer = get_stemmer("internal", "english")

# Force PyStemmer (nécessite l'installation)
stemmer = get_stemmer("pystemmer", "english")
```

### Lister les backends disponibles

```python
from whoosh_modern.analysis.stemmer_providers import list_available_backends

backends = list_available_backends()
print(backends)
# {'internal': 'available', 'pystemmer': 'available', 'my_custom': 'registered'}
```

| Backend     | Chaîne de statut    | Nécessite                          |
|-------------|---------------------|------------------------------------|
| `internal`  | `"available"`       | Aucun (toujours fourni)            |
| `pystemmer` | `"available"` / `"not installed"` | `pip install whoosh-ng[fast-stemming]` |
| Custom      | `"registered"`      | Enregistré via `@register_stemmer` |

## Providers de stemmers intégrés

### InternalStemmerProvider

Encapsule le Porter stemmer intégré de Whoosh. Toujours disponible (aucune
dépendance supplémentaire) :

```python
from whoosh_modern.analysis.stemmer_providers import InternalStemmerProvider

stemmer = InternalStemmerProvider("english")
print(stemmer.stem("cats"))    # "cat"
print(stemmer.stem("running")) # "run"
```

### PyStemmerProvider

Encapsule la bibliothèque `Stemmer` pour un stemming haute performance. Prend en
charge toutes les langues Snowball :

```python
from whoosh_modern.analysis.stemmer_providers import PyStemmerProvider

# Nécessite : pip install whoosh-ng[fast-stemming]
stemmer = PyStemmerProvider("english")
print(stemmer.stem("cats"))    # "cat"
```

**Note** : ce provider appelle `self._stemmer.stemWord(word)` pour stemmer les
mots. Assurez-vous que PyStemmer est installé, sinon l'auto-détection repliera
sur le stemmer interne.

### IdentityStemmerProvider

Un stemmer sans effet pour les tests ou quand le stemming n'est pas souhaité :

```python
from whoosh_modern.analysis.stemmer_providers import IdentityStemmerProvider

stemmer = IdentityStemmerProvider()
print(stemmer.stem("anything"))  # "anything"
```

## Enregistrer un stemmer personnalisé

Utilisez le décorateur `@register_stemmer` :

```python
from whoosh_modern.analysis.stemmer_providers import register_stemmer

@register_stemmer("simple")
class SimpleStemmer:
    def stem(self, word: str) -> str:
        # Suppression simple de suffixe
        if word.endswith("s") and len(word) > 3:
            return word[:-1]
        return word

    @property
    def name(self) -> str:
        return "simple"

    @property
    def language(self) -> str:
        return "english"

# Utilisez-le maintenant
from whoosh_modern.analysis.stemmer_providers import get_stemmer

stemmer = get_stemmer("simple", "english")
print(stemmer.stem("cats"))  # "cat"
```

## StemmingAnalyzer (enrichi)

Localisé dans `whoosh_modern.analysis.stemming_analyzer`, c'est le point d'entrée
principal pour créer des analyseurs conscient de la langue :

```python
from whoosh_modern.analysis import StemmingAnalyzer

# Auto-détecte le meilleur stemmer pour l'anglais
analyzer = StemmingAnalyzer(stemmer="auto", language="english")

# Stemmer interne explicite
analyzer = StemmingAnalyzer(stemmer="internal", language="english")

# Backend PyStemmer (si installé)
analyzer = StemmingAnalyzer(stemmer="pystemmer", language="french")

# Provider de stemmer personnalisé
analyzer = StemmingAnalyzer(stemmer=my_stemmer_instance)
```

### Paramètres de StemmingAnalyzer

| Paramètre   | Type                          | Défaut                  | Description                      |
|-------------|-------------------------------|-------------------------|----------------------------------|
| `expression`| Motif regex                  | motif de token par défaut | Regex de tokenization          |
| `stoplist`  | Itérable de mots vides       | `whoosh.analysis.STOP_WORDS` | Mots vides à filtrer       |
| `minsize`   | `int`                         | `2`                     | Longueur minimale des tokens    |
| `maxsize`   | `int \| None`                 | `None`                  | Longueur maximale des tokens    |
| `gaps`      | `bool`                        | `False`                 | Découpe sur l'expression vs. le match |
| `stemmer`   | `str \| StemmerProvider`      | `"auto"`                | Backend de stemming             |
| `language`  | `str`                         | `"english"`             | Code de langue                  |
| `ignore`    | `set[str] \| None`            | `None`                  | Mots à ignorer                  |
| `cachesize` | `int`                         | `50000`                 | Taille du cache de stemming     |

### Utilisation avec les types de champs

```python
from whoosh_modern.analysis import StemmingAnalyzer
from whoosh.fields import Schema, TEXT

# Stemmer anglais avec mots vides
en_analyzer = StemmingAnalyzer("auto", language="english")

# Stemmer français
fr_analyzer = StemmingAnalyzer("auto", language="french")

schema = Schema(
    title=TEXT(stored=True),
    content_en=TEXT(analyzer=en_analyzer),
    content_fr=TEXT(analyzer=fr_analyzer),
)
```

## Analyseurs spécifiques à une langue

Analyseurs préconstruits pour cinq langues, disponibles dans
`whoosh_modern.linguistics.stemmers` :

```python
from whoosh_modern.linguistics.stemmers import (
    EnglishAnalyzer,
    FrenchAnalyzer,
    GermanAnalyzer,
    SpanishAnalyzer,
    ItalianAnalyzer,
)

# Chacun est appelable et renvoie une liste de tokens
en = EnglishAnalyzer()
tokens = en("The quick brown foxes")
# tokens sont stemmés : ["quick", "brown", "fox"] (mots vides comme "the" supprimés)
```

### Analyseurs de langue disponibles

| Classe             | Langue   | Module                              |
|--------------------|----------|-------------------------------------|
| `EnglishAnalyzer`  | Anglais  | `whoosh_modern.linguistics.stemmers` |
| `FrenchAnalyzer`   | Français | `whoosh_modern.linguistics.stemmers` |
| `GermanAnalyzer`   | Allemand | `whoosh_modern.linguistics.stemmers` |
| `SpanishAnalyzer`  | Espagnol | `whoosh_modern.linguistics.stemmers` |
| `ItalianAnalyzer`  | Italien  | `whoosh_modern.linguistics.stemmers` |

Chacun utilise en interne `get_stemmer("auto", language)` pour sélectionner le
meilleur backend disponible et applique les mots vides spécifiques à la langue.

## Validation de compatibilité des stemmers

Validez qu'un provider de stemmer fonctionne correctement avec un ensemble de
mots de test :

```python
from whoosh_modern.analysis.stemmer_providers import (
    get_stemmer,
    validate_stemmer_compatibility,
)

stemmer = get_stemmer("auto", "english")
report = validate_stemmer_compatibility(stemmer, ["running", "cats", "jumps", "houses"])

print(report["total_words"])   # 4
print(report["successful"])    # 4 (ou moins en cas d'erreur)
print(report["failed"])        # 0
print(report["results"])       # [{'word': 'running', 'stemmed': 'run', 'success': True}, ...]
```

### Structure du rapport de compatibilité

| Champ         | Type       | Description                          |
|---------------|------------|--------------------------------------|
| `provider`    | `str`      | Nom du provider de stemming          |
| `language`    | `str`      | Code de langue                       |
| `total_words` | `int`      | Nombre total de mots de test         |
| `successful`  | `int`      | Mots stemmés avec succès             |
| `failed`      | `int`      | Mots en échec                        |
| `results`     | `list[dict]` | Résultats par mot avec `word`, `stemmed`, `success` |

## Intégration avec StemmingMiddleware

Les providers de stemmers peuvent être utilisés avec le `StemmingMiddleware`
depuis `whoosh_modern.middleware.analyzer` :

```python
from whoosh_modern.analysis.stemmer_providers import get_stemmer
from whoosh_modern.middleware.analyzer import StemmingMiddleware

stemmer = get_stemmer("auto", "english")
middleware = StemmingMiddleware(
    stemmer=stemmer.stem,
    fields=["title", "content"],  # Stemme uniquement ces champs
    stem_query=True,              # Stemme aussi la requête de recherche
)
```

## Migration depuis le Whoosh classique

### Ancienne API (Whoosh 1.x/2.x)

```python
from whoosh.analysis import StemmingAnalyzer as OldAnalyzer
analyzer = OldAnalyzer("en")  # Code en dur sur "english"
```

### Nouvelle API (Whoosh-NG 2.0)

```python
from whoosh_modern.analysis import StemmingAnalyzer

# Auto-détecte le backend (recommandé)
analyzer = StemmingAnalyzer("auto", language="en")

# Ou utilisez un analyseur spécifique à une langue
from whoosh_modern.linguistics.stemmers import EnglishAnalyzer
analyzer = EnglishAnalyzer()
```

> **Note** : l'ancien `StemmingAnalyzer("en")` codait en dur la langue sur
> `"english"`. Le nouveau paramètre `StemmingAnalyzer(stemmer, language)` est
> explicite et prend en charge toutes les langues Snowball via PyStemmer.

## Installation

```bash
# Sans PyStemmer (utilise le stemmer interne, plus lent)
pip install whoosh-ng

# Avec PyStemmer (recommandé, plus rapide)
pip install whoosh-ng[fast-stemming]

# Analyse moderne complète
pip install whoosh-ng[modern]
```

## Intégration des providers de stemming dans le pipeline

Le système `StemmerProvider` s'intègre à **deux niveaux** : les analyseurs de
niveau champ et le middleware de pipeline. Comprendre les deux est essentiel
pour éviter le double stemming.

### Architecture

```text
┌─────────────────────────────────────────────────────────────────┐
│  StemmingAnalyzer (niveau champ, dans le Schema)                │
│  ┌───────────────────────────────────────────────────────────┐  │
│  │ RegexTokenizer() │ StopFilter │ StemmingAnalyzer          │  │
│  │                    (mots vides)   │                       │  │
│  │                                   ▼                       │  │
│  │                         stemfn = provider.stem            │  │
│  │                                   │                       │  │
│  │                                   ▼                       │  │
│  │                         Token(stemmed=True)                │  │
│  └───────────────────────────────────────────────────────────┘  │
│                                                                 │
│  Appliqué par le cœur de Whoosh à l'indexation ET à la recherche  │
│  (via QueryParser). Automatique, aucun middleware nécessaire.     │
└─────────────────────────────────────────────────────────────────┘

┌─────────────────────────────────────────────────────────────────┐
│  StemmingMiddleware (niveau pipeline)                            │
│  ┌───────────────────────────────────────────────────────────┐  │
│  │ before_index(context)                                     │  │
│  │   └── stemme toutes les valeurs str dans context.document │  │
│  │                                                             │  │
│  │ before_search(context)                                     │  │
│  │   └── stemme context.query si stem_query=True             │  │
│  └───────────────────────────────────────────────────────────┘  │
│                                                                 │
│  Accroché à la MiddlewareChain. Opt-in manuel.                  │
└─────────────────────────────────────────────────────────────────┘
```

### Niveau 1 : niveau champ (automatique)

Le `StemmingAnalyzer` encapsule le `StemmingAnalyzer` intégré de Whoosh et
injecte la méthode `.stem` d'un `StemmerProvider` comme `stemfn`. Le cœur de
Whoosh l'applique automatiquement au champ à l'indexation et à la recherche.

```python
from whoosh.fields import Schema, TEXT
from whoosh_modern.analysis import StemmingAnalyzer, get_stemmer

# Auto-détecte le meilleur stemmer (PyStemmer privilégié)
stemmer = get_stemmer("auto", "english")

# Crée l'analyseur avec la fonction de stemming du provider
analyzer = StemmingAnalyzer(stemmer=stemmer)

schema = Schema(
    title=TEXT(stored=True),
    content=TEXT(analyzer=analyzer),
)

# À l'indexation : "running cats" → ["run", "cat"]
# À la recherche : QueryParser utilise aussi le même analyseur
# donc "running cats" correspond aux documents contenant "run cat"
```

**Avantages** : automatique, aucune configuration de middleware requise,
comportement index/requête cohérent.

**Inconvénients** : nécessite de définir l'analyseur sur chaque champ `TEXT`.
Plus difficile à modifier à l'exécution.

### Niveau 2 : niveau middleware (opt-in)

`StemmingMiddleware` applique le stemming au niveau du pipeline, opérant sur
les valeurs chaîne brutes dans `context.document` et `context.query` avant que
les analyseurs de Whoosh ne les voient.

```python
from whoosh_modern.middleware import StemmingMiddleware
from whoosh_modern.analysis import get_stemmer
from whoosh.middleware.chain import MiddlewareChain
from whoosh.middleware.wrappers import MiddlewareWriter

stemmer = get_stemmer("auto", "english")

chain = MiddlewareChain([
    StemmingMiddleware(
        stemmer=stemmer.stem,
        fields=["title", "content"],  # None = tous les champs str
        stem_query=True,
    ),
])

with MiddlewareWriter(ix.writer(), chain) as writer:
    writer.add_document(title="Running cats", content="Fast dogs")
    # before_index stemme : "Running cats" → "run cat"
    writer.commit()
```

**Avantages** : fonctionne sur n'importe quel champ sans modifier le schéma.
Peut être activé/désactivé à l'exécution.

**Inconvénients** : doit être câblé manuellement dans le pipeline. Risque de
double stemming si le champ utilise aussi `StemmingAnalyzer`.

### Exemple de pipeline complet : index + recherche

```python
from whoosh import index, fields
from whoosh.qparser import QueryParser
from whoosh_modern.analysis import StemmingAnalyzer, get_stemmer
from whoosh_modern.middleware import StemmingMiddleware
from whoosh.middleware.chain import MiddlewareChain
from whoosh.middleware.wrappers import MiddlewareWriter, MiddlewareSearcher

# 1. Schéma avec analyseur de niveau champ
stemmer = get_stemmer("auto", "english")
schema = fields.Schema(
    title=fields.TEXT(stored=True, analyzer=StemmingAnalyzer(stemmer=stemmer)),
    content=fields.TEXT(analyzer=StemmingAnalyzer(stemmer=stemmer)),
)

ix = index.create_in("indexdir", schema)

# 2. Index avec middleware (pas de double stemming car
#    on n'utilise pas StemmingMiddleware quand les champs ont déjà StemmingAnalyzer)
with ix.writer() as writer:
    writer.add_document(title="Running cats", content="Fast dogs")
    writer.commit()

# 3. Recherche : QueryParser applique le même analyseur à la requête
with ix.searcher() as searcher:
    qp = QueryParser("content", schema)
    q = qp.parse("running cats")
    results = searcher.search(q)
    # "running" est stemmé en "run" par l'analyseur
    # "cats" est stemmé en "cat" par l'analyseur
    # Correspond au document contenant "run" et "cat"
```

### Éviter le double stemming

```python
# FAUX : double stemming
schema = Schema(
    content=TEXT(analyzer=StemmingAnalyzer(stemmer="auto")),
)
chain = MiddlewareChain([
    StemmingMiddleware(stemmer=get_stemmer("auto").stem),  # À ne pas faire !
])
# Résultat : "running" → "run" (analyseur) → "run" (middleware) — inoffensif mais gaspilleur

# CORRECT : choisissez UN seul niveau
# Option A : niveau champ uniquement (recommandé pour les schémas statiques)
schema = Schema(content=TEXT(analyzer=StemmingAnalyzer(stemmer="auto")))
# Aucun StemmingMiddleware nécessaire

# Option B : middleware uniquement (pour les champs dynamiques)
schema = Schema(content=TEXT)  # Pas d'analyseur
chain = MiddlewareChain([StemmingMiddleware(stemmer=get_stemmer("auto").stem)])
```

### Provider de stemmer personnalisé

```python
from whoosh_modern.analysis import register_stemmer, get_stemmer

@register_stemmer("my_stemmer")
class MyStemmer:
    def stem(self, word: str) -> str:
        return word.lower().rstrip("s")

# Utilisez-le comme n'importe quel backend intégré
stemmer = get_stemmer("my_stemmer", "english")
analyzer = StemmingAnalyzer(stemmer=stemmer)
```

## Voir aussi

- [Guide Stemming et Mots Vides](../core/stemming.md) — Guide classique de stemming de Whoosh
- [Guide Synonymes & Linguistique](linguistics.md) — Moteur d'expansion de synonymes
- [Guide d'Intégration des Providers](provider-integration.md) — Guide complet du pipeline pour tous les providers
- [API : Linguistique](../api/modern.md) — Référence complète de l'API pour les extensions d'analyse


## DOCUMENT (FR): Storage Providers

# Fournisseurs de stockage

Whoosh-NG fournit des backends de stockage modulaires via les contrats
`SyncStorageProvider` / `AsyncStorageProvider`. Cela permet de persister
l'index sur le disque local, SQLite, S3, ou une configuration hybride
(cache local + distant) sans modifier l'écrivain ou l'index.

## Aperçu de l'architecture

### Niveau 1 : SnapshotStorage (Simple)

```
Writer → Local FS → Commit → Upload Segment → S3
Reader → Download Segment → Open locally
```

Très simple à maintenir. Utilisez `SnapshotStorage` quand vous voulez
utiliser S3 comme cible de sauvegarde/restauration simple sans la
complexité d'un cache local.

### Niveau 2 : CachedObjectStorage (Recommandé pour la production)

```
+----------+
|  MinIO   |
+----------+
    ^
    |
Sync |
    v
+-----------+   Cache Layer   +-----------+
| Searcher  |<--------------->| Writer    |
+-----------+                 +-----------+
         |
         v
  Local SSD
```

- L'index réside sur un SSD
- S3 sert de réplication
- Les segments sont poussés après le commit
- La restauration est possible à tout moment

C'est ce que font de nombreux systèmes de recherche distribués modernes.

## Fournisseurs disponibles

| Fournisseur | Type | Backend | Cas d'utilisation |
|-------------|------|---------|--------------------|
| `FileStorage` | sync | système de fichiers local | Nœud unique, pas de cloud |
| `AsyncFileStorage` | async | système de fichiers local | Nœud unique async |
| `S3Storage` | sync | compatible S3 | Accès direct S3 |
| `SnapshotStorage` | sync | compatible S3 | Sauvegarde/restauration simple |
| `HybridStorage` | sync | cache local + distant | **Production** (alias : `CachedObjectStorage`) |
| `AsyncHybridStorage` | async | cache local + distant | Production async |

Tous les fournisseurs sont importables depuis `whoosh_modern.storage`.

## FileStorage

Stockage sur le système de fichiers local. Les clés sont des chemins
relatifs sous `root`.

```python
from whoosh_modern.storage import FileStorage

storage = FileStorage("indexdir")
storage.write("segment_1.dat", b"data")
assert storage.read("segment_1.dat") == b"data"
assert storage.exists("segment_1.dat") is True
storage.delete("segment_1.dat")
keys = storage.list_keys()
```

## AsyncFileStorage

 Variante asynce de `FileStorage`. Toutes les opérations s'exécutent sur
un thread de travail via `asyncio.to_thread` afin de ne jamais bloquer
la boucle d'événements.

```python
import asyncio
from whoosh_modern.storage import AsyncFileStorage

storage = AsyncFileStorage("indexdir")

async def main() -> None:
    await storage.awrite("segment_1.dat", b"data")
    data = await storage.aread("segment_1.dat")
    await storage.adelete("segment_1.dat")

asyncio.run(main())
```

## S3Storage

Stockage d'objets compatible S3. `boto3` est importé de manière paresseuse,
il s'agit donc d'une dépendance optionnelle. Un `client` peut être injecté
pour les tests.

```python
from whoosh_modern.storage import S3Storage

# Client par défaut (nécessite boto3 installé et configuré)
storage = S3Storage(bucket="my-index-bucket", prefix="segments")

# Ou injecter un client pour tests / configuration personnalisée
storage = S3Storage(
    bucket="my-index-bucket",
    prefix="segments",
    client=my_boto3_client,
)

storage.write("segment_1.dat", b"data")
data = storage.read("segment_1.dat")
keys = storage.list_keys()
```

Installez la dépendance optionnelle :

```bash
pip install whoosh-ng[s3]
```

## SnapshotStorage

Stockage d'instantané S3 simple sans cache local. C'est la stratégie de
stockage S3 la plus simple :

- Écriture : téléverse le segment directement vers S3
- Lecture : télécharge le segment depuis S3 vers un fichier temporaire local

Utilisez cela quand vous voulez utiliser S3 comme cible de
sauvegarde/restauration simple sans la complexité d'un cache local.

```python
from whoosh_modern.storage import SnapshotStorage

storage = SnapshotStorage(
    local_path="./index",
    bucket="my-index-bucket",
    prefix="snapshots",
)

storage.write("segment_1.dat", b"data")
data = storage.read("segment_1.dat")
```

## HybridStorage / CachedObjectStorage

`HybridStorage` compose un cache local et un backend distant. Le backend
distant est la source de vérité ; le cache local est un anneau de
performance en écriture-transparente.

`CachedObjectStorage` est un alias pour `HybridStorage` qui exprime
mieux l'intention : un cache d'objets local synchronisé avec S3.

C'est l'architecture recommandée pour les déploiements de production
avec des modèles de lecture répétés.

```python
from whoosh_modern.storage import HybridStorage, S3Storage

remote = S3Storage(bucket="my-index-bucket", prefix="segments")
storage = HybridStorage(local_cache="./cache", remote=remote)

# Write-through : le distant est la source de vérité, le cache est mis à jour en cas de succès
storage.write("segment_1.dat", b"data")

# Première lecture : cache manquant → récupère depuis S3, écrit dans le cache
data = storage.read("segment_1.dat")

# Deuxième lecture : cache hit → servi depuis le disque local, zéro réseau
data = storage.read("segment_1.dat")

# Forcer le rafraîchissement depuis le distant
storage.invalidate("segment_1.dat")

# Préchauffer le cache de manière proactive
storage.prefetch(["segment_2.dat", "segment_3.dat"])
```

### Chemin de lecture

1. cache local hit → retourne immédiatement
2. cache manquant → lit depuis le distant, écrit dans le cache, retourne

### Chemin d'écriture

- `remote.write(key, data)` (source de vérité)
- en cas de succès → `local_cache.write(key, data)`
- en cas d'échec → lève l'erreur avant de polluer le cache

### Éviction du cache

Le cache local est borné par `max_cache_size_mb` (par défaut 1024 Mo).
Lorsque la limite est atteinte, les entrées les plus anciennes sont
évictées selon une politique LRU.

### `list_keys`

`list_keys()` utilise le backend distant comme source de vérité car le
cache n'est qu'partiel. Passez `include_cache=True` pour retourner
l'union des clés distantes et du cache.

## AsyncHybridStorage

 Variante asynce de `HybridStorage`. Les opérations distantes sont
exécutées sur un thread de travail via `asyncio.to_thread` afin de ne
jamais bloquer la boucle d'événements.

```python
import asyncio
from whoosh_modern.storage import AsyncHybridStorage, S3Storage

remote = S3Storage(bucket="my-index-bucket", prefix="segments")
storage = AsyncHybridStorage(local_cache="./cache", remote=remote)

async def main() -> None:
    await storage.awrite("segment_1.dat", b"data")
    data = await storage.aread("segment_1.dat")
    await storage.adelete("segment_1.dat")
    keys = await storage.alist_keys()

asyncio.run(main())
```

## Utilisation du stockage avec SearchApplication

```python
from whoosh_modern import SearchApplication, SQLSource
from whoosh_modern.storage import HybridStorage, S3Storage

remote = S3Storage(bucket="my-index-bucket", prefix="segments")
storage = HybridStorage(local_cache="./cache", remote=remote)

app = SearchApplication(
    source=SQLSource(query="SELECT * FROM products", connection=engine),
    storage=storage,
)
app.build()
results = app.index.search("laptop")
```

## Benchmarks de performance

Les benchmarks ont été exécutés contre une instance MinIO locale en
utilisant un index Whoosh de 28,89 Mo (2 fichiers de segment). Les
résultats sont indicatives de la performance relative entre les
stratégies sur du stockage compatible S3.

| Stratégie | Sauvegarde (Mo/s) | Restauration (Mo/s) | Notes |
|-----------|-------------------|---------------------|-------|
| `1_obj_per_segment` | 39.44 | 139.72 | Meilleur débit de restauration ; le plus simple |
| `compressed_zstd` | 31.56 | 133.74 | Moins de bande passante, surcharge CPU |
| `hybrid_cache_s3` | 44.97 | 133.61 | Meilleur sauvegarde ; lectures excellentes avec cache chaud |
| `1_obj_per_posting_list` | 0.28 | 4.79 | **À éviter** : des millions de petits objets tuent S3 |

### Recommandations

- **Par défaut** : `S3Storage` avec 1 objet par fichier de segment. Offre
  le meilleur débit de restauration et est le plus simple à exploiter.
- **Production avec lectures répétées** : `HybridStorage(local_cache, S3Storage)`.
  Après la première lecture, les lectures suivantes sont servies depuis le
  disque local à ~133 Mo/s.
- **À éviter** : 1 objet par liste de postings. S3 n'est pas optimisé
  pour des millions de petits objets ; la latence et les coûts explosent.
- **Compression** : ZSTD réduit la taille de transfert d'environ 20-30%
  au coût du CPU. Utilisez-le quand la bande passante réseau est le
  goulot d'étranglement, pas quand c'est le CPU.

### Exécution des benchmarks

```bash
# Start MinIO
docker run -d --name minio-benchmark -p 9000:9000 -p 9001:9001 \
  -e MINIO_ROOT_USER=minioadmin -e MINIO_ROOT_PASSWORD=minioadmin \
  minio/minio:latest server /data --console-address ":9001"

# Run synthetic benchmark
python benchmark/s3_storage_benchmark.py

# Run real Whoosh index benchmark (requires customers CSV)
python benchmark/s3_storage_benchmark_real.py
```

## Comment les Fournisseurs de Stockage s'Intègre dans le Pipeline d'Indexation et de Recherche

Le fournisseur de stockage participe à deux phases distinctes : **la création de l'index** (déterminer où les segments vivent) et **l'intégration en temps réel** (via `StorageMiddleware`).

### Flux d'indexation complet avec un fournisseur de stockage

```text
DataSource.stream_batches()
    │
    ▼
SearchApplication.build()
    │
    ├── source.discover_schema() ──► Whoosh Schema
    │
    ├── résolution de storage._root
    │       │
    │       ├── HybridStorage/S3Storage/FileStorage
    │       │   └── a _root ? ──► whoosh.index.create_in(root, schema)
    │       │
    │       └── Pas de _root (S3 pur/SnapshotStorage)
    │           └── tempfile.mkdtemp() ──► create_in(tmpdir, schema)
    │
    ├── Writer = index.writer()
    │       │
    │       ├── MiddlewareChain.before_index()
    │       │   └── StorageMiddleware.before_index()
    │       │       ├── context.labels["storage_backend"] = provider.__class__.__name__
    │       │       └── context.metadata["storage_provider"] = self
    │       │
    │       ├── for batch in source.stream_batches():
    │       │       for doc in batch:
    │       │           writer.add_document(**doc)
    │       │
    │       └── writer.commit()
    │           │
    │           └── StorageMiddleware.on_commit()
    │               └── provider.write("commits/{name}/{timestamp}", b"1")
    │
    ▼
Index persistsé sur le disque / S3 / cache hybride
```

### Flux de recherche complet avec un fournisseur de stockage

```text
SearchApplication.search(query)
    │
    ├── index.searcher()
    │       │
    │       └── Whoosh core ouvre les fichiers segment depuis :
    │           ├── système de fichiers local (racine FileStorageProvider)
    │           ├── base de données SQLite (SQLiteStorageProvider)
    │           └── S3 / cache hybride (S3StorageProvider / HybridStorage)
    │
    ├── QueryParser.parse(query) ──► Objet Query
    │
    └── searcher.search(query)
        │
        └── Whoosh core lit les listes de postings depuis les fichiers segment
            └── Retourne Results (Hits)
```

### Hooks détaillés de StorageMiddleware

`StorageMiddleware` (`whoosh_modern.middleware.storage`) est le point d'intégration
qui redirige la persistance de l'index via n'importe quel `SyncStorageProvider` sans
modifier le writer.

| Hook | Quand | Ce qu'il fait |
|------|------|--------------|
| `before_index(context)` | Avant qu'un document soit ajouté | Marque le contexte avec l'étiquette `storage_backend` et les métadonnées `storage_provider` |
| `on_commit(context)` | Après `writer.commit()` | Écrit un point de commit (`commits/{name}/{timestamp}`) dans le provider |

### Insight clé : StorageProvider vs StorageMiddleware

| Composant | Rôle |
|-----------|------|
| `SyncStorageProvider` / `AsyncStorageProvider` | **Contrat** définissant `write()`, `read()`, `delete()`, `exists()`, `list_keys()` |
| `FileStorageProvider`, `S3StorageProvider`, `HybridStorage` | **Implémentations** du contrat |
| `StorageMiddleware` | **Couche d'intégration** qui appelle le provider aux hooks de cycle de vie (`before_index`, `on_commit`) |
| `SearchApplication` | **Point d'entrée** qui extrait `_root` du provider pour créer le répertoire d'index Whoosh |

Le provider ne **n'intercepte pas** les lectures internes de segments de Whoosh. Ces lectures passent par le `FileStorage` intégré de Whoosh (`whoosh.filedb.filestore`) qui lit depuis le chemin du système de fichiers donné à `create_in()`. L'abstraction provider de Whoosh-NG est conçue pour :
- Le routage personnalisé de segments (S3, SQLite, cache hybride)
- Le pointage de commit via le middleware
- Futur : l'interception de lectures/écritures au niveau des segments

## Voir Aussi

- [Intégration des Providers](provider-integration.md) — Guide complet du pipeline pour tous les providers
- [Guide Middleware](middleware-pipeline.md) — Pipeline hooks et adaptateurs de providers


## DOCUMENT (FR): Synonyms

# Synonymes

Module: `whoosh_modern.linguistics.synonyms`
Version: 3.0.0

Le moteur de synonymes fournit une expansion de synonymes au moment de la requête et de l'indexation via un système de providers pluggables. Il supporte les mappings statiques en mémoire, les fichiers YAML/JSON, la persistance SQLite, et les dictionnaires Wiktionary à grande échelle.

## Architecture des providers

Tous les providers de synonymes implémentent le protocole `SynonymProvider` :

```python
from whoosh_modern.linguistics.synonyms import SynonymProvider

class MyProvider(SynonymProvider):
    def get_synonyms(self, word: str) -> list[str]: ...
    def add_synonym(self, word: str, synonyms: list[str]) -> None: ...
    def remove_synonym(self, word: str, synonym: str) -> None: ...
```

## Providers intégrés

### StaticSynonymProvider

Provider en mémoire sauvegardé par un dictionnaire :

```python
from whoosh_modern.linguistics.synonyms import StaticSynonymProvider

provider = StaticSynonymProvider({
    "voiture": ["automobile", "véhicule"],
    "maison": ["domicile", "résidence"],
})
print(provider.get_synonyms("voiture"))  # ['automobile', 'véhicule']
```

### YAMLSynonymProvider

Charge les synonymes depuis un fichier YAML :

```yaml
# synonyms.yaml
voiture:
  - automobile
  - véhicule
maison:
  - domicile
  - résidence
```

```python
from whoosh_modern.linguistics.synonyms import YAMLSynonymProvider

provider = YAMLSynonymProvider("synonyms.yaml")
print(provider.get_synonyms("voiture"))  # ['automobile', 'véhicule']
```

### JSONSynonymProvider

Charge les synonymes depuis un fichier JSON :

```json
{
    "voiture": ["automobile", "véhicule"],
    "maison": ["domicile", "résidence"]
}
```

```python
from whoosh_modern.linguistics.synonyms import JSONSynonymProvider

provider = JSONSynonymProvider("synonyms.json")
print(provider.get_synonyms("voiture"))
```

### WiktionarySynonymProvider

Charge les synonymes depuis un fichier JSON Lines kaikki.org :

```python
from whoosh_modern.linguistics.synonyms import WiktionarySynonymProvider

provider = WiktionarySynonymProvider(
    "src/whoosh_modern/linguistics/dictionaries/wiktionary/fr.json"
)
print(provider.get_synonyms("voiture"))  # ['automobile', 'véhicule']
```

Chaque ligne du fichier dictionnaire est un objet JSON :

```json
{"word": "voiture", "s": ["automobile", "véhicule"]}
{"word": "ordinateur", "s": ["pc", "machine"]}
```

Le provider filtre :
- Les mots contenant des espaces (expressions multi-mots)
- Les entrées avec parties du discours non standard
- Les listes de synonymes vides ou manquantes

### SQLiteSynonymStore

Store de synonymes persistant sauvegardé par SQLite :

```python
from whoosh_modern.linguistics.synonyms import SQLiteSynonymStore

store = SQLiteSynonymStore("synonyms.db")
store.add_synonym("voiture", ["automobile", "véhicule"])
print(store.get_synonyms("voiture"))  # ['automobile', 'véhicule']
store.close()
```

## SynonymManager

`SynonymManager` est l'interface de haut niveau pour gérer les synonymes :

```python
from whoosh_modern.linguistics.synonyms import SynonymManager

manager = SynonymManager({"voiture": ["automobile", "véhicule"]})

# CRUD
manager.add_synonyms("maison", ["domicile", "résidence"])
print(manager.get_synonyms("maison"))  # ['domicile', 'résidence']
manager.remove_synonym("maison", "domicile")

# Import depuis des sources externes
manager.import_yaml("synonyms.yaml")       # Requiert PyYAML
manager.import_json("synonyms.json")
manager.import_wiktionary("dictionaries/wiktionary/fr.json")

# Export
manager.export_json("output.json")
```

## Mise à jour des dictionnaires Wiktionary

Les dictionnaires pré-générés se trouvent dans `src/whoosh_modern/linguistics/dictionaries/wiktionary/` :

```
wiktionary/
├── fr.json
├── en.json
├── de.json
├── es.json
├── it.json
├── manifest.json
└── README.md
```

Pour les régénérer depuis le dernier dump kaikki.org :

```bash
python scripts/update_wiktionary_dictionaries.py --all
```

Ou pour une seule langue :

```bash
python scripts/update_wiktionary_dictionaries.py --lang fr
```

Le script télécharge `kaikki.org-dictionary-all.jsonl`, extrait les synonymes par langue, filtre par tags POS autorisés, et écrit des fichiers JSON Lines compacts par langue.

## SynonymExpansionMiddleware

Intègre l'expansion de synonymes dans le pipeline de middleware :

```python
from whoosh_modern.linguistics.synonyms import (
    SynonymManager,
    SynonymExpansionMiddleware,
)

manager = SynonymManager({
    "voiture": ["automobile", "véhicule"],
    "maison": ["domicile", "résidence"],
})
middleware = SynonymExpansionMiddleware(manager)
```

Le middleware étend à la fois les requêtes de recherche et les documents indexés :

```python
# Expansion de requête
ctx = MiddlewareContext(operation="search")
ctx.query = "voiture"
ctx = middleware.before_search(ctx)
# ctx.query == "voiture automobile véhicule"

# Expansion de document
ctx = MiddlewareContext(operation="index")
ctx.document = {"title": "maison à vendre"}
ctx = middleware.before_index(ctx)
# ctx.document["title"] == "maison à vendre domicile résidence"
```

## Synonymes préconstruits par langue

`LANG_SYNONYMS` fournit des dictionnaires de démarrage pour cinq langues :

```python
from whoosh_modern.linguistics.synonyms import LANG_SYNONYMS

french_syns = LANG_SYNONYMS["fr"]
print(french_syns["voiture"])  # ['automobile', 'véhicule']

english_syns = LANG_SYNONYMS["en"]
print(english_syns["car"])  # ['automobile', 'vehicle']
```

| Langue   | Code | Entrée exemple                          |
|----------|------|----------------------------------------|
| Français | `fr` | `"voiture": ["automobile", "véhicule"]` |
| Anglais  | `en` | `"car": ["automobile", "vehicle"]`      |
| Allemand | `de` | `"auto": ["wagen", "fahrzeug"]`         |
| Espagnol | `es` | `"coche": ["automóvil", "vehículo"]`    |
| Italien  | `it` | `"auto": ["automobile", "veicolo"]`     |

## Exemple d'intégration

```python
from whoosh_modern.linguistics import (
    LANG_SYNONYMS,
    SynonymExpansionMiddleware,
    SynonymManager,
)
from whoosh.middleware.chain import MiddlewareChain
from whoosh.middleware.wrappers import MiddlewareWriter, MiddlewareSearcher

# 1. Construit le manager de synonymes
syn_manager = SynonymManager(LANG_SYNONYMS["fr"])
syn_manager.add_synonyms("recherche", ["query", "cherche"])

# 2. Crée le middleware
syn_middleware = SynonymExpansionMiddleware(syn_manager)

# 3. Construit la chaîne de middleware
chain = MiddlewareChain([syn_middleware])

# 4. Utilise avec writer/searcher
with MiddlewareWriter(ix.writer(), chain) as writer:
    writer.add_document(title="Comment faire une recherche dans Whoosh")

with MiddlewareSearcher(ix.searcher(), chain) as searcher:
    results = searcher.search("recherche")
```

## Intégration de l'indexation Wiktionary

`WiktionaryIndexer` peut alimenter les synonymes directement dans `SynonymManager` et `SearchApplication`.

### SynonymManager.import_wiktionary_index()

Peuple un manager depuis un index Whoosh construit :

```python
from whoosh_modern.linguistics.synonyms import SynonymManager
from whoosh_modern.linguistics.wiktionary_indexer import WiktionaryIndexer

indexer = WiktionaryIndexer("indexdir")
# ... build_index() appelé précédemment ...

manager = SynonymManager()
manager.import_wiktionary_index("indexdir", language="fr")
print(manager.get_synonyms("voiture"))
# ['automobile', 'véhicule']
```

### Intégration SearchApplication

Passe un `WiktionaryIndexer` à `SearchApplication` pour exposer un `synonym_manager` pré-peuplé :

```python
from whoosh_modern import SearchApplication
from whoosh_modern.linguistics.wiktionary_indexer import WiktionaryIndexer

indexer = WiktionaryIndexer("indexdir")
app = SearchApplication(wiktionary_indexer=indexer)

# synonym_manager est peuplé paresseusement depuis l'index
manager = app.synonym_manager
```

### Câblage de SynonymExpansionMiddleware

Combine avec le middleware pour étendre les requêtes au moment de la recherche :

```python
from whoosh_modern.linguistics.synonyms import SynonymExpansionMiddleware

middleware = SynonymExpansionMiddleware(app.synonym_manager)
```

## Voir aussi

- [Vue d'ensemble linguistique](linguistics.md) — Stemmers, analyseurs de langue, et intégration complète du pipeline
- [Pipeline de middleware](middleware-pipeline.md) — Fonctionnement des chaînes de middleware
- [Providers de stemming](stemming-providers.md) — Backends de stemming spécifiques aux langues


## DOCUMENT (FR): Vector

# Recherche vectorielle

Whoosh-NG supporte la recherche sémantique via des embeddings vectoriels. Ce guide couvre la configuration et l'utilisation des champs vectoriels.

## Concept

La recherche vectorielle permet de trouver des documents par similarité sémantique plutôt que par correspondance exacte de mots-clés.

```
Embedding requête  ----\
                       >--- Similarité cosinus ---> Résultats classés
Embedding document ---/
```

## Configuration

```python
from whoosh.fields import Schema, TEXT, VectorField

schema = Schema(
    title=TEXT(stored=True),
    content=TEXT,
    embedding=VectorField(dimensions=384)  # ex: all-MiniLM-L6-v2
)
```

## Providers

| Provider | Description | Cas d'usage |
|----------|-------------|-------------|
| `NumpyProvider` | NumPy pur, similarité cosinus | Petits/moyens indexes |
| `HNSWProvider` | Hierarchical Navigable Small World | Gros indexes, ANN rapide |
| `FaissProvider` | Facebook AI Similarity Search | Très gros indexes |
| `QdrantProvider` | Qdrant vector DB | Distribué |

## Indexation avec vecteurs

```python
from sentence_transformers import SentenceTransformer

model = SentenceTransformer("all-MiniLM-L6-v2")
embeddings = model.encode([
    "Premier document",
    "Deuxième document"
])

with ix.writer() as writer:
    writer.add_document(
        title="Doc 1",
        content="Python est génial",
        embedding=embeddings[0].tolist()
    )
    writer.commit()
```

## Recherche hybride (mots-clés + vecteur)

```python
with ix.searcher() as searcher:
    # Composante sémantique
    query_embedding = model.encode(["Tutoriel Python"])[0]
    vector_results = searcher.vector_search(
        "embedding", query_embedding, limit=20
    )

    # Composante mots-clés
    keyword_query = QueryParser("content", schema).parse("Python")
    keyword_results = searcher.search(keyword_query, limit=20)

    # Combiner (ex: fusion RRF)
    final_results = fuse_results(vector_results, keyword_results)
```

## Bonnes pratiques

1. **Normalisez les embeddings**: Utilisez la similarité cosinus avec des vecteurs normalisés
2. **Choisissez le provider wisely**: Numpy pour &lt;100k vecteurs, HNSW/Faiss pour plus
3. **Recherche hybride**: Combinez vecteur et mots-clés pour de meilleurs résultats
4. **Cachez les embeddings**: Pré-calculez et stockez pour éviter de recalculer
5. **Indexation par lots**: Indexez les vecteurs en lots pour l'efficacité

## Intégration des Fournisseurs de Vecteurs dans le Pipeline

Le système de recherche vectorielle s'intègre via le registre de plugins de Whoosh
et le format de segment. Le provider est stocké dans le segment d'index et résolu
au moment de la recherche.

### Architecture

```text
┌─────────────────────────────────────────────────────────────────────┐
│  Enregistrement (démarrage)                                         │
│                                                                     │
│  VectorPlugin.register(PluginManager)                              │
│    └── VectorRegistry.register("numpy", NumpyProvider(), owner)     │
│                                                                     │
│  Le provider est maintenant disponible pour tout champ VECTOR      │
│  qui spécifie provider="numpy"                                     │
└─────────────────────────────────────────────────────────────────────┘

┌─────────────────────────────────────────────────────────────────────┐
│  Indexation                                                          │
│                                                                     │
│  VECTOR(dimensions=384, provider="numpy")                          │
│       │                                                             │
│       ▼                                                             │
│  PerDocWriter.add_vector_items(fieldname, field, items)            │
│       │                                                             │
│       ▼                                                             │
│  Le fichier segment contient :                                     │
│    - octets vectoriels (bruts)                                     │
│    - nom du provider ("numpy")                                     │
│    - métrique ("cosine")                                           │
│       │                                                             │
│       ▼                                                             │
│  writer.commit() → segments écrits sur le disque                  │
└─────────────────────────────────────────────────────────────────────┘

┌─────────────────────────────────────────────────────────────────────┐
│  Recherche                                                         │
│                                                                     │
│  searcher.vector_search("embedding", query_vec, k=10)              │
│       │                                                             │
│       ▼                                                             │
│  Whoosh core lit le segment                                        │
│    └── récupère le nom du provider ("numpy")                        │
│       │                                                             │
│       ▼                                                             │
│  VectorRegistry.get("numpy")                                        │
│       │                                                             │
│       ▼                                                             │
│  NumpyProvider.search(query_vec, k, filter_ids)                    │
│       │                                                             │
│       ▼                                                             │
│  VectorHit[] trié par similarité cosinus                           │
└─────────────────────────────────────────────────────────────────────┘
```

### Chaîne de résolution du provider

Quand `searcher.vector_search()` est appelé, Whoosh core :

1. Lit la configuration du champ `VECTOR` depuis le schéma
2. Ouvre le fichier segment contenant les données vectorielles
3. Extrait le nom du provider stocké dans le segment (ex: `"numpy"`)
4. Recherche le provider dans `VectorRegistry`
5. Appelle `provider.search(query_vector, k, filter_ids)`
6. Retourne `list[VectorHit]`

Si le provider n'est pas enregistré, la recherche échoue avec un manquant de registre.
C'est pourquoi `VectorPlugin().register(manager)` (ou l'enregistrement manuel) est
requise au démarrage.

## Voir Aussi

- [Intégration des Providers](provider-integration.md) — Guide complet du pipeline pour tous les providers
- [Guide Middleware](middleware-pipeline.md) — Pipeline hooks et adaptateurs de providers


# Code Examples
