Skip to content

Search Functionality

The Elasticsearch integration provides advanced search capabilities with native Lithuanian language support, fuzzy matching, and comprehensive filtering options for 60,000+ products.

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
}
}
}
}
}
{
"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"
}
}
}
}

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
]
];
}

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'
]
// Hierarchical category filtering
if (!empty($filters['category'])) {
$filter[] = [
'term' => [
'category' => $filters['category']
]
];
}
// Price range with min/max support
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
]
];
}
// Multi-brand filtering
if (!empty($filters['brand'])) {
$filter[] = [
'terms' => [
'brand' => (array)$filters['brand']
]
];
}
// Stock status filtering
if (!empty($filters['availability'])) {
$filter[] = [
'term' => [
'availability' => $filters['availability']
]
];
}
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);
}
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)
];
}
<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>
<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>
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 search
document.addEventListener('DOMContentLoaded', () => {
new ProductSearch();
});
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'
]
]
];
}
// Cache search results
public 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;
}
// Optimize search queries
private 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
]
];
}