Mise en place d'un moteur de recherche full-text
Ce guide détaille la création d'une application de recherche de produits en utilisant Elasticsearch pour l'indexation et les requêtes full-text, couplé à une interface frontend réactive développée avec Vue.js. L'objectif est de permettre une recherche instantanée avec mise en évidence des termes correspondants.
1. Préparation du jeu de données
Voici la structure des données simulées qui seront indexées. Les champs ont été standardisés en anglais pour faciliter l'intégration technique.
| Name | Description | Price | Stock | Brand |
|---|---|---|---|---|
| Smart Watch | Smratwatch with health tracking and notifications. | 199.99 | 1000 | TechWatch |
| 4K Smart TV | 4K resolution smart TV offering excellent picture quality. | 699.99 | 500 | VisionTech |
| Wireless Earbuds | Noise-cancelling wireless earbuds for premium audio. | 149.99 | 800 | AudioMasters |
| Laptop | High-performence laptop with a fast processor. | 999.99 | 300 | TechLaptops |
| Digital Camera | High-resolution digital camera with multiple shooting modes. | 449.99 | 200 | PhotoPro |
| Portable Charger | Portable power bank for mobile devices. | 29.99 | 2000 | PowerBoost |
| Wireless Router | High-speed wireless router for large networks. | 79.99 | 400 | NetSpeed |
| Gaming Console | Gaming console supporting various games and entertainment. | 399.99 | 100 | GameZone |
| Phone Case | Durable phone case compatible with multiple models. | 19.99 | 1500 | PhoneGuard |
| Sneakers | High-performance sneakers for various sports. | 79.99 | 800 | SportsTech |
| 4K Monitor | 4K UHD monitor providing exceptional image quality. | 599.99 | 150 | UltraView |
| Smart Home Hub | Smart home device for centralized automation control. | 249.99 | 300 | SmartLiving |
2. Script d'ingestion de données en Python
Le script suivant utilise la bibliothèque officielle elasticsearch pour lire un fichier CSV et indexer les documents en masse via l'API helpers.bulk, ce qui est plus performant que des insertions unitaires.
import csv
from elasticsearch import Elasticsearch, helpers
# Initialisation du client Elasticsearch
client_es = Elasticsearch(hosts=["http://localhost:9200"])
# Vérification de la connexion au cluster
if not client_es.ping():
raise ConnectionError("Impossible d'établir une connexion avec Elasticsearch.")
csv_file_path = 'products_dataset.csv'
def generate_document_actions():
"""Générateur pour préparer les actions d'indexation."""
with open(csv_file_path, mode='r', encoding='utf-8') as file:
reader = csv.DictReader(file)
for row in reader:
yield {
"_index": "product_catalog",
"_id": row['sku_id'],
"_source": {
"name": row['name'],
"description": row['description'],
"price": float(row['price']),
"stock": int(row['stock']),
"brand": row['brand']
}
}
# Exécution de l'indexation en masse
try:
success_count, errors = helpers.bulk(
client_es,
generate_document_actions(),
refresh=True
)
print(f"Indexation terminée : {success_count} documents insérés avec succès.")
if errors:
print(f"Erreurs rencontrées : {errors}")
except Exception as e:
print(f"Échec de l'opération d'indexation : {e}")
3. Interface de recherche avec Vue.js
L'interface frontend est construite avec Vue 3 en utilisant l'API de composition. Elle interroge directement l'API REST d'Elasticsearch, gère la recherche floue et affiche les résultats avec la mise en évidence (highlight) des termes recherchés.
<html lang="fr">
<head>
<meta charset="UTF-8">
<title>Recherche de Produits</title>
<script src="https://unpkg.com/vue@3/dist/vue.global.js"></script>
<style>
body { font-family: Arial, sans-serif; background: #f4f7f6; padding: 20px; }
.search-container { max-width: 800px; margin: 0 auto; }
input[type="text"] { width: 100%; padding: 10px; font-size: 16px; margin-bottom: 20px; border: 1px solid #ccc; border-radius: 4px; box-sizing: border-box; }
.product-card { background: #fff; padding: 15px; margin-bottom: 15px; border-radius: 8px; box-shadow: 0 2px 4px rgba(0,0,0,0.1); }
.product-title { font-size: 1.2em; font-weight: bold; color: #333; }
.product-title em { color: #e74c3c; font-style: normal; font-weight: bold; }
.product-details { margin-top: 8px; color: #666; }
.price { color: #27ae60; font-weight: bold; }
</style>
</head>
<body>
<div id="app" class="search-container">
<h2>Catalogue de Produits</h2>
<input
type="text"
v-model="searchTerm"
@input="executeSearch"
placeholder="Rechercher un produit par nom..."
>
<div v-if="results.length > 0">
<div v-for="item in results" :key="item.id" class="product-card">
<div class="product-title" v-html="item.highlightedName"></div>
<div class="product-details">
<p><strong>Description:</strong> {{ item.description }}</p>
<p><strong>Marque:</strong> {{ item.brand }}</p>
<p><strong>Prix:</strong> <span class="price">{{ item.price }} €</span></p>
<p><strong>Stock disponible:</strong> {{ item.stock }} unités</p>
</div>
</div>
</div>
<p v-else-if="searchTerm && !isLoading">Aucun produit trouvé.</p>
</div>
<script>
const { createApp, ref } = Vue;
createApp({
setup() {
const searchTerm = ref('');
const results = ref([]);
const isLoading = ref(false);
const executeSearch = async () => {
if (!searchTerm.value.trim()) {
results.value = [];
return;
}
isLoading.value = true;
const esQuery = {
query: {
match: {
"name": {
"query": searchTerm.value,
"fuzziness": "AUTO"
}
}
},
highlight: {
fields: {
"name": {}
}
}
};
try {
const response = await fetch('http://localhost:9200/product_catalog/_search', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(esQuery)
});
const data = await response.json();
results.value = data.hits.hits.map(hit => ({
id: hit._id,
highlightedName: hit.highlight?.name?.[0] || hit._source.name,
description: hit._source.description,
brand: hit._source.brand,
price: hit._source.price,
stock: hit._source.stock
}));
} catch (error) {
console.error('Erreur lors de la requête Elasticsearch:', error);
} finally {
isLoading.value = false;
}
};
return {
searchTerm,
results,
isLoading,
executeSearch
};
}
}).mount('#app');
</script>
</body>
</html>