Appearance
AJAX Search Endpoints 
Search with Elastic provides secure AJAX endpoints for implementing dynamic search experiences in your frontend templates.
Available Endpoints 
Basic Search 
POST /search-with-elastic/search
Simple search endpoint for basic text queries.
Request Parameters 
| Parameter | Type | Required | Description | 
|---|---|---|---|
query | string | Yes | The search query text | 
siteId | integer | No | Site ID to search within (defaults to current site) | 
Response Format 
json
{
    "success": true,
    "results": [
        {
            "id": 123,
            "title": "Search Result Title",
            "url": "/path/to/content",
            "score": 0.95,
            "highlight": {
                "title": ["Search Result <mark>Title</mark>"],
                "content": ["...matching <mark>content</mark> excerpt..."]
            }
        }
    ],
    "meta": {
        "query": "search term",
        "siteId": 1,
        "timestamp": 1704067200
    }
}Advanced Search 
POST /search-with-elastic/search-extra
Extended search endpoint with additional options for complex queries.
Request Parameters 
| Parameter | Type | Required | Description | 
|---|---|---|---|
query | string | Yes | The search query text | 
fuzzy | boolean | No | Enable fuzzy matching for typos | 
fields | array/string | No | Specific fields to search in | 
siteId | integer | No | Site ID to search within | 
size | integer | No | Number of results to return (default: 10) | 
from | integer | No | Offset for pagination (default: 0) | 
Response Format 
json
{
    "success": true,
    "results": [...],
    "meta": {
        "query": "search term",
        "siteId": 1,
        "timestamp": 1704067200,
        "options": {
            "fuzzy": true,
            "fields": ["title", "content"],
            "size": 10,
            "from": 0
        }
    }
}Implementation Examples 
Basic Search with JavaScript 
javascript
// Simple search implementation
async function performSearch(searchTerm) {
    const csrfToken = document.querySelector('meta[name="csrf-token"]').content;
    
    try {
        const response = await fetch('/search-with-elastic/search', {
            method: 'POST',
            headers: {
                'Content-Type': 'application/json',
                'X-CSRF-Token': csrfToken
            },
            body: JSON.stringify({
                query: searchTerm
            })
        });
        
        if (response.ok) {
            const data = await response.json();
            displayResults(data.results);
        } else if (response.status === 429) {
            handleRateLimit(response);
        } else {
            console.error('Search failed:', response.statusText);
        }
    } catch (error) {
        console.error('Search error:', error);
    }
}
function displayResults(results) {
    const container = document.getElementById('search-results');
    container.innerHTML = results.map(result => `
        <div class="search-result">
            <h3><a href="${result.url}">${result.title}</a></h3>
            <p>${result.highlight?.content?.[0] || result.excerpt}</p>
            <small>Score: ${result.score.toFixed(2)}</small>
        </div>
    `).join('');
}
function handleRateLimit(response) {
    const retryAfter = response.headers.get('Retry-After');
    console.warn(`Rate limited. Retry after ${retryAfter} seconds`);
    // Show user-friendly message
    document.getElementById('search-error').textContent = 
        'Too many searches. Please wait a moment and try again.';
}Advanced Search with Options 
javascript
// Advanced search with fuzzy matching and field selection
async function advancedSearch(query, options = {}) {
    const csrfToken = document.querySelector('meta[name="csrf-token"]').content;
    
    const searchParams = {
        query: query,
        fuzzy: options.fuzzy || false,
        fields: options.fields || ['title', 'content'],
        size: options.size || 10,
        from: options.from || 0
    };
    
    try {
        const response = await fetch('/search-with-elastic/search-extra', {
            method: 'POST',
            headers: {
                'Content-Type': 'application/json',
                'X-CSRF-Token': csrfToken
            },
            body: JSON.stringify(searchParams)
        });
        
        if (response.ok) {
            return await response.json();
        }
        throw new Error(`Search failed: ${response.statusText}`);
    } catch (error) {
        console.error('Advanced search error:', error);
        throw error;
    }
}
// Usage example
advancedSearch('craft cms', {
    fuzzy: true,
    fields: ['title', 'excerpt', 'content'],
    size: 20
}).then(data => {
    console.log(`Found ${data.results.length} results`);
    displayResults(data.results);
});Search with jQuery 
javascript
// jQuery implementation
$(document).ready(function() {
    $('#search-form').on('submit', function(e) {
        e.preventDefault();
        
        const searchTerm = $('#search-input').val();
        const csrfToken = $('meta[name="csrf-token"]').attr('content');
        
        $.ajax({
            url: '/search-with-elastic/search',
            method: 'POST',
            headers: {
                'X-CSRF-Token': csrfToken
            },
            data: JSON.stringify({ query: searchTerm }),
            contentType: 'application/json',
            success: function(data) {
                displaySearchResults(data.results);
            },
            error: function(xhr) {
                if (xhr.status === 429) {
                    const retryAfter = xhr.getResponseHeader('Retry-After');
                    showRateLimitMessage(retryAfter);
                } else {
                    showErrorMessage('Search failed. Please try again.');
                }
            }
        });
    });
});React Component Example 
jsx
import React, { useState, useCallback } from 'react';
function SearchComponent() {
    const [query, setQuery] = useState('');
    const [results, setResults] = useState([]);
    const [loading, setLoading] = useState(false);
    const [error, setError] = useState(null);
    
    const performSearch = useCallback(async () => {
        if (!query.trim()) return;
        
        setLoading(true);
        setError(null);
        
        const csrfToken = document.querySelector('meta[name="csrf-token"]')?.content;
        
        try {
            const response = await fetch('/search-with-elastic/search-extra', {
                method: 'POST',
                headers: {
                    'Content-Type': 'application/json',
                    'X-CSRF-Token': csrfToken
                },
                body: JSON.stringify({
                    query: query,
                    fuzzy: true,
                    size: 20
                })
            });
            
            if (response.status === 429) {
                const retryAfter = response.headers.get('Retry-After');
                setError(`Rate limited. Please wait ${retryAfter} seconds.`);
                return;
            }
            
            if (!response.ok) {
                throw new Error('Search failed');
            }
            
            const data = await response.json();
            setResults(data.results);
        } catch (err) {
            setError(err.message);
        } finally {
            setLoading(false);
        }
    }, [query]);
    
    return (
        <div className="search-component">
            <form onSubmit={(e) => { e.preventDefault(); performSearch(); }}>
                <input
                    type="text"
                    value={query}
                    onChange={(e) => setQuery(e.target.value)}
                    placeholder="Search..."
                    disabled={loading}
                />
                <button type="submit" disabled={loading}>
                    {loading ? 'Searching...' : 'Search'}
                </button>
            </form>
            
            {error && <div className="error">{error}</div>}
            
            <div className="results">
                {results.map(result => (
                    <div key={result.id} className="result-item">
                        <h3><a href={result.url}>{result.title}</a></h3>
                        <p dangerouslySetInnerHTML={{ 
                            __html: result.highlight?.content?.[0] || result.excerpt 
                        }} />
                    </div>
                ))}
            </div>
        </div>
    );
}
export default SearchComponent;CSRF Token Setup 
All AJAX requests require a CSRF token for security. Include it in your layout:
twig
{# In your layout template #}
<meta name="csrf-token" content="{{ craft.app.request.csrfToken }}">Alternative method using Craft's JavaScript variables:
twig
{% js %}
    window.csrfTokenName = "{{ craft.app.config.general.csrfTokenName }}";
    window.csrfTokenValue = "{{ craft.app.request.csrfToken }}";
{% endjs %}Rate Limiting Handling 
When rate limiting is enabled, the endpoints return HTTP 429 when limits are exceeded:
javascript
// Complete rate limiting handler
function handleSearchWithRateLimit(searchTerm) {
    const startTime = Date.now();
    
    performSearch(searchTerm)
        .then(handleSuccess)
        .catch(error => {
            if (error.status === 429) {
                const retryAfter = parseInt(error.headers.get('Retry-After')) || 60;
                const remainingRequests = error.headers.get('X-RateLimit-Remaining');
                const resetTime = error.headers.get('X-RateLimit-Reset');
                
                console.log(`Rate limit exceeded. Retry after ${retryAfter} seconds`);
                console.log(`Remaining requests: ${remainingRequests}`);
                console.log(`Reset time: ${new Date(resetTime * 1000).toLocaleTimeString()}`);
                
                // Implement exponential backoff
                setTimeout(() => {
                    handleSearchWithRateLimit(searchTerm);
                }, retryAfter * 1000);
                
                // Show user message
                showUserMessage({
                    type: 'warning',
                    message: `Search limit reached. Please wait ${retryAfter} seconds.`,
                    duration: retryAfter * 1000
                });
            } else {
                console.error('Search failed:', error);
                showUserMessage({
                    type: 'error',
                    message: 'Search failed. Please try again later.'
                });
            }
        });
}Pagination Implementation 
Implement pagination using the size and from parameters:
javascript
class SearchPagination {
    constructor(resultsPerPage = 10) {
        this.resultsPerPage = resultsPerPage;
        this.currentPage = 1;
        this.totalResults = 0;
    }
    
    async search(query, page = 1) {
        this.currentPage = page;
        const from = (page - 1) * this.resultsPerPage;
        
        const response = await fetch('/search-with-elastic/search-extra', {
            method: 'POST',
            headers: {
                'Content-Type': 'application/json',
                'X-CSRF-Token': this.getCsrfToken()
            },
            body: JSON.stringify({
                query: query,
                size: this.resultsPerPage,
                from: from
            })
        });
        
        const data = await response.json();
        this.totalResults = data.meta.total || data.results.length;
        
        return {
            results: data.results,
            pagination: {
                current: this.currentPage,
                total: Math.ceil(this.totalResults / this.resultsPerPage),
                hasNext: from + this.resultsPerPage < this.totalResults,
                hasPrev: page > 1
            }
        };
    }
    
    getCsrfToken() {
        return document.querySelector('meta[name="csrf-token"]')?.content || '';
    }
}
// Usage
const paginator = new SearchPagination(20);
paginator.search('craft cms', 1).then(({ results, pagination }) => {
    displayResults(results);
    displayPagination(pagination);
});Search Suggestions 
Implement type-ahead suggestions with debouncing:
javascript
class SearchSuggestions {
    constructor(inputElement, options = {}) {
        this.input = inputElement;
        this.debounceTime = options.debounceTime || 300;
        this.minChars = options.minChars || 3;
        this.maxSuggestions = options.maxSuggestions || 5;
        this.debounceTimer = null;
        
        this.init();
    }
    
    init() {
        this.input.addEventListener('input', (e) => {
            this.handleInput(e.target.value);
        });
    }
    
    handleInput(value) {
        clearTimeout(this.debounceTimer);
        
        if (value.length < this.minChars) {
            this.hideSuggestions();
            return;
        }
        
        this.debounceTimer = setTimeout(() => {
            this.fetchSuggestions(value);
        }, this.debounceTime);
    }
    
    async fetchSuggestions(query) {
        try {
            const response = await fetch('/search-with-elastic/search-extra', {
                method: 'POST',
                headers: {
                    'Content-Type': 'application/json',
                    'X-CSRF-Token': this.getCsrfToken()
                },
                body: JSON.stringify({
                    query: query,
                    size: this.maxSuggestions,
                    fields: ['title'] // Search only in titles for suggestions
                })
            });
            
            if (response.ok) {
                const data = await response.json();
                this.displaySuggestions(data.results);
            }
        } catch (error) {
            console.error('Suggestion fetch error:', error);
        }
    }
    
    displaySuggestions(results) {
        // Implementation for displaying suggestions dropdown
        const container = this.getSuggestionsContainer();
        container.innerHTML = results.map(r => `
            <div class="suggestion-item" data-url="${r.url}">
                ${r.title}
            </div>
        `).join('');
        container.style.display = 'block';
    }
    
    hideSuggestions() {
        const container = this.getSuggestionsContainer();
        container.style.display = 'none';
    }
    
    getSuggestionsContainer() {
        // Get or create suggestions container
        let container = document.getElementById('search-suggestions');
        if (!container) {
            container = document.createElement('div');
            container.id = 'search-suggestions';
            container.className = 'suggestions-dropdown';
            this.input.parentNode.appendChild(container);
        }
        return container;
    }
    
    getCsrfToken() {
        return document.querySelector('meta[name="csrf-token"]')?.content || '';
    }
}
// Initialize suggestions
new SearchSuggestions(document.getElementById('search-input'), {
    debounceTime: 250,
    minChars: 2,
    maxSuggestions: 8
});Error Handling Best Practices 
javascript
class SearchErrorHandler {
    static handle(error, response = null) {
        // Rate limiting
        if (response?.status === 429) {
            return {
                type: 'rate_limit',
                message: 'Too many requests. Please wait.',
                retryAfter: response.headers.get('Retry-After')
            };
        }
        
        // Network errors
        if (error.name === 'NetworkError' || !navigator.onLine) {
            return {
                type: 'network',
                message: 'Network error. Check your connection.'
            };
        }
        
        // Timeout
        if (error.name === 'AbortError') {
            return {
                type: 'timeout',
                message: 'Search timed out. Try again.'
            };
        }
        
        // Server errors
        if (response?.status >= 500) {
            return {
                type: 'server',
                message: 'Server error. Please try later.'
            };
        }
        
        // Client errors
        if (response?.status >= 400) {
            return {
                type: 'client',
                message: 'Invalid search request.'
            };
        }
        
        // Unknown errors
        return {
            type: 'unknown',
            message: 'An error occurred during search.'
        };
    }
}Testing AJAX Endpoints 
Test your endpoints using curl or a REST client:
bash
# Basic search test
curl -X POST https://yoursite.com/search-with-elastic/search \
  -H "Content-Type: application/json" \
  -H "X-CSRF-Token: your-csrf-token" \
  -d '{"query": "test search"}'
# Advanced search test
curl -X POST https://yoursite.com/search-with-elastic/search-extra \
  -H "Content-Type: application/json" \
  -H "X-CSRF-Token: your-csrf-token" \
  -d '{
    "query": "craft cms",
    "fuzzy": true,
    "fields": ["title", "content"],
    "size": 20,
    "from": 0
  }'Security Considerations 
- Always include CSRF tokens in your AJAX requests
 - Validate and sanitize search input on the client side
 - Implement rate limiting feedback for users
 - Use HTTPS in production environments
 - Handle errors gracefully without exposing system details
 - Implement request timeouts to prevent hanging requests
 - Cache search results when appropriate to reduce server load