Complete Search Page: End-to-End Example

Documentation > WEB CRAWLER-Code Examples > Complete Search Page: End-to-End Example

Complete Search Page: End-to-End Example

This is a complete, production-ready search page that you can copy, paste, and adapt. It includes a search box, results with highlighting, pagination, a language facet sidebar, autocomplete, and spellcheck suggestions — all in a single HTML file.

This example uses keyword search (eDisMax) which works directly from the browser with CORS enabled. For full hybrid search with AI/vector, see the PHP and Node.js examples in the Code Examples article.


The Complete HTML File

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Search</title>
    <style>
        * { box-sizing: border-box; margin: 0; padding: 0; }
        body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; color: #333; background: #f8f9fa; }

        .container { max-width: 1100px; margin: 0 auto; padding: 20px; }
        .header { text-align: center; padding: 40px 0 30px; }
        .header h1 { font-size: 28px; margin-bottom: 16px; }

        /* Search box */
        .search-form { display: flex; max-width: 640px; margin: 0 auto 30px; }
        .search-form input {
            flex: 1; padding: 12px 16px; font-size: 16px;
            border: 2px solid #ddd; border-right: none;
            border-radius: 8px 0 0 8px; outline: none;
        }
        .search-form input:focus { border-color: #e8650a; }
        .search-form button {
            padding: 12px 28px; font-size: 16px; background: #e8650a;
            color: white; border: 2px solid #e8650a; border-radius: 0 8px 8px 0;
            cursor: pointer; font-weight: 600;
        }

        /* Autocomplete */
        .ac-wrap { position: relative; flex: 1; }
        .ac-list {
            position: absolute; top: 100%; left: 0; right: 0; z-index: 100;
            background: white; border: 1px solid #ddd; border-top: none;
            border-radius: 0 0 8px 8px; box-shadow: 0 4px 12px rgba(0,0,0,0.1);
            display: none; max-height: 300px; overflow-y: auto;
        }
        .ac-list a {
            display: block; padding: 10px 16px; text-decoration: none;
            color: #333; border-bottom: 1px solid #f0f0f0; font-size: 14px;
        }
        .ac-list a:hover, .ac-list a.active { background: #f5f5f5; }

        /* Layout */
        .content { display: flex; gap: 30px; }
        .sidebar { width: 220px; flex-shrink: 0; }
        .main { flex: 1; min-width: 0; }

        /* Facets */
        .facet-group h4 { font-size: 13px; text-transform: uppercase; color: #888; margin-bottom: 8px; letter-spacing: 0.5px; }
        .facet-group { margin-bottom: 24px; }
        .facet-group ul { list-style: none; }
        .facet-group li a { display: flex; justify-content: space-between; padding: 5px 0; text-decoration: none; color: #555; font-size: 14px; }
        .facet-group li a:hover { color: #e8650a; }
        .facet-group li.active a { font-weight: 700; color: #e8650a; }
        .facet-count { color: #aaa; font-size: 12px; }

        /* Results */
        .result-info { font-size: 14px; color: #666; margin-bottom: 16px; }
        .spellcheck { margin-bottom: 16px; font-size: 14px; }
        .spellcheck a { color: #e8650a; font-weight: 600; text-decoration: none; }

        .result { background: white; padding: 18px 20px; margin-bottom: 12px; border-radius: 8px; border: 1px solid #e8e8e8; }
        .result:hover { border-color: #ccc; }
        .result h3 { font-size: 17px; margin-bottom: 4px; }
        .result h3 a { color: #1a0dab; text-decoration: none; }
        .result h3 a:hover { text-decoration: underline; }
        .result .url { color: #006621; font-size: 13px; margin-bottom: 6px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
        .result .snippet { color: #545454; font-size: 14px; line-height: 1.6; }
        .result em { font-weight: 700; font-style: normal; color: #333; }
        .result .meta { margin-top: 6px; font-size: 12px; color: #999; }

        /* Pagination */
        .pagination { display: flex; justify-content: center; gap: 4px; margin: 30px 0; flex-wrap: wrap; }
        .pagination a, .pagination span {
            display: inline-block; padding: 8px 14px; font-size: 14px;
            border: 1px solid #ddd; border-radius: 6px; text-decoration: none; color: #333;
        }
        .pagination a:hover { background: #f0f0f0; }
        .pagination .current { background: #e8650a; color: white; border-color: #e8650a; font-weight: 600; }

        .no-results { text-align: center; padding: 60px 20px; color: #888; }

        @media (max-width: 768px) {
            .content { flex-direction: column; }
            .sidebar { width: 100%; }
        }
    </style>
</head>
<body>

<div class="container">
    <div class="header">
        <h1>Search</h1>
        <div class="search-form">
            <div class="ac-wrap">
                <input type="text" id="q" placeholder="Search..." autocomplete="off">
                <div class="ac-list" id="acList"></div>
            </div>
            <button onclick="doSearch()">Search</button>
        </div>
    </div>

    <div class="content">
        <div class="sidebar" id="facets"></div>
        <div class="main" id="results"></div>
    </div>
</div>

<script>
// ===========================================================================
// CONFIGURATION - Replace with your Opensolr index details
// ===========================================================================
var SOLR_HOST = 'https://YOUR_HOST/solr/YOUR_INDEX';
var SOLR_USER = 'opensolr';
var SOLR_PASS = 'YOUR_API_KEY';
var ROWS = 10;

var currentQuery = '';
var currentStart = 0;
var activeFilters = {};
var acTimer;

var qInput = document.getElementById('q');
var acList = document.getElementById('acList');

// Enter key to search
qInput.addEventListener('keydown', function(e) {
    if (e.key === 'Enter') { acList.style.display = 'none'; doSearch(); }
    if (e.key === 'Escape') { acList.style.display = 'none'; }
});

// Autocomplete on input
qInput.addEventListener('input', function() {
    clearTimeout(acTimer);
    var val = this.value.trim();
    if (val.length < 2) { acList.style.display = 'none'; return; }
    acTimer = setTimeout(function() { fetchAutocomplete(val); }, 150);
});

// Hide autocomplete on click outside
document.addEventListener('click', function(e) {
    if (!e.target.closest('.ac-wrap')) acList.style.display = 'none';
});

// ---- Autocomplete ----
function fetchAutocomplete(q) {
    var url = SOLR_HOST + '/autocomplete?' + new URLSearchParams({
        q: q, rows: 7, wt: 'json', fl: 'title,uri',
        qf: 'title_tags_ws tags_ws title_tags tags', defType: 'edismax'
    });
    solrFetch(url).then(function(data) {
        var docs = data.response.docs;
        if (!docs.length) { acList.style.display = 'none'; return; }
        acList.innerHTML = docs.map(function(d) {
            return '<a href="#" onclick="event.preventDefault(); pickAc(\''+esc(d.title)+'\')">' + esc(d.title) + '</a>';
        }).join('');
        acList.style.display = 'block';
    });
}

function pickAc(title) {
    qInput.value = title;
    acList.style.display = 'none';
    doSearch();
}

// ---- Main Search ----
function doSearch(start) {
    currentQuery = qInput.value.trim();
    currentStart = start || 0;
    if (!currentQuery) return;

    var params = {
        q: currentQuery, df: 'title', defType: 'edismax',
        qf: 'title^5 description^4 uri^0.5 text^1',
        pf: 'title^10 description^8',
        mm: '2<-1 5<-2',
        fl: 'id,uri,title,description,og_image,meta_icon,creation_date,meta_domain,meta_detected_language,score',
        rows: ROWS, start: currentStart, wt: 'json',
        hl: 'true', 'hl.fl': 'title,description,text', 'hl.method': 'unified',
        'hl.fragsize': 200, 'hl.tag.pre': '<em>', 'hl.tag.post': '</em>',
        facet: 'true', 'facet.field': 'meta_detected_language', 'facet.mincount': 1,
        spellcheck: 'true', 'spellcheck.q': currentQuery, 'spellcheck.count': 5,
        'spellcheck.collate': 'true', 'spellcheck.maxCollationTries': 15,
        fq: 'content_type:text*'
    };

    // Apply active filters
    var fqList = [params.fq];
    for (var field in activeFilters) {
        fqList.push(field + ':' + activeFilters[field]);
    }

    var url = SOLR_HOST + '/select?' + buildQuery(params, fqList);
    solrFetch(url).then(function(data) { render(data); });
}

// ---- Render ----
function render(data) {
    var docs = data.response.docs;
    var hl = data.highlighting || {};
    var total = data.response.numFound;
    var resultsDiv = document.getElementById('results');
    var html = '';

    // Spellcheck
    if (data.spellcheck && data.spellcheck.collations && total < 5) {
        for (var i = 0; i < data.spellcheck.collations.length; i++) {
            if (data.spellcheck.collations[i] === 'collation') {
                var suggestion = data.spellcheck.collations[i + 1];
                if (suggestion) {
                    html += '<div class="spellcheck">Did you mean: <a href="#" onclick="event.preventDefault();document.getElementById(\'q\').value=\''+esc(suggestion)+'\';doSearch()">' + esc(suggestion) + '</a>?</div>';
                }
                break;
            }
        }
    }

    // Result count
    if (total > 0) {
        var from = currentStart + 1;
        var to = Math.min(currentStart + ROWS, total);
        html += '<div class="result-info">Showing ' + from + '-' + to + ' of ' + total.toLocaleString() + ' results</div>';
    }

    // Results
    if (docs.length === 0) {
        html += '<div class="no-results"><h3>No results found</h3><p>Try different keywords or remove some filters.</p></div>';
    }

    docs.forEach(function(doc) {
        var h = hl[doc.id] || {};
        var t = (h.title && h.title[0]) || esc(doc.title || 'Untitled');
        var s = (h.description && h.description[0]) || (h.text && h.text[0]) || esc(doc.description || '');
        var date = doc.creation_date ? new Date(doc.creation_date).toLocaleDateString() : '';
        var domain = doc.meta_domain || '';

        html += '<div class="result">'
             +  '<h3><a href="' + esc(doc.uri) + '" target="_blank">' + t + '</a></h3>'
             +  '<div class="url">' + esc(doc.uri) + '</div>'
             +  '<div class="snippet">' + s + '</div>'
             +  '<div class="meta">' + [domain, date].filter(Boolean).join(' · ') + '</div>'
             +  '</div>';
    });

    // Pagination
    if (total > ROWS) {
        var pages = Math.ceil(total / ROWS);
        var cur = Math.floor(currentStart / ROWS) + 1;
        html += '<div class="pagination">';
        if (cur > 1) html += '<a href="#" onclick="event.preventDefault();doSearch(' + ((cur-2)*ROWS) + ')">&laquo;</a>';
        var pStart = Math.max(1, cur - 3);
        var pEnd = Math.min(pages, cur + 3);
        for (var p = pStart; p <= pEnd; p++) {
            if (p === cur) html += '<span class="current">' + p + '</span>';
            else html += '<a href="#" onclick="event.preventDefault();doSearch(' + ((p-1)*ROWS) + ')">' + p + '</a>';
        }
        if (cur < pages) html += '<a href="#" onclick="event.preventDefault();doSearch(' + (cur*ROWS) + ')">&raquo;</a>';
        html += '</div>';
    }

    resultsDiv.innerHTML = html;

    // Facets
    renderFacets(data);
    window.scrollTo(0, 0);
}

function renderFacets(data) {
    var div = document.getElementById('facets');
    if (!data.facet_counts) { div.innerHTML = ''; return; }
    var langs = data.facet_counts.facet_fields.meta_detected_language || [];
    if (!langs.length) { div.innerHTML = ''; return; }

    var langNames = {en:'English',de:'German',fr:'French',es:'Spanish',it:'Italian',nl:'Dutch',pt:'Portuguese',ro:'Romanian',ja:'Japanese',zh:'Chinese',ko:'Korean',ar:'Arabic',ru:'Russian',sv:'Swedish',pl:'Polish',tr:'Turkish',da:'Danish',fi:'Finnish',no:'Norwegian',cs:'Czech'};
    var html = '<div class="facet-group"><h4>Language</h4><ul>';
    for (var i = 0; i < langs.length; i += 2) {
        var code = langs[i], count = langs[i+1];
        if (!count) continue;
        var isActive = activeFilters.meta_detected_language === code;
        html += '<li' + (isActive ? ' class="active"' : '') + '>'
             +  '<a href="#" onclick="event.preventDefault();toggleFilter(\'meta_detected_language\',\'  ' + code + ' \')">'
             +  '<span>' + (langNames[code] || code.toUpperCase()) + '</span>'
             +  '<span class="facet-count">' + count + '</span>'
             +  '</a></li>';
    }
    html += '</ul></div>';

    if (Object.keys(activeFilters).length) {
        html += '<div class="facet-group"><a href="#" onclick="event.preventDefault();activeFilters={};doSearch()" style="color:#e8650a;font-size:13px">Clear all filters</a></div>';
    }
    div.innerHTML = html;
}

function toggleFilter(field, value) {
    if (activeFilters[field] === value) delete activeFilters[field];
    else activeFilters[field] = value;
    doSearch();
}

// ---- Helpers ----
function solrFetch(url) {
    return fetch(url, { headers: { 'Authorization': 'Basic ' + btoa(SOLR_USER + ':' + SOLR_PASS) } }).then(function(r) { return r.json(); });
}

function buildQuery(params, fqList) {
    var parts = [];
    for (var k in params) {
        if (k === 'fq') continue;
        parts.push(encodeURIComponent(k) + '=' + encodeURIComponent(params[k]));
    }
    fqList.forEach(function(f) { parts.push('fq=' + encodeURIComponent(f)); });
    return parts.join('&');
}

function esc(s) {
    if (!s) return '';
    var d = document.createElement('div'); d.textContent = s; return d.innerHTML;
}
</script>
</body>
</html>

What This Example Includes

  • Search box with Enter key support
  • Autocomplete dropdown with debounced requests (150ms) and keyboard/click interaction
  • eDisMax search with field weighting (title^5, description^4, text^1) and phrase boosting
  • Highlighted results with title, URL, snippet, domain, and date
  • Language facet sidebar with click-to-filter and visual active state
  • "Clear all filters" link when filters are active
  • Spellcheck "Did you mean...?" when few results are found
  • Pagination with page numbers and previous/next arrows
  • Responsive layout — sidebar collapses below on mobile
  • Clean, modern CSS — no external dependencies

To Use It

  1. Replace YOUR_HOST, YOUR_INDEX, and YOUR_API_KEY with your actual Opensolr credentials
  2. Make sure your domain is whitelisted for CORS (contact Opensolr support)
  3. Save as an HTML file and open in your browser

That is it — a complete search engine in a single file.