← back to Wine Finder
services/wineEnthusiastScraper.js
167 lines
const fs = require('fs');
const path = require('path');
const csv = require('csv-parser');
class WineEnthusiastScraper {
constructor() {
this.dataPath = path.join(__dirname, '../data/winemag-data-130k-v2.csv');
this.cache = null;
this.cacheTimestamp = null;
this.CACHE_TTL = 3600000; // 1 hour
}
/**
* Search Wine Enthusiast 130k dataset
* @param {string} query - Search query
* @param {object} options - Search options
*/
async search(query, options = {}) {
try {
const {
limit = 50,
minRating = 0,
maxPrice = 10000,
country = null,
variety = null
} = options;
// Load wines from CSV
const wines = await this.loadWines();
// Filter by query
const queryLower = query.toLowerCase();
let filtered = wines.filter(wine => {
const title = (wine.title || '').toLowerCase();
const description = (wine.description || '').toLowerCase();
const winery = (wine.winery || '').toLowerCase();
const variety = (wine.variety || '').toLowerCase();
return title.includes(queryLower) ||
description.includes(queryLower) ||
winery.includes(queryLower) ||
variety.includes(queryLower);
});
// Apply filters
filtered = filtered.filter(wine => {
const price = parseFloat(wine.price) || 0;
const points = parseInt(wine.points) || 0;
let matches = points >= minRating && (price === 0 || price <= maxPrice);
if (country && wine.country) {
matches = matches && wine.country.toLowerCase().includes(country.toLowerCase());
}
if (variety && wine.variety) {
matches = matches && wine.variety.toLowerCase().includes(variety.toLowerCase());
}
return matches;
});
// Sort by points (rating) descending
filtered.sort((a, b) => (parseInt(b.points) || 0) - (parseInt(a.points) || 0));
// Limit results
filtered = filtered.slice(0, limit);
// Format wines
const formattedWines = filtered.map(wine => ({
name: wine.title || 'Unknown',
vintage: this.extractVintage(wine.title),
price: wine.price ? `$${wine.price}` : null,
rating: parseInt(wine.points) || null,
country: wine.country || '',
region: wine.region_1 || wine.province || '',
winery: wine.winery || '',
variety: wine.variety || '',
description: wine.description || '',
designation: wine.designation || '',
url: `https://www.winemag.com/?s=${encodeURIComponent(wine.title)}`,
availability: 'Wine Enthusiast Database',
source: 'Wine Enthusiast',
type: wine.variety || 'Wine'
}));
return {
success: true,
wines: formattedWines,
count: formattedWines.length,
source: 'Wine Enthusiast',
datasetInfo: '130k professional wine reviews from Wine Enthusiast Magazine',
timestamp: new Date().toISOString()
};
} catch (error) {
console.error('Wine Enthusiast scraper error:', error.message);
return {
success: false,
wines: [],
error: error.message,
source: 'Wine Enthusiast'
};
}
}
/**
* Load wines from CSV file
*/
async loadWines() {
// Return cached data if available and not expired
if (this.cache && this.cacheTimestamp && (Date.now() - this.cacheTimestamp) < this.CACHE_TTL) {
return this.cache;
}
return new Promise((resolve, reject) => {
const wines = [];
if (!fs.existsSync(this.dataPath)) {
return resolve([]);
}
fs.createReadStream(this.dataPath)
.pipe(csv())
.on('data', (row) => {
wines.push(row);
})
.on('end', () => {
this.cache = wines;
this.cacheTimestamp = Date.now();
console.log(`Loaded ${wines.length} wines from Wine Enthusiast dataset`);
resolve(wines);
})
.on('error', (error) => {
console.error('CSV parsing error:', error);
reject(error);
});
});
}
/**
* Extract vintage year from wine title
*/
extractVintage(title) {
if (!title) return '';
const match = title.match(/\b(19|20)\d{2}\b/);
return match ? match[0] : '';
}
/**
* Get statistics about the dataset
*/
async getStats() {
const wines = await this.loadWines();
return {
totalWines: wines.length,
countries: [...new Set(wines.map(w => w.country).filter(c => c))].length,
varieties: [...new Set(wines.map(w => w.variety).filter(v => v))].length,
avgPrice: wines.reduce((sum, w) => sum + (parseFloat(w.price) || 0), 0) / wines.length,
avgRating: wines.reduce((sum, w) => sum + (parseInt(w.points) || 0), 0) / wines.length
};
}
}
module.exports = new WineEnthusiastScraper();