> ## Documentation Index
> Fetch the complete documentation index at: https://docs.wethehivers.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Recherche plein-texte

> Indexation tsvector, pondération, autocomplete et filtres

# Recherche plein-texte

Les offres et les candidats sont indexés en PostgreSQL via **`tsvector`** avec configuration `french`. Les résultats sont triés par pertinence + date.

## Pipeline d'indexation

```mermaid theme={null}
flowchart LR
    NEW[INSERT / UPDATE offre] --> TRIG[Trigger AFTER]
    TRIG --> VECT[Générer tsvector\n weights A/B/C/D]
    VECT --> COL[search_vector]
    COL --> GIN[Index GIN]
    GIN --> QRY[Requête rapide]
```

## Pondération des colonnes

```mermaid theme={null}
flowchart TD
    DOC[Document Offre] --> A[Titre\n weight A]
    DOC --> B[Compétences\n weight B]
    DOC --> C[Secteur + type contrat\n weight C]
    DOC --> D[Description\n weight D]

    A --> VECT[search_vector]
    B --> VECT
    C --> VECT
    D --> VECT

    style A fill:#264194,color:#fff
    style B fill:#4A63B5,color:#fff
    style C fill:#7B8DCB,color:#fff
    style D fill:#B2BEE2,color:#000
```

| Weight | Facteur de rang | Champs                                                  |
| ------ | --------------- | ------------------------------------------------------- |
| A      | 1.0             | `titre`                                                 |
| B      | 0.4             | `competences_requises` (tableau)                        |
| C      | 0.2             | `secteur`, `type_contrat`, `ville`, `niveau_experience` |
| D      | 0.1             | `description`                                           |

## Requête type

```sql theme={null}
SELECT
  o.*,
  ts_rank_cd(o.search_vector, query, 32) AS rank
FROM offres o,
  websearch_to_tsquery('french', :q) AS query
WHERE
  o.search_vector @@ query
  AND o.status = 'PUBLISHED'
ORDER BY rank DESC, o.published_at DESC
LIMIT :size OFFSET :page * :size;
```

## Syntaxe utilisateur (`websearch_to_tsquery`)

| Input                   | Interprétation                  |          |
| ----------------------- | ------------------------------- | -------- |
| `développeur java`      | `développeur & java`            |          |
| `développeur OR python` | \`développeur                   | python\` |
| `"lead developer"`      | Phrase exacte                   |          |
| `java -senior`          | `java & !senior`                |          |
| `dev*`                  | Préfixe (avec `tsquery` custom) |          |

## Endpoints concernés

| Endpoint                          | Index                     |
| --------------------------------- | ------------------------- |
| `GET /v1/api/offres/search`       | `offres.search_vector`    |
| `GET /v1/api/offres/autocomplete` | `offres.titre` (trigram)  |
| `GET /v1/api/admin/candidates`    | `candidats.search_vector` |
| `GET /v1/api/vivier/search`       | idem                      |

## Autocomplete

Séparé de la recherche principale — utilise **pg\_trgm** (trigrams) pour la tolérance aux fautes.

```mermaid theme={null}
flowchart LR
    Q[saisie 'dev'] --> TRIG[pg_trgm similarity]
    TRIG --> RES[Top 10 titres\n similaires]
    RES --> CACHE[Cache Redis 60s]
    CACHE --> CLI[Frontend]
```

```sql theme={null}
SELECT DISTINCT titre
FROM offres
WHERE status = 'PUBLISHED'
  AND titre % :q              -- similarity > threshold
ORDER BY similarity(titre, :q) DESC, titre
LIMIT 10;
```

## Filtres combinables

```mermaid theme={null}
flowchart TD
    REQ["GET /offres/search?q=java\n&secteur=SOFTWARE\n&ville=Douala\n&typeContrat=CDI\n&niveauExperience=SENIOR\n&salaireMin=500000"] --> BUILD[Query builder]
    BUILD --> FTS[WHERE search_vector @@ query]
    BUILD --> SEC[AND secteur IN ...]
    BUILD --> V[AND ville ILIKE ...]
    BUILD --> TC[AND type_contrat = ...]
    BUILD --> NE[AND niveau_experience = ...]
    BUILD --> SAL[AND salaire_min >= ...]
    FTS --> SQL[SELECT + pagination]
    SEC --> SQL
    V --> SQL
    TC --> SQL
    NE --> SQL
    SAL --> SQL
```

## Paramètres de recherche

| Paramètre          | Type   | Exemple                                |
| ------------------ | ------ | -------------------------------------- |
| `q`                | string | `développeur java`                     |
| `secteur`          | array  | `SOFTWARE,FINANCE`                     |
| `ville`            | string | `Douala`                               |
| `typeContrat`      | array  | `CDI,CDD`                              |
| `niveauExperience` | string | `SENIOR`                               |
| `salaireMin`       | int    | `500000`                               |
| `dateDebut`        | date   | `2026-01-01`                           |
| `entrepriseId`     | long   | `42`                                   |
| `page`             | int    | `0`                                    |
| `size`             | int    | `20`                                   |
| `sort`             | string | `relevance,desc` / `published_at,desc` |

## Limitations

<Warning>
  * La recherche `tsvector` ignore les mots vides français (stopwords). Ex. "le", "de", "un" ne sont jamais indexés.
  * La longueur max de `q` est 200 caractères.
  * `websearch_to_tsquery` supporte les opérateurs simples ; pour `prefix*` il faut passer par `/autocomplete`.
</Warning>

## Rafraîchissement de l'index

L'index GIN est mis à jour en **synchrone** via trigger. Pas de job asynchrone nécessaire.

```sql theme={null}
CREATE TRIGGER offres_search_vector_update
BEFORE INSERT OR UPDATE ON offres
FOR EACH ROW EXECUTE FUNCTION update_offres_search_vector();
```

## Monitoring

```mermaid theme={null}
flowchart LR
    SLOW[Requêtes > 500ms] --> LOG[pg_stat_statements]
    LOG --> ALERT[Grafana dashboard\n api-latency]
    ALERT --> OPS[Oncall investigation]
```

## Candidats — recherche recruteur

La recherche candidats (accessible aux recruteurs via vivier) est plus restreinte :

* Le candidat doit avoir opté pour la visibilité publique de son profil
* Les critères sont : compétences, niveau d'expérience, secteur, ville
* La description libre et le CV ne sont **pas** indexés dans `search_vector` (RGPD)
