Search Functionality
Search Functionality Overview
Section titled “Search Functionality Overview”The Elasticsearch integration provides advanced search capabilities with native Lithuanian language support, fuzzy matching, and comprehensive filtering options for 60,000+ products.
Lithuanian Language Support
Section titled “Lithuanian Language Support”Custom Analyzer Configuration
Section titled “Custom Analyzer Configuration”The Elasticsearch index is configured with specialized Lithuanian language analysis:
{ "settings": { "analysis": { "analyzer": { "lithuanian_analyzer": { "tokenizer": "standard", "filter": [ "lowercase", "asciifolding", "lithuanian_snowball", "edge_ngram_filter" ] } }, "filter": { "lithuanian_snowball": { "type": "snowball", "language": "Lithuanian" }, "edge_ngram_filter": { "type": "edge_ngram", "min_gram": 2, "max_gram": 15 } } } }}Index Mapping
Section titled “Index Mapping”{ "mappings": { "properties": { "id": { "type": "integer" }, "title": { "type": "text", "analyzer": "lithuanian_analyzer", "fields": { "keyword": { "type": "keyword" }, "raw": { "type": "keyword" } } }, "brand": { "type": "keyword", "fields": { "text": { "type": "text", "analyzer": "lithuanian_analyzer" } } }, "description": { "type": "text", "analyzer": "lithuanian_analyzer" }, "price": { "type": "float" }, "list_price": { "type": "float" }, "discount_percentage": { "type": "float" }, "availability": { "type": "keyword" }, "category": { "type": "keyword" }, "category_path": { "type": "text", "fields": { "keyword": { "type": "keyword" } } }, "sku": { "type": "keyword" }, "ean": { "type": "keyword" }, "image_url": { "type": "keyword", "index": false }, "product_url": { "type": "keyword", "index": false }, "specifications": { "type": "object", "enabled": false }, "created_at": { "type": "date" }, "updated_at": { "type": "date" } } }}Search Features
Section titled “Search Features”1. Full-Text Search
Section titled “1. Full-Text Search”Multi-field search across title, brand, description with relevance scoring:
public function buildSearchQuery($query, $filters) { $must = []; $filter = [];
// Full-text search if (!empty($query)) { $must[] = [ 'multi_match' => [ 'query' => $query, 'fields' => [ 'title^3', // Title has 3x weight 'brand^2', // Brand has 2x weight 'description', // Description has 1x weight 'sku' // SKU has 1x weight ], 'fuzziness' => 'AUTO', 'operator' => 'or', 'type' => 'best_fields' ] ]; }
// Apply filters if (!empty($filters['category'])) { $filter[] = [ 'term' => [ 'category' => $filters['category'] ] ]; }
if (!empty($filters['brand'])) { $filter[] = [ 'terms' => [ 'brand' => (array)$filters['brand'] ] ]; }
if (!empty($filters['min_price']) || !empty($filters['max_price'])) { $range = []; if (!empty($filters['min_price'])) { $range['gte'] = $filters['min_price']; } if (!empty($filters['max_price'])) { $range['lte'] = $filters['max_price']; } $filter[] = [ 'range' => [ 'price' => $range ] ]; }
if (!empty($filters['availability'])) { $filter[] = [ 'term' => [ 'availability' => $filters['availability'] ] ]; }
return [ 'bool' => [ 'must' => $must, 'filter' => $filter ] ];}2. Fuzzy Matching
Section titled “2. Fuzzy Matching”Automatic typo tolerance with configurable fuzziness:
// Fuzzy search configuration'multi_match' => [ 'query' => $query, 'fields' => ['title^3', 'brand^2', 'description'], 'fuzziness' => 'AUTO', // Allows 1-2 character differences 'operator' => 'or']3. Advanced Filtering
Section titled “3. Advanced Filtering”Category Filtering
Section titled “Category Filtering”// Hierarchical category filteringif (!empty($filters['category'])) { $filter[] = [ 'term' => [ 'category' => $filters['category'] ] ];}Price Range Filtering
Section titled “Price Range Filtering”// Price range with min/max supportif (!empty($filters['min_price']) || !empty($filters['max_price'])) { $range = []; if (!empty($filters['min_price'])) { $range['gte'] = $filters['min_price']; } if (!empty($filters['max_price'])) { $range['lte'] = $filters['max_price']; } $filter[] = [ 'range' => [ 'price' => $range ] ];}Brand Filtering
Section titled “Brand Filtering”// Multi-brand filteringif (!empty($filters['brand'])) { $filter[] = [ 'terms' => [ 'brand' => (array)$filters['brand'] ] ];}Availability Filtering
Section titled “Availability Filtering”// Stock status filteringif (!empty($filters['availability'])) { $filter[] = [ 'term' => [ 'availability' => $filters['availability'] ] ];}4. Search Results
Section titled “4. Search Results”Result Structure
Section titled “Result Structure”public function searchProducts($query, $filters = [], $page = 1, $per_page = 20, $sort = []) { $from = ($page - 1) * $per_page;
$search_body = [ 'from' => $from, 'size' => $per_page, 'query' => $this->buildSearchQuery($query, $filters), 'highlight' => [ 'fields' => [ 'title' => [ 'fragment_size' => 150, 'number_of_fragments' => 1 ], 'description' => [ 'fragment_size' => 200, 'number_of_fragments' => 2 ], 'brand' => [ 'fragment_size' => 100, 'number_of_fragments' => 1 ] ], 'pre_tags' => ['<mark>'], 'post_tags' => ['</mark>'] ], 'sort' => $this->buildSortParams($sort) ];
$response = $this->makeRequest('POST', "/{$this->index_name}/_search", $search_body);
return $this->processSearchResponse($response, $page, $per_page);}Response Processing
Section titled “Response Processing”private function processSearchResponse($response, $page, $per_page) { if (!$response || !isset($response['hits'])) { return [ 'hits' => [], 'total' => 0, 'page' => $page, 'per_page' => $per_page, 'total_pages' => 0 ]; }
$hits = []; foreach ($response['hits']['hits'] as $hit) { $hits[] = [ 'id' => $hit['_id'], 'source' => $hit['_source'], 'score' => $hit['_score'], 'highlight' => $hit['highlight'] ?? [] ]; }
return [ 'hits' => $hits, 'total' => $response['hits']['total']['value'], 'page' => $page, 'per_page' => $per_page, 'total_pages' => ceil($response['hits']['total']['value'] / $per_page) ];}User Interface
Section titled “User Interface”Search Form
Section titled “Search Form”<form id="search-form" class="search-form"> <div class="search-input-group"> <input type="text" id="search-query" name="q" placeholder="Search products..."> <button type="submit">Search</button> </div>
<div class="search-filters"> <div class="filter-group"> <label>Category:</label> <select id="category-filter"> <option value="">All Categories</option> <!-- Categories loaded via AJAX --> </select> </div>
<div class="filter-group"> <label>Price Range:</label> <input type="number" id="min-price" placeholder="Min"> <input type="number" id="max-price" placeholder="Max"> </div>
<div class="filter-group"> <label>Brand:</label> <select id="brand-filter" multiple> <!-- Brands loaded via AJAX --> </select> </div>
<div class="filter-group"> <label>Availability:</label> <select id="availability-filter"> <option value="">All</option> <option value="In Stock">In Stock</option> <option value="Out of Stock">Out of Stock</option> <option value="Pre-order">Pre-order</option> </select> </div> </div></form>Search Results Display
Section titled “Search Results Display”<div id="search-results" class="search-results"> <div class="results-header"> <div class="results-count"> <span id="results-count">0</span> products found </div> <div class="results-sort"> <label>Sort by:</label> <select id="sort-select"> <option value="relevance">Relevance</option> <option value="price_asc">Price: Low to High</option> <option value="price_desc">Price: High to Low</option> <option value="newest">Newest</option> <option value="oldest">Oldest</option> </select> </div> </div>
<div class="results-grid" id="results-grid"> <!-- Results loaded via AJAX --> </div>
<div class="pagination" id="pagination"> <!-- Pagination loaded via AJAX --> </div></div>JavaScript Search Implementation
Section titled “JavaScript Search Implementation”class ProductSearch { constructor() { this.currentPage = 1; this.currentQuery = ''; this.currentFilters = {}; this.init(); }
init() { this.bindEvents(); this.loadFilters(); }
bindEvents() { document.getElementById('search-form').addEventListener('submit', (e) => { e.preventDefault(); this.performSearch(); });
document.getElementById('sort-select').addEventListener('change', () => { this.performSearch(); });
// Filter change events ['category-filter', 'min-price', 'max-price', 'brand-filter', 'availability-filter'].forEach(id => { document.getElementById(id).addEventListener('change', () => { this.performSearch(); }); }); }
performSearch() { this.currentQuery = document.getElementById('search-query').value; this.currentFilters = this.getFilters(); this.currentPage = 1;
this.searchProducts(); }
getFilters() { return { category: document.getElementById('category-filter').value, min_price: document.getElementById('min-price').value, max_price: document.getElementById('max-price').value, brand: Array.from(document.getElementById('brand-filter').selectedOptions).map(o => o.value), availability: document.getElementById('availability-filter').value }; }
searchProducts() { const data = { action: 'search_products', nonce: ajax_nonce, query: this.currentQuery, page: this.currentPage, per_page: 20, sort: this.getSortOption(), ...this.currentFilters };
jQuery.post(ajaxurl, data, (response) => { if (response.success) { this.displayResults(response.data); } else { this.displayError(response.data); } }); }
displayResults(data) { document.getElementById('results-count').textContent = data.total; document.getElementById('results-grid').innerHTML = this.renderResults(data.hits); document.getElementById('pagination').innerHTML = this.renderPagination(data); }
renderResults(hits) { return hits.map(hit => ` <div class="product-card"> <div class="product-image"> <img src="${hit.source.image_url}" alt="${hit.source.title}"> </div> <div class="product-info"> <h3 class="product-title">${this.highlightText(hit.source.title, hit.highlight.title)}</h3> <p class="product-brand">${hit.source.brand}</p> <p class="product-price">€${hit.source.price}</p> <p class="product-availability">${hit.source.availability}</p> </div> </div> `).join(''); }
highlightText(text, highlights) { if (!highlights || !highlights.length) { return text; }
return highlights[0]; }}
// Initialize searchdocument.addEventListener('DOMContentLoaded', () => { new ProductSearch();});Aggregations
Section titled “Aggregations”Faceted Search
Section titled “Faceted Search”public function getSearchAggregations() { return [ 'categories' => [ 'terms' => [ 'field' => 'category', 'size' => 50 ] ], 'brands' => [ 'terms' => [ 'field' => 'brand', 'size' => 100 ] ], 'price_ranges' => [ 'range' => [ 'field' => 'price', 'ranges' => [ ['to' => 50], ['from' => 50, 'to' => 100], ['from' => 100, 'to' => 250], ['from' => 250, 'to' => 500], ['from' => 500] ] ] ], 'availability' => [ 'terms' => [ 'field' => 'availability' ] ], 'price_stats' => [ 'stats' => [ 'field' => 'price' ] ] ];}Performance Optimization
Section titled “Performance Optimization”Caching
Section titled “Caching”// Cache search resultspublic function getCachedSearchResults($cache_key, $callback) { $cached = wp_cache_get($cache_key, 'elasticsearch_search');
if ($cached === false) { $cached = $callback(); wp_cache_set($cache_key, $cached, 'elasticsearch_search', 300); // 5 min cache }
return $cached;}Query Optimization
Section titled “Query Optimization”// Optimize search queriesprivate function optimizeSearchQuery($query, $filters) { // Use filters instead of queries when possible $filter_clauses = []; $query_clauses = [];
// Convert exact matches to filters if (!empty($filters['category'])) { $filter_clauses[] = ['term' => ['category' => $filters['category']]]; }
// Keep fuzzy search in query if (!empty($query)) { $query_clauses[] = ['multi_match' => ['query' => $query, 'fields' => ['title^3', 'brand^2', 'description']]]; }
return [ 'bool' => [ 'must' => $query_clauses, 'filter' => $filter_clauses ] ];}Next Steps
Section titled “Next Steps”- Monitoring & Management - Console and monitoring features
- API Endpoints - API reference
- Configuration - System configuration
- Performance Optimization - Performance tuning