import http from 'k6/http'; import { check, sleep } from 'k6'; import { Rate, Trend } from 'k6/metrics'; import { BASE_URL, SLA_THRESHOLDS } from '../lib/config.js'; /** * Advanced Search Load Test — Goodgo Platform * * Tests combined geo + text + filter queries against /search and /search/geo * endpoints. Simulates real user behaviour: multi-filter search, pagination, * sort variations, and geo-bounded text queries. */ const advancedSearchDuration = new Trend('advanced_search_duration', true); const geoFilterDuration = new Trend('geo_filter_search_duration', true); const paginatedSearchDuration = new Trend('paginated_search_duration', true); const searchFailRate = new Rate('advanced_search_failures'); export const options = { scenarios: { advanced_search_load: { executor: 'ramping-vus', startVUs: 0, stages: [ { duration: '30s', target: 50 }, // warm up { duration: '1m', target: 200 }, // ramp to peak { duration: '1m', target: 300 }, // stress peak { duration: '30s', target: 0 }, // ramp down ], gracefulRampDown: '10s', }, }, thresholds: { ...SLA_THRESHOLDS, advanced_search_duration: ['p(95)<800'], geo_filter_search_duration: ['p(95)<800'], paginated_search_duration: ['p(95)<500'], advanced_search_failures: ['rate<0.05'], }, }; // Vietnamese text queries combined with property filters const COMBINED_QUERIES = [ { q: 'căn hộ', propertyType: 'APARTMENT', transactionType: 'SALE', priceMin: 1000000000, priceMax: 5000000000 }, { q: 'nhà phố', propertyType: 'HOUSE', transactionType: 'SALE', city: 'TP. Hồ Chí Minh' }, { q: 'đất nền', propertyType: 'LAND', areaMin: 100, areaMax: 500, district: 'Quận 9' }, { q: 'chung cư', transactionType: 'RENT', priceMin: 5000000, priceMax: 20000000, bedrooms: 2 }, { q: 'biệt thự', propertyType: 'HOUSE', areaMin: 200, city: 'Hà Nội' }, { q: 'apartment', transactionType: 'SALE', priceMin: 2000000000, bedrooms: 3 }, { q: 'phòng trọ', transactionType: 'RENT', priceMax: 5000000 }, { q: 'villa', propertyType: 'HOUSE', areaMin: 300, priceMin: 10000000000 }, ]; // Geo-bounded searches with various radii and filters const GEO_FILTER_QUERIES = [ { lat: 10.7769, lng: 106.7009, radiusKm: 5, propertyType: 'APARTMENT', transactionType: 'SALE' }, { lat: 10.7769, lng: 106.7009, radiusKm: 2, propertyType: 'HOUSE', priceMin: 3000000000 }, { lat: 21.0285, lng: 105.8542, radiusKm: 3, transactionType: 'RENT', bedrooms: 2 }, { lat: 10.8231, lng: 106.6297, radiusKm: 1, propertyType: 'LAND', areaMin: 50 }, { lat: 16.0544, lng: 108.2022, radiusKm: 10, transactionType: 'SALE' }, { lat: 21.0067, lng: 105.8400, radiusKm: 5, propertyType: 'APARTMENT', priceMax: 5000000000 }, { lat: 10.8500, lng: 106.7700, radiusKm: 3, propertyType: 'HOUSE', bedrooms: 3 }, { lat: 10.7300, lng: 106.6500, radiusKm: 2, transactionType: 'RENT', priceMax: 15000000 }, ]; // Sort options const SORT_OPTIONS = ['priceAsc', 'priceDesc', 'newest', 'areaDesc']; function buildQueryString(params) { const parts = []; for (const [key, value] of Object.entries(params)) { if (value !== undefined && value !== null) { parts.push(`${key}=${encodeURIComponent(value)}`); } } return parts.join('&'); } export default function () { const iter = __ITER; const scenario = iter % 5; if (scenario <= 1) { // --- Combined text + filter search (40% of traffic) --- const query = COMBINED_QUERIES[iter % COMBINED_QUERIES.length]; const sortBy = SORT_OPTIONS[iter % SORT_OPTIONS.length]; const qs = buildQueryString({ ...query, sortBy, page: 1, perPage: 20, }); const url = `${BASE_URL}/search?${qs}`; const res = http.get(url, { tags: { name: 'GET /search (advanced)' }, }); advancedSearchDuration.add(res.timings.duration); const ok = check(res, { 'advanced search: status 200|503': (r) => r.status === 200 || r.status === 503, 'advanced search: valid response': (r) => { if (r.status === 503) return true; // Typesense unavailable try { return JSON.parse(r.body).data !== undefined; } catch { return false; } }, }); if (!ok) searchFailRate.add(1); } else if (scenario <= 3) { // --- Geo + filter search (40% of traffic) --- const geoQuery = GEO_FILTER_QUERIES[iter % GEO_FILTER_QUERIES.length]; const { lat, lng, radiusKm, ...filters } = geoQuery; const qs = buildQueryString({ lat, lng, radiusKm, ...filters, limit: 20, }); const url = `${BASE_URL}/search/geo?${qs}`; const res = http.get(url, { tags: { name: 'GET /search/geo (filtered)' }, }); geoFilterDuration.add(res.timings.duration); const ok = check(res, { 'geo filter: status 200|503': (r) => r.status === 200 || r.status === 503, 'geo filter: valid response': (r) => { if (r.status === 503) return true; try { return JSON.parse(r.body).data !== undefined; } catch { return false; } }, }); if (!ok) searchFailRate.add(1); } else { // --- Paginated deep search (20% of traffic) --- const query = COMBINED_QUERIES[iter % COMBINED_QUERIES.length]; const page = 1 + (iter % 5); // pages 1-5 const qs = buildQueryString({ q: query.q, propertyType: query.propertyType, page, perPage: 20, sortBy: SORT_OPTIONS[iter % SORT_OPTIONS.length], }); const url = `${BASE_URL}/search?${qs}`; const res = http.get(url, { tags: { name: 'GET /search (paginated)' }, }); paginatedSearchDuration.add(res.timings.duration); const ok = check(res, { 'paginated search: status 200|503': (r) => r.status === 200 || r.status === 503, }); if (!ok) searchFailRate.add(1); } sleep(Math.random() * 1 + 0.3); }