← back to Wine Finder
services/xWinesScraper.js
319 lines
const axios = require('axios');
const csv = require('csv-parser');
const { Readable } = require('stream');
/**
* X-Wines Dataset Scraper
*
* A world wines dataset with user ratings for recommendation systems
* Source: Rogério Xavier de Azambuja / Kaggle / GitHub
*
* Dataset Details:
* - 100,646 wine instances with 17 attributes
* - 21,013,536 user ratings (5-star scale, 2012-2021)
* - 1,056,079 anonymized users
* - Wines from 62 different countries
*
* Files:
* - XWines_100K_wines.csv - Wine attributes and metadata
* - XWines_21M_ratings.csv - User ratings with timestamps
*
* Kaggle: https://www.kaggle.com/datasets/rogerioxavier/x-wines-slim-version
* GitHub: https://github.com/rogerioxavier/X-Wines
* License: Open for educational and research use
*/
class XWinesScraper {
constructor() {
this.kaggleDataset = 'rogerioxavier/x-wines-slim-version';
this.githubUrl = 'https://github.com/rogerioxavier/X-Wines';
this.rawDataUrl = 'https://raw.githubusercontent.com/rogerioxavier/X-Wines/main';
// Cache for dataset
this.winesCache = null;
this.ratingsCache = null;
this.cacheTimestamp = null;
this.cacheDuration = 24 * 60 * 60 * 1000; // 24 hours
this.datasetInfo = {
name: 'X-Wines Dataset',
source: 'Rogério Xavier (Open Web 2022)',
wines: 100646,
ratings: 21013536,
users: 1056079,
countries: 62,
ratingPeriod: '2012-2021',
license: 'Open for educational/research use'
};
}
/**
* Search X-Wines dataset for wines
* Note: This requires the CSV dataset to be downloaded locally or accessed via GitHub
*/
async search(query, options = {}) {
try {
const {
limit = 50,
minRating = 0,
maxPrice = 10000,
country = null,
minYear = 1900,
maxYear = 2023
} = options;
// For now, return dataset information with instructions
// In production, this would query the downloaded CSV data or GitHub raw files
return {
success: true,
wines: [],
count: 0,
source: 'X-Wines Dataset',
message: 'X-Wines dataset requires local download or Kaggle API access. Dataset contains 100k wines with 21M ratings.',
datasetInfo: this.datasetInfo,
downloadInstructions: {
option1: {
method: 'Kaggle Download',
url: 'https://www.kaggle.com/datasets/rogerioxavier/x-wines-slim-version',
steps: [
'Create Kaggle account',
'Download XWines_100K_wines.csv',
'Download XWines_21M_ratings.csv',
'Place files in /data/xwines/ directory'
]
},
option2: {
method: 'Kaggle API',
command: 'kaggle datasets download -d rogerioxavier/x-wines-slim-version',
requiresSetup: 'Install kaggle CLI and configure API token'
},
option3: {
method: 'GitHub Clone',
url: 'https://github.com/rogerioxavier/X-Wines',
note: 'May contain sample data only'
}
},
timestamp: new Date().toISOString()
};
} catch (error) {
console.error('X-Wines scraper error:', error.message);
return {
success: false,
wines: [],
error: error.message,
source: 'X-Wines'
};
}
}
/**
* Load wines CSV from local file
*/
async loadWinesDataset(csvPath) {
return new Promise((resolve, reject) => {
const results = [];
const fs = require('fs');
if (!fs.existsSync(csvPath)) {
return reject(new Error(`Dataset file not found: ${csvPath}`));
}
fs.createReadStream(csvPath)
.pipe(csv())
.on('data', (data) => results.push(data))
.on('end', () => {
this.winesCache = results;
this.cacheTimestamp = Date.now();
resolve(results);
})
.on('error', reject);
});
}
/**
* Load ratings CSV from local file
*/
async loadRatingsDataset(csvPath) {
return new Promise((resolve, reject) => {
const results = [];
const fs = require('fs');
if (!fs.existsSync(csvPath)) {
return reject(new Error(`Dataset file not found: ${csvPath}`));
}
fs.createReadStream(csvPath)
.pipe(csv())
.on('data', (data) => results.push(data))
.on('end', () => {
this.ratingsCache = results;
resolve(results);
})
.on('error', reject);
});
}
/**
* Search loaded wines dataset
*/
async searchLoadedData(query, options = {}) {
if (!this.winesCache) {
return {
success: false,
wines: [],
error: 'Dataset not loaded. Call loadWinesDataset() first.',
source: 'X-Wines'
};
}
const {
limit = 50,
minRating = 0,
maxPrice = 10000,
country = null
} = options;
const queryLower = query.toLowerCase();
// Filter wines by query
let wines = this.winesCache.filter(wine => {
const name = (wine.WineName || wine.name || '').toLowerCase();
const winery = (wine.Winery || wine.winery || '').toLowerCase();
const region = (wine.Region || wine.region || '').toLowerCase();
const wineCountry = (wine.Country || wine.country || '').toLowerCase();
const matchesQuery = name.includes(queryLower) ||
winery.includes(queryLower) ||
region.includes(queryLower);
const matchesCountry = !country || wineCountry === country.toLowerCase();
return matchesQuery && matchesCountry;
});
// Apply rating and price filters if available
wines = wines.filter(wine => {
const rating = parseFloat(wine.Rating || wine.rating || 0);
const price = parseFloat(wine.Price || wine.price || 0);
return rating >= minRating && price <= maxPrice;
});
// Limit results
wines = wines.slice(0, limit);
// Transform to standard wine format
const formattedWines = wines.map(wine => ({
name: wine.WineName || wine.name || 'Unknown',
vintage: wine.Year || wine.year || '',
price: parseFloat(wine.Price || wine.price) || null,
rating: parseFloat(wine.Rating || wine.rating) || null,
country: wine.Country || wine.country || '',
region: wine.Region || wine.region || '',
winery: wine.Winery || wine.winery || '',
type: wine.Type || wine.type || '',
grapes: wine.Grapes || wine.grapes || '',
url: wine.URL || wine.url || '',
availability: 'User-rated wines (2012-2021)',
source: 'X-Wines Dataset',
wineId: wine.WineID || wine.wine_id || wine.id || ''
}));
return {
success: true,
wines: formattedWines,
count: formattedWines.length,
source: 'X-Wines Dataset',
timestamp: new Date().toISOString()
};
}
/**
* Get wine ratings for a specific wine
*/
async getWineRatings(wineId) {
if (!this.ratingsCache) {
return {
success: false,
ratings: [],
error: 'Ratings dataset not loaded. Call loadRatingsDataset() first.'
};
}
const ratings = this.ratingsCache.filter(r =>
(r.WineID || r.wine_id || r.id) === wineId
);
return {
success: true,
wineId,
ratings: ratings.map(r => ({
userId: r.UserID || r.user_id,
rating: parseFloat(r.Rating || r.rating),
date: r.Date || r.date || r.timestamp
})),
count: ratings.length,
averageRating: ratings.length > 0
? ratings.reduce((sum, r) => sum + parseFloat(r.Rating || r.rating || 0), 0) / ratings.length
: null
};
}
/**
* Get dataset statistics
*/
getStats() {
return {
datasetInfo: this.datasetInfo,
githubUrl: this.githubUrl,
kaggleUrl: `https://www.kaggle.com/datasets/${this.kaggleDataset}`,
citation: 'Xavier, R. X-Wines: A Wine Dataset for Recommender Systems and Machine Learning. Big Data and Cognitive Computing, 2023.',
features: [
'100,646 wine instances',
'21 million user ratings',
'1 million+ anonymized users',
'Wines from 62 countries',
'Rating period: 2012-2021',
'17 wine attributes per instance',
'Temporal rating data',
'User-generated ratings'
],
attributes: [
'Wine name',
'Winery',
'Year/Vintage',
'Price',
'Rating',
'Country',
'Region',
'Type (red, white, etc.)',
'Grapes/Varietals'
],
useCases: [
'Wine recommendation systems',
'Collaborative filtering',
'Rating prediction',
'User preference analysis',
'Wine popularity trends',
'Price-rating correlation',
'Geographic wine analysis',
'Machine learning research'
]
};
}
/**
* Check if cache is valid
*/
isCacheValid() {
if (!this.winesCache || !this.cacheTimestamp) {
return false;
}
return (Date.now() - this.cacheTimestamp) < this.cacheDuration;
}
}
module.exports = new XWinesScraper();