---
title: "Construire un moteur de recherche d'emploi sémantique avec Bright Data, LanceDB et Cohere"
slug: semantic-job-search-engine-with-bright-data-lancedb-and-cohere
date: 2026-08-12T16:46:28+00:00
modified: 2026-09-06T15:56:59+00:00
permalink: https://brightdata.fr/blog/ai/semantic-job-search-engine-with-bright-data-lancedb-and-cohere
type: blog
---

[ Blog ](https://brightdata.fr/blog "Blog") / [AI](https://brightdata.fr/blog/ai)







 [AI](https://brightdata.fr/blog/ai)

# Construire un moteur de recherche d’emploi sémantique avec Bright Data, LanceDB et Cohere

Construire un moteur de recherche d’emploi sémantique. Le Web Scraper de Bright Data retourne des offres LinkedIn structurées ; Cohere fournit des embeddings pour la correspondance basée sur le sens.

 32 min de lecture





 [ ![Satyam Tripathi](https://media.brightdata.fr/2024/09/Satyam-Tripathi-50x50.png) ](https://brightdata.com/blog/authors/satyam-tripathi)

 [Satyam Tripathi

Technical Writer

 ](https://brightdata.com/blog/authors/satyam-tripathi)





 ![Build a Semantic Job Search Engine with Bright Data, LanceDB, and Cohere](https://media.brightdata.fr/2026/08/Semantic-Job-Search-Engine-with-Bright-Data-LanceDB-and-Cohere.svg)





Les sites d’emploi ne recherchent que par mots exacts, donc le bon poste reste caché quand votre formulation ne correspond pas à l’annonce. La recherche sémantique associe sur le sens à la place. Nous la construisons de bout en bout, puis mesurons quel mode de recherche gagne plutôt que de supposer que le plus complexe l’emporte.

## TL;DR

Ce guide construit un moteur de recherche d’emploi sémantique sur 200 vraies offres LinkedIn en utilisant Bright Data (Scraping web), Cohere (embeddings + rerank) et LanceDB (vector store local).

- La recherche par mots-clés correspond aux mots exacts. La recherche vectorielle correspond au sens. Une requête comme « ingénieur qui travaille sur les LLMs » trouve un poste « Développeur GenAI » que la recherche par mots-clés rate.
- L’API Web Scraper de Bright Data retourne des offres LinkedIn structurées en JSON à 0,0015 $ par enregistrement, sans analyse HTML ni maintenance de Scraper.
- LanceDB fonctionne localement et combine la recherche vectorielle avec des filtres SQL (salaire, ancienneté) en 1 requête, plus la recherche plein texte et le reranking Cohere.
- Sur 10 requêtes de test, la recherche vectorielle a obtenu 70 % de précision@3 contre 43 % pour les mots-clés. Le mode hybride + rerank n’a apporté aucune amélioration mesurable à cette échelle, donc en dessous de ~10 000 lignes, le vecteur seul est une valeur par défaut raisonnable.
- Le projet complet comprend 9 petits fichiers, dont un harnais d’évaluation, et le [code complet est sur GitHub](https://github.com/triposat/semantic-job-search). L’exécution complète coûte ~0,34 $.

## Le problème avec la recherche par mots-clés

La recherche par mots-clés sur un site d’emploi fait exactement ce que vous demandez. Elle retourne les annonces dont le titre ou la description contient les tokens littéraux de votre requête. Demandez *« ingénieur qui travaille sur les LLMs et le prompt engineering »* et vous raterez des postes comme *« Développeur GenAI »* même quand ils correspondent parfaitement. La recherche lexicale correspond aux mots exacts, pas au sens.

La [recherche vectorielle](/blog/ai/vector-databases) correspond au sens. Chaque description de poste est convertie en embedding (un vecteur haute dimension qui capture son contenu sémantique), ainsi que votre requête. Un poste dont le vecteur est proche de celui de votre requête est une bonne correspondance en termes de sens, même quand il ne partage aucun des mêmes mots.

Transformer cela en moteur de recherche fonctionnel nécessite 3 éléments :

1. **Bright Data** scrape 200 vraies offres d’emploi LinkedIn en JSON structuré propre.
2. **Cohere** transforme les descriptions en embeddings et reclasse les résultats finaux.
3. **LanceDB** stocke les embeddings localement et sert des requêtes hybrides (vecteur + plein texte) avec des filtres de style SQL.

## La stack en un coup d’œil

Ce que fait chaque couche, et pourquoi nous l’utilisons :

CoucheOutilPourquoi celui-ciDonnées web**Bright Data** API Web ScraperLe Scraper LinkedIn pré-construit retourne du JSON structuré avec salaire, ancienneté et localisation, sans analyse HTML ni maintenance de Scraper.Embeddings**Cohere** `embed-english-v3.0`Encodage asymétrique (types d’entrée différents pour les documents vs les requêtes). Cohere propose aussi `embed-v4.0`, qui est multimodal. Nous utilisons v3 ici pour son profil prix/latence en anglais uniquement (prévoir de ré-embedder avant la fin de vie de v3).Reranker**Cohere** `rerank-v3.5`Nous épinglons v3.5 pour son profil prix/latence. Cohere propose aussi `rerank-v4.0` (`-pro` pour la qualité, `-fast` pour la latence).Vector store**LanceDB**Local, embarqué, sans serveurs. Il supporte la recherche hybride (vecteur + BM25) et les préfiltres SQL.UI (optionnel)**Streamlit**Interface web à code minimal pour une application de données Python.Cette stack fonctionne depuis un seul venv Python sur votre ordinateur portable. Bright Data et Cohere sont les seuls services gérés impliqués.

## Configuration

Le projet complet et exécutable est sur [GitHub](https://github.com/triposat/semantic-job-search). Clonez-le et installez les dépendances (Python 3.10 ou plus récent) :

```none
git clone https://github.com/triposat/semantic-job-search.git
cd semantic-job-search
python -m venv .venv && source .venv/bin/activate
pip install -r requirements.txt
```

Copiez le fichier env exemple et ajoutez vos deux clés API, un [token Bright Data](/cp/setting/users) et une clé Cohere depuis dashboard.cohere.com (une clé d’essai fonctionne pour tout le guide) :

```none
cp .env.example .env
# then edit .env with your keys:
#   BRIGHTDATA_API_TOKEN=...
#   COHERE_API_KEY=...
```

Les deux clés en place, exécutez `python scrape.py` pour récupérer les données et `python index.py` pour construire l’index.

## Architecture

Le système comprend deux flux, pas un. **L’ingestion** construit l’index (exécuté une fois, ou selon un calendrier). La **requête** s’exécute à chaque recherche. Les deux utilisent Cohere et LanceDB, mais pour des travaux différents.

![Diagramme d'architecture à deux flux. INGESTION (exécuté une fois, ou selon un calendrier) : une flèche 'mot-clé' entre dans Bright Data ('Découvrir des emplois par mot-clé (async)'), qui envoie 'emplois (JSON)' à Cohere ('embed (document)'), qui envoie 'vecteurs' à LanceDB ('index vecteur + FTS + scalaire, versionné'). REQUÊTE (par recherche, mode hybride par défaut) : une flèche 'requête' entre dans Cohere ('embed (query)'), qui envoie un 'vecteur de requête' à LanceDB ('recherche vecteur + FTS + préfiltre SQL'), qui envoie des 'candidats' à Cohere ('rerank'), qui retourne des 'résultats classés'. Cohere et LanceDB sont teintés pour montrer qu'ils sont réutilisés dans les deux flux, et le rerank ne s'exécute qu'en mode hybride.](https://media.brightdata.com/2026/08/Bk1rV5yffl.png)*Les deux flux côte à côte. L’ingestion embed les documents et les stocke. La requête embed le texte de recherche, exécute une recherche vecteur + plein texte avec un préfiltre SQL, puis reclasse. Cohere et LanceDB apparaissent dans les deux flux mais font un travail différent dans chacun, c’est pourquoi le rerank ne touche jamais le chemin d’ingestion.*

3 scripts exécutent le pipeline : `scrape.py`, `index.py`, `search.py`. 6 autres helpers : `lib.py` (backend de recherche partagé), `compare.py` (comparaison de modes), `eval.py` (précision@3), `stats.py` (résumé du jeu de données), `versions.py` (navigateur de snapshots), et `app.py` (UI Streamlit).

## Scraper LinkedIn avec Bright Data

LinkedIn est une source majeure de données d’emploi, mais difficile à scraper de manière fiable : limites de débit, balisage dynamique et HTML qui change sans préavis. L’[API Web Scraper](/products/web-scraper) retourne du JSON structuré propre depuis des endpoints pré-construits, donc vous ne maintenez pas de parsers.

### Choisir le bon endpoint

Bright Data expose plusieurs Scrapers LinkedIn :

- **Profils de personnes** → profils de membres individuels
- **Informations d’entreprise** → pages d’entreprises
- **Offres d’emploi → Collecter par URL** → URLs d’emploi spécifiques que vous avez déjà
- **Offres d’emploi → [Découvrir par mot-clé](/products/web-scraper/linkedin/jobs)** ← c’est ce que nous voulons
- **Offres d’emploi → Découvrir par URL** → emplois depuis une URL de résultats de recherche
- **Publications LinkedIn** et **Recherche de personnes** → autres types d’entités

`Découvrir par mot-clé` est le bon choix car nous voulons une découverte d’emplois en masse depuis une requête de recherche. Un seul appel API retourne jusqu’à 1 000 offres d’emploi structurées par mot-clé, incluant titre, entreprise, localisation, niveau d’ancienneté, type d’emploi, fourchette salariale si indiquée, et la description complète du poste.

Chaque Scraper a son propre `dataset_id`. Pour en trouver un, ouvrez la [Bibliothèque de Scrapers](/cp/scrapers/browse) de Bright Data, recherchez le site (ici, `linkedin.com`), et ouvrez-le. Choisissez l’endpoint **Offres d’emploi → Découvrir par mot-clé**, et son `dataset_id` (`gd_lpfll7v5hcqtkxl6l`) ainsi qu’une requête prête à exécuter apparaissent dans le panneau Exemples de code. Un token valide est tout ce dont `scrape.py` a besoin pour l’appeler.

![Tableau de bord Bright Data montrant le menu de la Bibliothèque de Web Scrapers, avec l'endpoint LinkedIn offres d'emploi → 'Découvrir par mot-clé' sélectionné dans la barre latérale gauche. Le panneau central montre l'onglet Configuration avec des exemples d'entrées (paris/product manager, New York/python developer). Le panneau droit montre la vue Exemples de code avec une requête curl authentifiée contenant `dataset_id=gd_lpfll7v5hcqtkxl6l`.](https://media.brightdata.com/2026/08/B1JSEqJGze.png)*La page du Scraper ‘Découvrir par mot-clé’. Le panneau Exemples de code à droite est là où vous trouverez le `dataset_id`.*

### Synchrone vs asynchrone

Bright Data propose 2 modes de livraison :

- **Synchrone** (`POST /datasets/v3/scrape`) retourne les données en ligne, idéal pour les petits lots.
- **Asynchrone** (`POST /datasets/v3/trigger`) retourne un ID de snapshot. Vous interrogez jusqu’à la complétion et téléchargez le résultat, idéal pour tout ce qui est plus grand.

Dans nos exécutions, le temps de réponse était en moyenne de ~6 secondes **par entrée**. Pour 2 mots-clés avec `limit_per_input=100` (200 emplois au total), un appel synchrone doit maintenir la connexion ouverte pour tout le lot, ce qui risque d’expirer. L’asynchrone est la valeur par défaut sûre.

### Contrôler le coût avec des limites par entrée

Le paramètre de requête `limit_per_input=N` plafonne le nombre de résultats que chaque recherche d’entrée retourne, ce qui est exactement le levier que vous voulez pour une dépense prévisible :

```none
2 keywords × 100 jobs × $0.0015 = $0.30 per run
```

Augmentez-le pour des exécutions plus grandes, jusqu’à 1 000 emplois par mot-clé.

### Le code

Le Scraper [déclenche un snapshot](https://docs.brightdata.com/scraping-automation/web-data-apis/web-scraper-api/trigger-a-collection), interroge jusqu’à ce qu’il soit prêt, et télécharge le JSON. L’essentiel est ci-dessous (une version de production ajouterait des tentatives/backoff et une gestion d’erreurs plus riche) :

```none
# scrape.py
import json, time, sys
from pathlib import Path
import requests
from lib import require_env

BD_TOKEN = require_env("BRIGHTDATA_API_TOKEN")
DATASET_ID = "gd_lpfll7v5hcqtkxl6l"  # LinkedIn jobs - discover by keyword
LIMIT_PER_INPUT = 100

SEARCHES = [
    {"location": "San Francisco", "keyword": "machine learning engineer",
     "country": "US", "time_range": "Past month", "job_type": "Full-time",
     "experience_level": "", "remote": "", "company": "", "location_radius": ""},
    {"location": "New York", "keyword": "python developer",
     "country": "US", "time_range": "Past month", "job_type": "Full-time",
     "experience_level": "", "remote": "", "company": "", "location_radius": ""},
]

API = "https://api.brightdata.com/datasets/v3"
HEADERS = {"Authorization": f"Bearer {BD_TOKEN}", "Content-Type": "application/json"}

def trigger_snapshot() -> str:
    r = requests.post(f"{API}/trigger", headers=HEADERS, json={"input": SEARCHES},
        params={"dataset_id": DATASET_ID, "type": "discover_new",
                "discover_by": "keyword", "include_errors": "true",
                "limit_per_input": str(LIMIT_PER_INPUT)})
    r.raise_for_status()
    return r.json()["snapshot_id"]

def wait_until_ready(snapshot_id: str) -> None:
    while True:
        status = requests.get(f"{API}/progress/{snapshot_id}", headers=HEADERS).json()["status"]
        if status == "ready": return
        if status == "failed": raise RuntimeError("snapshot failed")
        time.sleep(10)

def download(snapshot_id: str) -> list[dict]:
    return requests.get(f"{API}/snapshot/{snapshot_id}",
                        headers=HEADERS, params={"format": "json"}).json()
```

Exécution :

```none
$ python scrape.py
→ scraping 2 keyword searches, max 100 jobs each
  estimated max cost: $0.30 (at $0.0015/record × 200 max records)
  triggered snapshot: sd_mojicp6g39xwbwqn2
  status: ready
✓ saved 204 jobs → data/raw_jobs.json
  actual cost: $0.31
```

### Ce que vous obtenez en retour

Chaque emploi dans le JSON possède plus de 25 champs. Voici ceux qui comptent :

```none
{
  "job_posting_id": "<id>",
  "job_title": "Associate Machine Learning Engineer",
  "company_name": "ExampleCo",
  "job_location": "San Francisco, CA",
  "job_seniority_level": "Entry level",
  "job_employment_type": "Full-time",
  "job_industries": "Software Development",
  "job_summary": "About ExampleCo. ExampleCo is the career network for the AI economy...",
  "base_salary": {
    "min_amount": 115000,
    "max_amount": 144000,
    "currency": "$",
    "payment_period": "yr"
  },
  "job_posted_date": "2026-04-25T03:41:21.072Z",
  "url": "https://www.linkedin.com/jobs/view/<id>"
}
```

Le champ structuré `base_salary` est ce qui rend possibles les requêtes de filtre par salaire à l’étape suivante.

## Indexer avec Cohere et LanceDB

Nous avons 204 enregistrements d’emplois bruts, dont 4 lignes d’erreur que nous filtrons au chargement. Maintenant nous rendons les 200 restants sémantiquement consultables.

### Pourquoi Cohere

Nous avons choisi Cohere plutôt que les alternatives (modèles d’embedding d’OpenAI, Voyage AI, ou sentence-transformers locaux) :

1. **Encodage asymétrique.** Cohere vous permet de taguer l’entrée comme `search_document` lors de l’indexation ou `search_query` lors de la recherche. Le modèle encode chaque côté différemment, ce qui fonctionne mieux que de traiter les deux de la même façon.
2. **Embedding déclaratif.** Le registre de LanceDB supporte Cohere nativement (comme OpenAI et sentence-transformers), donc l’embedding se produit à l’insertion et à la requête sans appels manuels à `embed()`.
3. **L’API Rerank.** C’est un modèle séparé qui prend une requête et une liste de candidats et réordonne les candidats par pertinence réelle. C’est la deuxième étape qui peut affiner le classement d’un pipeline hybride, et nous ajoutons cette étape avec un seul appel `.rerank()`.

### Le registre d’embedding LanceDB

Les embeddings dans LanceDB passent par son registre d’embedding. Vous déclarez votre schéma une fois, et les embeddings se produisent automatiquement à chaque insertion et chaque requête, chacun avec le bon `input_type`.

```none
# index.py
import lancedb
from lancedb.embeddings import get_registry
from lancedb.pydantic import LanceModel, Vector

cohere = get_registry().get("cohere").create(
    name="embed-english-v3.0",
    api_key=COHERE_API_KEY,
)

class Job(LanceModel):
    text: str = cohere.SourceField()              # ← quoi embedder
    vector: Vector(cohere.ndims()) = cohere.VectorField()  # ← embedding stocké
    job_id: str
    title: str
    company: str
    location: str
    country_code: str
    seniority: str
    employment_type: str
    job_function: str
    industry: str
    posted_date: str
    apply_url: str
    search_keyword: str
    salary_min_annual: float
    salary_max_annual: float
    salary_currency: str
    salary_display: str
    description_snippet: str
```

Tout ce qui suit `vector` est une colonne stockée ordinaire, utilisée pour le filtrage et l’affichage.

### L’astuce de normalisation des salaires

La plupart des emplois ont des salaires exprimés par an, mais quelques-uns sont par heure. Pour que `salary_min_annual >= 200000` fonctionne de manière cohérente, nous normalisons à l’ingestion :

```none
HOURS_PER_YEAR = 2080

def _normalize_salary(base):
    if not base:
        return 0.0, 0.0, "", ""
    lo = float(base.get("min_amount") or 0)
    hi = float(base.get("max_amount") or 0)
    if (base.get("payment_period") or "").lower() == "hr":
        lo *= HOURS_PER_YEAR
        hi *= HOURS_PER_YEAR
    currency = base.get("currency") or ""
    display = f"{currency}{int(lo):,}–{currency}{int(hi):,}/yr" if (lo and hi) else ""
    return lo, hi, currency, display
```

Nous stockons à la fois les valeurs numériques brutes (pour les filtres) et une chaîne d’affichage lisible par l’homme (pour l’UI).

### Mises à jour incrémentielles avec des upserts

La première fois que `index.py` s’exécute, il crée la table. Chaque exécution suivante est un **upsert** basé sur `job_id` :

```none
result = (
    table.merge_insert("job_id")
         .when_matched_update_all()       # actualiser les offres d'emploi existantes
         .when_not_matched_insert_all()   # ajouter les nouvelles découvertes
         .execute(rows)
)
print(f"inserted={result.num_inserted_rows}, updated={result.num_updated_rows}")
```

Les nouvelles offres d’emploi d’un nouveau scrape Bright Data sont insérées, et les offres re-publiées (même `job_id`) ont leurs salaires, descriptions et horodatages actualisés. Pour supprimer entièrement les offres obsolètes, chaînez `.when_not_matched_by_source_delete()`.

L’upsert entier est une transaction atomique unique. Parce que Lance stocke les données de manière colonnaire avec copie sur écriture, la ré-ingestion est une écriture incrémentielle plutôt qu’une reconstruction complète de la table.

### Index scalaires pour des filtres SQL rapides

Quand `search.py, where "salary_min_annual >= 200000"` s’exécute, LanceDB applique le filtre *avant* le scan vectoriel (`prefilter=True`). À 200 lignes c’est instantané dans tous les cas. À 200 000 lignes, le filtre parcourrait toute la colonne à moins que nous ne disions à LanceDB comment l’indexer :

```none
table.create_scalar_index("salary_min_annual", index_type="BTREE",  replace=True)
table.create_scalar_index("seniority",         index_type="BITMAP", replace=True)
table.create_scalar_index("search_keyword",    index_type="BITMAP", replace=True)
table.create_scalar_index("employment_type",   index_type="BITMAP", replace=True)
```

2 types d’index couvrent ce dont nous avons besoin :

- **BTREE** pour les colonnes triables à cardinalité élevée. `salary_min_annual` en bénéficie car nous voulons des requêtes de plage (`>=`, `BETWEEN`).
- **BITMAP** pour les enums à faible cardinalité. `seniority` a ~6 valeurs distinctes, `employment_type` est presque entièrement `Full-time`, et `search_keyword` est l’un de nos 2 mots-clés de scrape. Chaque valeur distincte obtient son propre bitmap. Un filtre `=` devient un simple AND bit à bit.

Les deux s’exécutent avec `replace=True`, donc ré-exécuter `index.py` les reconstruit de manière idempotente. Après l’appel, `table.list_indices()` rapporte les 5 (les 4 scalaires + l’index FTS) :

```none
text_idx               type=FTS      columns=['text']
salary_min_annual_idx  type=BTree    columns=['salary_min_annual']
seniority_idx          type=Bitmap   columns=['seniority']
search_keyword_idx     type=Bitmap   columns=['search_keyword']
employment_type_idx    type=Bitmap   columns=['employment_type']
```

### Inspecter les données indexées

Après avoir exécuté `python index.py`, notre script compagnon `stats.py` résume ce qui se trouve dans la base de données :

```none
$ python stats.py

📊 LanceDB · table 'jobs'  ·  200 rows

by source keyword
  machine learning engineer  ████████████████████ 100
  python developer           ████████████████████ 100

by seniority
  Mid-Senior level  ████████████████████ 99
  Entry level       ████████████ 62
  Not Applicable    ████ 20
  Internship        ██ 14
  Associate          4
  Director           1

salary coverage: 43/200 jobs (22%)
  min  $   65,000
  med  $  150,000
  max  $1,000,000

  highest-paying jobs:
    • Quantitative Developer (Python)                  Fintal Partners       $400,000–$1,000,000/yr
    • Machine Learning Engineer                        Mercor                $130,000–$500,000/yr
    • Data Scientist                                   Triumph               $200,000–$400,000/yr
    • Senior Python Developer (Middle Office Tech)     Quantitative Systems  $200,000–$400,000/yr
    • ML Engineer (Infra & Distributed training)       techire ai            $250,000–$400,000/yr

top hiring companies (top 10)
  Turing          ████████████████████ 7
  Handshake       █████████████████ 6
  OpenAI          █████████████████ 6
  Meta            █████████████████ 6
  Jack & Jill     ██████████████ 5
  DataAnnotation  ██████████████ 5
  Catalyst Labs   ███████████ 4
  Notion          ███████████ 4
  LangChain       ███████████ 4
  Uber            ████████ 3
```

## Exécuter la recherche hybride avec reranking

LanceDB supporte 3 modes de recherche, et notre `lib.py` expose les 3 derrière une seule fonction :

```none
# lib.py
from lancedb.rerankers import CohereReranker

reranker = CohereReranker(model_name="rerank-v3.5")  # épinglé ; le modèle plus récent de Cohere est rerank-v4.0

def search(query: str, mode: str = "hybrid", limit: int = 10, where: str | None = None):
    table = _table()
    if mode == "vector":
        q = table.search(query, query_type="vector")
    elif mode == "keyword":
        q = table.search(query, query_type="fts")
    elif mode == "hybrid":
        q = table.search(query, query_type="hybrid").rerank(reranker=reranker)
    if where:
        q = q.where(where, prefilter=True)
    return q.limit(limit).to_pandas()
```

Trois éléments de `search()` méritent une explication :

- **`query_type="hybrid"`** combine la similarité vectorielle et les scores BM25 de l’index plein texte que nous avons construit au moment de l’indexation (FTS natif de LanceDB). L’union des candidats est ensuite reclassée.
- **`.rerank(reranker)`** envoie la liste de candidats à l’API Rerank de Cohere et retourne son classement. Nous passons explicitement `model_name="rerank-v3.5"` car le défaut LanceDB est plus ancien.
- **`prefilter=True`** applique la clause SQL `WHERE` *avant* le scan vectoriel, pas après. C’est plus rapide (espace de recherche plus petit) et plus précis (vous ne perdez pas de résultats par troncature).

### Une vraie requête

Voici les 2 meilleurs résultats pour une requête qui ne partage pas beaucoup de mots littéraux avec un titre de poste dans le jeu de données :

```none
$ python search.py "deep learning model training with GPUs"

  ▸ Training: ML Framework Engineer  ·  score 0.275
    OpenAI — San Francisco, CA
    Entry level · Full-time · 2026-04-22
    "About The Team Training Runtime designs the core distributed
     machine-learning training runtime that powers everything from early
     research experiments to frontier-scale model runs..."

  ▸ Machine Learning Engineer  ·  score 0.138
    Skild AI — San Mateo, CA
    Entry level · Full-time · 2026-04-15
    "Company Overview At Skild AI, we are building the world's first
     general purpose robotic intelligence that is robust and adapts to
     unseen scenarios without failing. We believe massive scale through
     data-driven machine learning..."
```

Aucun titre de poste ne contient « GPUs », mais les deux descriptions parlent d’entraînement ML distribué, ce qui est ce que la requête demande. La recherche par mots-clés purs raterait probablement les deux.

> Chaque mode retourne un type de score différent. Le mode vectoriel retourne la distance cosinus (plus bas = plus proche), hybride+rerank retourne le score de pertinence de Cohere (0 à 1, plus haut = meilleur), et le mode par mots-clés retourne le BM25 brut (non borné, plus haut = plus de chevauchement de mots-clés). Les chiffres ne sont pas comparables entre les modes, seulement au sein d’un seul mode.

### Combiner la sémantique avec des contraintes strictes

La similarité sémantique et les filtres SQL se combinent en une seule requête dans LanceDB :

```none
$ python search.py "fintech python role with equity" \
    --where "salary_min_annual >= 250000"

  ▸ Quantitative Developer (Python)  ·  score 0.374
    Fintal Partners — New York, United States
    Mid-Senior level · Full-time · $400,000–$1,000,000/yr · 2026-04-22

  ▸ Senior Software Engineer (Python)  ·  score 0.272
    Fintal Partners — New York, NY
    Mid-Senior level · Full-time · $250,000–$400,000/yr · 2026-04-23
```

La partie vectorielle correspond à la partie descriptive (« fintech python avec equity »). Le filtre SQL applique la contrainte numérique (`>= 250 000 $`). Les deux résultats sont des postes Fintal Partners dans la bonne fourchette salariale.

Le même pattern hybride + filtre s’exécute dans l’UI Streamlit, sur un scrape ultérieur (les annonces en direct diffèrent de l’exécution CLI ci-dessus) :

![Interface de recherche Streamlit avec la requête 'fintech python role with equity' et le curseur Salaire minimum dans la barre latérale réglé à 250 000 $. L'en-tête des résultats indique '3 résultats · mode : hybride · filtre : salary_min_annual >= 250000′. La carte du premier résultat montre Senior Software Engineer (Python) chez Fintal Partners à New York, NY, avec des badges Mid-Senior level, Full-time, et un badge de salaire vert indiquant $300 000,$500 000/an, score de pertinence 0,272, et un extrait sur une société de trading quantitatif. Un deuxième résultat, Data Scientist chez OpenArt AI à San Francisco, score 0,165, commence en dessous.”/></figure>
<p class=](https://media.brightdata.com/2026/08/SyJrN5JzMx.png)*L’application Streamlit exécutant une recherche hybride. Le curseur de salaire produit le préfiltre `salary_min_annual >= 250000` affiché dans la bannière de filtre vert sur noir.*## Où les mots-clés, le vecteur et le mode hybride divergent

`compare.py` exécute la même requête à travers les 3 modes et imprime un rapport côte à côte :

```none
$ python compare.py "engineer working on LLMs and prompt engineering" --top 3

══════════════════════════════════════════════════════════════════════════
  query: engineer working on LLMs and prompt engineering
══════════════════════════════════════════════════════════════════════════

  ── keyword (BM25) ───────────────────────────────────────────────────────
  1. AI/ML Engineer                                          — Careerswift
  2. AI/ML Engineer                                          — Careerswift
  3. Applied AI Engineer                                     — Serval

  ── vector (Cohere) ──────────────────────────────────────────────────────
  1. Senior Software Engineer (Prompt Engineer Python/GenAI)        — Genpact
  2. 15+ Years exp/ Need f2f/ AI/ML Engineer or Python AI Engi...   — Jobs via Dice
  3. ML Engineer (Infra & Distributed training)                     — techire ai

  ── hybrid + rerank ──────────────────────────────────────────────────────
  1. Applied AI Engineer                                     — Serval
  2. Senior Software Engineer (Prompt Engineer Python/GenAI) — Genpact
  3. AI/ML Engineer                                          — Careerswift

  overlap: keyword∩vector=0/3 · hybrid∩vector=1/3 · hybrid∩keyword=2/3
```

Dans la ligne de chevauchement, **les mots-clés et le vecteur ont trouvé 0 des mêmes emplois dans le top 3.** Ils cherchent dans des espaces conceptuels différents.

- Les mots-clés (BM25) trouvent les annonces où les tokens littéraux « LLMs » et « prompt » apparaissent le plus fréquemment. Il retourne des titres IA/ML génériques.
- Le vecteur (Cohere) trouve l’annonce *Senior Software Engineer (Prompt Engineer Python/GenAI)* en #1, même si la requête utilisateur disait « prompt engineering » (gérondif) et le titre dit « Prompt Engineer » (nom). Il retourne aussi une annonce axée LLM de Jobs via Dice qui est une forte correspondance sémantique mais lexicalement éloignée de la requête.
- Hybride + rerank prend l’union, déduplique, et l’exécute à travers Cohere Rerank. Le poste *Applied AI Engineer* de Serval (200 k$ à 325 k$) passe en #1. Sa description est dense en travail de prompt engineering et d’agents LLM, mais ni son titre ni ses termes BM25 les mieux pondérés n’auraient classé le poste aussi haut.

Pour cette requête spécifique, le vecteur et le mode hybride ont tous deux fait mieux que les mots-clés. Le chevauchement brut de tokens a classé les résultats Genpact et Serval en dessous de ce que leur pertinence sémantique leur attribuait. Mais une seule requête est une anecdote, pas une preuve. Si ce pattern se maintient en général est une question à laquelle seule une vraie évaluation peut répondre.

## Mesurer la qualité avec précision@3

Pour mesurer cela correctement, `eval.py` évalue **10 requêtes écrites à la main** contre les 3 modes et calcule la **précision@3**, la fraction des 3 meilleurs résultats qui correspondent à un prédicat de vérité terrain transparent.

La vérité terrain pour chaque requête est un prédicat Python, pas un nombre magique, donc un lecteur peut décider s’il noterait les résultats de la même façon.

Pour *« machine learning engineer chez OpenAI »*, un résultat compte comme pertinent uniquement si son champ `company` contient « OpenAI ». Pour *« développeur quantitatif dans une société de trading »*, la règle est plus large. Un résultat compte si le titre contient « Quant » ou « Trading », ou si l’entreprise est une société de trading connue (Fintal Partners, DRW, Hudson River Trading, Tower Research, Mondrian Alpha). Ces prédicats sont calibrés sur le jeu de données exemple, donc vos scores évolueront sur de nouveaux emplois. Recalibrez-les sur vos propres données. L’écart entre les modes persiste même quand les pourcentages exacts ne le font pas.

Exécution :

```none
$ python eval.py

precision@3 per query (hits/3)
────────────────────────────────────────────────────────────────────────
  query                                          keyword    vector     hybrid
────────────────────────────────────────────────────────────────────────
  machine learning engineer at OpenAI            1.00 (3/3)  1.00 (3/3)  1.00 (3/3)
  founding engineer at AI startup with equity    0.33 (1/3)  0.67 (2/3)  0.67 (2/3)
  prompt engineer working with LLMs              0.00 (0/3)  0.67 (2/3)  0.33 (1/3)
  quantitative developer at trading firm         0.67 (2/3)  1.00 (3/3)  1.00 (3/3)
  computer vision and robotics engineer          1.00 (3/3)  0.67 (2/3)  1.00 (3/3)
  data scientist role                            0.67 (2/3)  1.00 (3/3)  1.00 (3/3)
  distributed training infrastructure for ML     0.33 (1/3)  0.67 (2/3)  0.67 (2/3)
  backend engineer at AI company                 0.33 (1/3)  0.33 (1/3)  0.33 (1/3)
  python developer at fintech                    0.00 (0/3)  0.67 (2/3)  0.33 (1/3)
  high-paying machine learning role with equity  0.00 (0/3)  0.33 (1/3)  0.33 (1/3)
────────────────────────────────────────────────────────────────────────
  AVERAGE (10 queries)                           0.433       0.700       0.667
```

Les mêmes chiffres sous forme de graphique :

![Graphique à barres de précision@3 sur 10 requêtes de test : mots-clés 43 %, vecteur 70 %, hybride + rerank 67 %.](https://media.brightdata.com/2026/08/SkR4N9yMzg.png)*Précision@3 moyennée sur les 10 requêtes d’évaluation. Le vecteur score bien au-dessus des mots-clés, et le mode hybride est à quelques points du vecteur.*

### Ce que disent les chiffres

D’après le tableau :

- **La recherche vectorielle a scoré bien au-dessus de la recherche par mots-clés** avec **70 % contre 43 % de précision@3 moyenne.** Les 3 requêtes où les mots-clés ont scoré 0 (« prompt engineer », « développeur python en fintech », « ML bien rémunéré avec equity ») avaient au moins 1 résultat pertinent avec le vecteur.
- **Hybride + rerank n’a pas battu le vecteur à cette échelle.** L’écart de 67 % contre 70 % est dans le bruit : le reranker ajoute un appel Cohere par requête, et la partie FTS lui fournit des quasi-correspondances lexicales qu’il doit ensuite filtrer.
- **Aucun mode n’est strictement dominé.** « Vision par ordinateur et ingénierie robotique » est la seule requête où les mots-clés (1,00) score au-dessus du vecteur (0,67), car les entreprises pertinentes contiennent toutes des termes robotiques littéraux dans leurs descriptions.

### Quand activer hybride + rerank

Cela dépend de plusieurs facteurs :

- **Taille du pool de candidats.** Avec quelques centaines de lignes, le vecteur seul est généralement suffisant. La récupération en 2 étapes du mode hybride nécessite un pool plus grand (10 000+) avant que l’étape de rerank vaille son coût.
- **Type de requête.** Les requêtes avec à la fois une intention sémantique *et* des mots-clés distinctifs (un nom de marque, une technologie spécifique) bénéficient du mode hybride. Les requêtes purement sémantiques généralement non.
- **Qualité du reranker.** Le rerank-v3.5 de Cohere a bien performé dans notre évaluation. Si vous remplacez par un reranker différent, ré-exécutez `eval.py` avant de lui faire confiance, car un reranker plus faible peut réordonner les bons résultats vectoriels vers le bas sur un petit pool de candidats.

Exécutez `eval.py` sur vos propres données pour décider. Ajouter une requête est une chaîne plus un prédicat de vérité terrain.

**Note :** l’évaluation hybride s’exécute bien avec une clé Cohere gratuite. La limite de débit d’essai la fait reculer et se terminer en ~90s au lieu de ~15s.

## Ajouter une interface web avec Streamlit

Streamlit transforme le même backend de recherche en une application web cliquable. Le cœur de recherche et rendu est ci-dessous :

```none
# app.py
import streamlit as st
from lib import search

mode = st.sidebar.radio("Mode", ["hybrid", "vector", "keyword"])
seniority = st.sidebar.selectbox("Seniority", ["any", "Entry level", "Associate", "Mid-Senior level", "Director", "Internship", "Not Applicable"])
min_salary = st.sidebar.slider("Min salary ($/yr)", 0, 500_000, 0, step=10_000)

query = st.text_input("Search jobs", placeholder="e.g. remote ML engineer...")

if query:
    where_clauses = []
    if seniority != "any":
        where_clauses.append(f"seniority = '{seniority}'")
    if min_salary > 0:
        where_clauses.append(f"salary_min_annual >= {min_salary}")
    where = " AND ".join(where_clauses) or None

    df = search(query, mode=mode, where=where, limit=10)
    for _, row in df.iterrows():
        with st.container(border=True):
            st.markdown(f"### [{row['title']}]({row['apply_url']})")
            st.markdown(f"**{row['company']}** — {row['location']}")
            st.caption(row["description_snippet"] + "…")
```

Exécutez-le :

```none
streamlit run app.py
```

Vous obtenez une page de recherche complète sur `localhost:8501` avec une boîte de recherche, un toggle de mode, des filtres de barre latérale pour l’ancienneté, le mot-clé source et le salaire, plus des cartes de résultats avec des badges, des scores et des aperçus d’extraits.

![Interface de recherche Streamlit pour la requête 'founding ML engineer at AI startup with computer vision' montrant une liste de résultats hybrides. La barre latérale contient des filtres pour le mode de recherche, l'ancienneté, le mot-clé de recherche source, le salaire minimum et le nombre de résultats. La carte du premier résultat est 'Founding ML Engineer | Frontier Medical AI | $150k,$200k | SF' de CoffeeSpace dans la région de la baie de San Francisco, avec des badges Mid-Senior level + Full-time, un score de pertinence Cohere de 0,720, et un extrait de description. Un deuxième résultat, 'AI/ML Engineer - AI Design Software Leader' avec score 0,711, commence en dessous.](https://media.brightdata.com/2026/08/Sk1S4c1Gzg.png)*L’application Streamlit exécutant une recherche hybride. Le badge de score sur chaque carte est le score de pertinence de Cohere, et l’extrait sous les badges montre pourquoi chaque résultat a atteint le top 3.*

## Le voyage dans le temps gratuit avec LanceDB

Cela couvre la recherche et l’UI. LanceDB a une autre fonctionnalité qui vaut la peine d’être montrée. Chaque écriture dans LanceDB crée automatiquement une nouvelle version, sans coût ni infrastructure supplémentaire. C’est ainsi que fonctionne le format colonnaire Lance sous-jacent. Pour qu’une version soit facile à trouver plus tard, `index.py` la tague après chaque ingestion :

```none
table.tags.create(f"ingest-{datetime.now():%Y-%m-%d-%H%M}", table.version)
```

Notre script compagnon `versions.py` vous permet ensuite de parcourir et d’ouvrir des snapshots historiques. Après avoir exécuté `python index.py` une fois, vous verrez 1 tag. Après une deuxième ingestion (par exemple, re-scraper une semaine plus tard) vous en verrez 2 :

```none
$ python versions.py

📊 table 'jobs'  ·  current version: 13  ·  200 rows

🏷  tags (2):
  • ingest-2026-05-20-0905           → version 7
  • ingest-2026-05-20-0906           → version 13  ← current

  travel back with: `python versions.py --tag <name>`

$ python versions.py --tag ingest-2026-05-20-0905

📌 snapshot 'ingest-2026-05-20-0905'  ·  version 7  ·  200 rows
  • Associate Machine Learning Engineer  — Handshake
  • Machine Learning Engineer            — RZR
  • Machine Learning Engineer            — ChatGPT Jobs
```

Le voyage dans le temps est un appel `table.checkout(tag_or_version)`. Pour un produit de recherche d’emploi, il répond à des questions comme *« quels postes étaient publiés le trimestre dernier ? »* ou *« la distribution des salaires évolue-t-elle dans le temps ? »* sans base de données de séries temporelles séparée. C’est l’une des raisons pour lesquelles nous avons choisi LanceDB ici.

## Coût et échelle

Pour la démo (200 emplois, ~5 exemples de requêtes) :

ÉlémentCoûtScrape Bright Data (204 enregistrements à 0,0015 $/enregistrement)**0,31 $**Embeddings Cohere (~228 000 tokens au total à 0,10 $/1M)~0,02 $Rerank Cohere (~0,002 $/requête, Rerank v3.5 à 2 $ / 1 000 recherches)~0,01 $ pour 5 requêtesLanceDB**gratuit**La démo de bout en bout coûte **~0,34 $** au total. Ces prix sont issus d’une exécution en 2026, vérifiez donc les tarifs actuels des fournisseurs.

### Monter en échelle

La démo locale gère 200 emplois. Quelques leviers couvrent le chemin d’ici à un jeu de données à l’échelle de production :

- **Plus d’emplois.** Changez `LIMIT_PER_INPUT` (max 1 000 par mot-clé) ou ajoutez plus de recherches par mots-clés. 10 000 emplois coûtent ~15 $ en crédits Bright Data.
- **Plus de mots-clés / localisations.** Ajoutez des entrées à la liste `SEARCHES` dans `scrape.py`.
- **Rafraîchissement planifié.** L’upsert `merge_insert` que nous avons construit signifie que ré-exécuter le pipeline actualise ce qui a changé. Bright Data supporte la collecte et la livraison planifiées depuis le tableau de bord. Associez cela à l’upsert et vous avez un jeu de données qui se met à jour automatiquement.
- **Index vectoriel.** Au-delà de ~10 000 lignes, remplacez la recherche par force brute par un index HNSW ou IVF\_PQ via `table.create_index(vector_column_name="vector")`. Il se construit sur CPU par défaut. Pour une construction GPU, passez `accelerator="cuda"` (ou `"mps"` sur Apple Silicon) avec PyTorch&gt;2.0. L’indexation GPU automatique est actuellement une fonctionnalité LanceDB Enterprise.
- **Vector store de production.** LanceDB OSS scale à des millions de vecteurs sur un seul nœud. Au-delà de centaines de millions de vecteurs ou de téraoctets de données, LanceDB Cloud et Enterprise ajoutent l’indexation distribuée et l’exécution de requêtes (leurs docs ciblent ~10 à 50B lignes / ~10 à 30 To).

Avant ces mesures de mise à l’échelle, cependant, la démo elle-même a des aspects à affiner.

## 8 bugs et pièges que nous avons rencontrés

Au cas où cela vous économise les heures qu’ils nous ont coûtées :

1. **`list_tables()` ne retourne pas une liste.** Dans LanceDB 0.30, elle retourne un objet `ListTablesResponse` qui *semble* itérable dans le REPL mais `if TABLE in db.list_tables()` échoue silencieusement. Utilisez `try: db.open_table(TABLE)` et attrapez l’exception à la place, ou `.tables` sur la réponse.
2. **`table.checkout(tag)` retourne `None`** et mute le handle de table en place. Cela ressemble à un bug, mais non. Faites `t = db.open_table(...); t.checkout(tag); use(t)`, pas `t = db.open_table(...).checkout(tag)`.
3. **Le `CohereReranker()` par défaut utilise un ancien modèle** (`rerank-english-v3.0` dans les versions testées). Passez un modèle explicitement, soit `rerank-v3.5` (ce que nous épinglons ici) ou `rerank-v4.0-pro` pour une meilleure qualité. Le défaut ne vous avertit pas.
4. **Utilisez `/trigger` + polling, pas `/scrape`, pour les vrais lots.** Le mode synchrone (`/scrape`) est conçu pour les petites extractions. Maintenir la connexion ouverte pour `limit_per_input=100` × 2 mots-clés (~200 emplois) peut expirer, donc utilisez `/trigger` + polling pour tout ce qui dépasse ~50 enregistrements.
5. **Quelques enregistrements scrapés sont des lignes d’erreur.** Sur 204 emplois, 4 avaient un champ `error` défini au lieu d’un `job_title` (par exemple, `"Crawl aborted on job cancel"`). Ils ressemblent superficiellement à des enregistrements normaux, donc filtrez-les dans `index.py` ou `merge_insert` échouera sur un `job_id` vide.
6. **Les salaires viennent en 2 périodes (`yr` et `hr`)** mais le champ du schéma est le même. Sans normaliser en annuel (multiplier horaire par 2080), un filtre comme `salary_min_annual >= 200000` rate silencieusement les contrats horaires bien rémunérés et inclut des postes salariés implausiblement bas.
7. **Les chaînes d’aide `argparse` avec des `%` bruts se cassent sur Python 3.14.** Écrire `--where "salary > 200000 AND location LIKE '%SF%'"` dans votre texte d’aide lève `ValueError: badly formed help string` car argparse essaie de le formater. Échappez en `%%` ou reformulez l’exemple.
8. **Streamlit rend le texte entre les signes `$` comme des maths LaTeX.** Un salaire comme `$150k,$200k` affiché avec `st.markdown` ou `st.caption` devient des maths illisibles. Échappez chaque `$` dans vos chaînes d’affichage (le `app.py` du dépôt le fait avec un `replace` d’une ligne), ou les badges de salaire s’affichent en charabia.

## Ce que vous pouvez construire ensuite

Le pattern, *Bright Data ⟶ embeddings ⟶ vector DB ⟶ recherche hybride*, se généralise à presque n’importe quel domaine :

DomaineProduit Bright DataCe que vous interrogeriez**Accès web agentique**[Le MCP Bright Data](/ai/mcp-server) (niveau gratuit actuellement 5 000 requêtes/mois)*« donner à un agent IA des outils de recherche + scrape en direct, puis ancrer ses réponses dans un cache LanceDB de résultats passés »***Corpus de sites entiers**[API de crawl](/products/crawl-api)*« indexer un site de documentation entier ou une base de connaissances pour la récupération hybride »***E-commerce**API Web Scraper (produits Amazon)*« chaussures de course confortables à moins de 100 $ avec 4+ étoiles »***Immobilier**API Web Scraper (Zillow / Redfin)*« maison familiale calme près de bonnes écoles, 3+ chambres »***Intelligence d’actualités**[API SERP](/products/serp-api) + [Web Unlocker](/products/web-unlocker)*« articles sur la sécurité IA de cette semaine, classés par pertinence pour l’alignement »***Prospection commerciale**Infos entreprises LinkedIn*« startups Série A en IA de santé basées en Europe »***Restaurants**Jeu de données Yelp*« restaurant italien cosy avec terrasse »*Quelques extensions naturelles de ce projet exact :

- **Recherche multimodale.** Passez à Cohere `embed-v4.0` (nativement multimodal) et embedez les logos d’entreprises aux côtés des descriptions de postes.
- **Filtres extraits par LLM.** Laissez l’utilisateur taper *« emplois ML à distance payant 200 000 $+ »* et demandez à un LLM d’extraire automatiquement `remote=true, salary_min_annual >= 200000`.
- **Recherches sauvegardées avec alertes email.** Ré-exécutez une requête contre le scrape le plus récent et notifiez sur les nouvelles correspondances.
- **Correspondance de CV.** Embedez un CV et recherchez des emplois par similarité avec le candidat. L’[assistant IA de recherche d’emploi LinkedIn](/blog/ai/linkedin-job-hunting-agent) de Bright Data est un exemple plus complet.
- **Un Scraper auto-maintenu.** Donnez à un agent accès au MCP de Bright Data et il peut inspecter la page, écrire le Scraper, et tenter une correction quand la mise en page change, plutôt que vous corrigiez `scrape.py` à la main. Le [Scraper Studio](/products/web-scraper/studio) de Bright Data emballe cela comme un produit géré, transformant une invite en langage naturel en un Scraper auto-réparant.

## Prochaines étapes

La recherche par mots-clés ratait les bons postes, et la recherche vectorielle les trouvait même quand les titres ne correspondaient jamais à la requête. Dans l’évaluation, le vecteur a scoré 70 % de précision@3 contre 43 % pour les mots-clés, avec le mode hybride n’apportant aucune amélioration à cette échelle.

Le [projet complet sur GitHub](https://github.com/triposat/semantic-job-search) comprend 9 petits fichiers. Pour l’utiliser sur vos propres données, exécutez d’abord `python eval.py`, car le meilleur mode dépend des données, pas de celui qui est le plus complexe. Décidez ensuite d’une cadence de rafraîchissement, où `merge_insert` n’upserte que ce qui a changé et `versions.py` prend un snapshot de chaque ingestion. Et avant tout déploiement, planifiez une routine de rotation des clés, car les clés BD et Cohere vont dans `.env`.

Le même pattern fonctionne pour tout ce que [Bright Data](/) peut scraper, pas seulement les emplois. À partir de là, vous disposez d’un moteur de recherche sémantique réutilisable pour n’importe quel jeu de données que vous scrapez.

## FAQ

### Puis-je l’utiliser pour d’autres sites que LinkedIn ?

Oui. La [Bibliothèque de Web Scrapers](https://docs.brightdata.com/datasets/scrapers/scrapers-library/overview) de Bright Data couvre des centaines de sites (Amazon, Zillow, Yelp, et plus), chacun avec son propre `dataset_id`. Remplacez le `DATASET_ID` dans `scrape.py` et le mapping `to_row()` dans `index.py` pour la nouvelle forme JSON. La logique de recherche et d’indexation est agnostique aux données et se transporte.

### Ai-je besoin d’un compte Cohere payant pour cela ?

Non, une clé d’essai exécute toute la démo. L’endpoint Rerank d’essai de Cohere est actuellement limité à 10 appels/min, donc `eval.py` rencontre un 429 et recule automatiquement (~90s au lieu de ~15s). Le Scraping web, l’indexation et la recherche ad hoc restent bien en dessous des limites. Mettez à niveau uniquement si vous itérez souvent sur l’évaluation.

### Pourquoi LanceDB, pas Pinecone, Weaviate ou pgvector ?

LanceDB est une bibliothèque embarquée sans serveur, sans base de données séparée, et sans facture de service géré. Il supporte la recherche hybride et le reranking Cohere nativement, et chaque écriture est un snapshot de version. Pour un pipeline sur une seule machine sans ops, c’est le moins de surcharge. Les autres sont capables mais ajoutent plus d’infrastructure.

### À quelle fréquence devrais-je ré-exécuter le Scraper ?

Une fois par jour convient à un site d’emploi actif. Bright Data peut exécuter la collecte planifiée depuis le tableau de bord, et l’upsert `merge_insert` déduplique côté LanceDB, donc les ré-exécutions sont peu coûteuses. Les annonces de plus de ~30 jours sont généralement fermées, donc les anciens snapshots deviennent historiques, et `versions.py` les garde consultables.



Contacter ventesEssai gratuit![google social icon](/wp-content/themes/brightdata/assets/images/ic_google.svg)









 Table des matières













 [ ](https://news.ycombinator.com/submitlink?t=Construire+un+moteur+de+recherche+d%26%238217%3Bemploi+s%C3%A9mantique+avec+Bright+Data%2C+LanceDB+et+Cohere&u=https://brightdata.fr/blog/ai/semantic-job-search-engine-with-bright-data-lancedb-and-cohere) [ ](https://www.linkedin.com/shareArticle?mini=true&title=Construire+un+moteur+de+recherche+d%26%238217%3Bemploi+s%C3%A9mantique+avec+Bright+Data%2C+LanceDB+et+Cohere&url=https://brightdata.fr/blog/ai/semantic-job-search-engine-with-bright-data-lancedb-and-cohere) [ ](http://www.reddit.com/submit?title=Construire+un+moteur+de+recherche+d%26%238217%3Bemploi+s%C3%A9mantique+avec+Bright+Data%2C+LanceDB+et+Cohere&url=https://brightdata.fr/blog/ai/semantic-job-search-engine-with-bright-data-lancedb-and-cohere)







##  Vous pourriez aussi être intéressé par

 [ ![OpenHuman with Bright Data](https://media.brightdata.fr/2026/09/OpenHuman-with-Bright-Data.png) ](https://brightdata.fr/blog/ai/openhuman-with-bright-data "Accès Web Prêt pour la Production dans OpenHuman via le CLI Bright Data")

 [AI



 ![Antonello Zanini](https://media.brightdata.fr/2022/12/Antonello-Zanini-2-50x50.jpg)

Antonello Zanini

Technical Writer





### Accès Web Prêt pour la Production dans OpenHuman via le CLI Bright Data

Intégrez le CLI Bright Data avec OpenHuman pour permettre un accès web prêt pour la production et la collecte de données pour les agents IA.



 09-Sep-2026

 14 min de lecture

 ](https://brightdata.fr/blog/ai/openhuman-with-bright-data)

 [ ![Multimodal Web Scraping with MiniMax](https://media.brightdata.fr/2026/09/Multimodal-Web-Scraping-with-MiniMax.png) ](https://brightdata.fr/blog/donnees-web/multimodal-web-scraping-with-minimax "Scraping Web Multimodal avec MiniMax")

 [Données Web



 ![Antonello Zanini](https://media.brightdata.fr/2022/12/Antonello-Zanini-2-50x50.jpg)

Antonello Zanini

Technical Writer





### Scraping Web Multimodal avec MiniMax

Associez Bright Data Web Unlocker avec la vision MiniMax M3 pour extraire des données structurées à partir d’images et de captures d’écran de pages web.



 09-Sep-2026

 5 min de lecture

 ](https://brightdata.fr/blog/donnees-web/multimodal-web-scraping-with-minimax)

 [ ![The 10 Best CLI Tools for Codex in 2026](https://media.brightdata.fr/2026/08/The-10-Best-CLI-Tools-for-Codex-in-2026.png) ](https://brightdata.fr/blog/ai/best-cli-tools-for-codex "Les 10 meilleurs outils CLI pour Codex en 2026 – Testés & Classés")

 [AI



 ![Daniel Shashko](https://media.brightdata.fr/2022/04/Daniel-Shashko-2-50x50.png)

Daniel Shashko

Web Data &amp; AI Expert





### Les 10 meilleurs outils CLI pour Codex en 2026 – Testés &amp; Classés

Les 10 outils CLI qui rendent Codex plus rapide et plus capable, en commençant par le Bright Data CLI pour un accès web réel depuis l’intérieur du sandbox.



 06-Sep-2026

 24 min de lecture

 ](https://brightdata.fr/blog/ai/best-cli-tools-for-codex)
