WEB CRAWLER-Code Examples

Find answers to your questions quickly and easily

Code Examples: Build Your Own Search UI

Code Examples: Build Your Own Search UI

Complete, copy-paste-ready code examples for querying your Opensolr Web Crawler index with full hybrid search (keyword + AI vector). Every example below includes the exact same parameters used by the built-in Opensolr Search UI.


How Hybrid Search Works (The Flow)

Every search request follows this flow:

  1. User types a query (e.g., "warm beverage for cold weather")
  2. Your app calls the Opensolr Embed API to convert the query text into a 1024-dimensional vector embedding
  3. Your app sends the hybrid query to Solr — combining the vector embedding (semantic search) with the keyword query (lexical search)
  4. Solr returns results ranked by a combined score of both vector similarity and keyword relevance

The embedding API endpoint is:

https://api.opensolr.com/solr_manager/api/embed?payload=your+search+query

It returns:

{
  "embedding": [0.0234, -0.0567, 0.1234, ... ]   // 1024 floats
}

This vector is then passed into the vectorQuery parameter of your Solr request.


The Complete Hybrid Search Parameters

These are ALL the parameters used by the Opensolr Search UI. You can see them yourself by clicking the Solr Query Inspector (magnifying glass icon, bottom-right corner) on your search page at https://search.opensolr.com/YOUR_INDEX.

Core Query

q = {!func}sum(product(1, query($vectorQuery)), product(1, div(log(sum(1, query($lexicalQuery))), sum(log(sum(1, query($lexicalQuery))), 20))))

vectorQuery = {!knn f=embeddings topK=250}[1024-dim embedding vector here]

lexicalQuery = {!edismax
  qf="title^5 description^4 uri^0.5 text^0.01"
  pf="title^10 description^8 uri^1 text^0.02"
  pf2="title^5 description^4 uri^0.5 text^0.01"
  pf3="title^2.5 description^2 uri^0.25 text^0.005"
  ps=0 ps2=1 ps3=2
  mm="2<-1 5<-2"
}your search terms here

df = title
rows = 50
start = 0

Filter Queries

fq = content_type:text*
fq = -uri_s:"https://yoursite.com/sitemap.xml"

Field List

fl = id,uri,title,description,text,og_image,meta_icon,content_type,creation_date,timestamp,meta_domain,meta_*,score,price_f,currency_s

Highlighting (all 15 parameters)

hl = true
hl.q = {!edismax qf="title^5 description^4 uri^0.5 text^0.01"}your search terms here
hl.fl = uri,title,description,text
hl.method = unified
hl.fragsize = 200
hl.snippets = 1
hl.bs.type = SENTENCE
hl.defaultSummary = false
hl.fragAlignRatio = 0.33
hl.tag.pre = <em>
hl.tag.post = </em>
hl.tag.ellipsis = ...
hl.requireFieldMatch = false
hl.highlightMultiTerm = true
hl.usePhraseHighlighter = true
hl.maxAnalyzedChars = 10000

Faceting

facet = true
facet.field = meta_detected_language
facet.field = currency_s
facet.mincount = 1
facet.sort = index

Stats

stats = true
stats.field = price_f

Spellcheck

spellcheck = true
spellcheck.q = your search terms here
spellcheck.onlyMorePopular = false
spellcheck.extendedResults = false
spellcheck.count = 5
spellcheck.collate = true
spellcheck.collateExtendedResults = false
spellcheck.maxCollationTries = 15
spellcheck.maxCollations = 3

CORS: Client-Side Only (No Server Needed)

If your domain is CORS-whitelisted by Opensolr, your JavaScript running in the browser can talk directly to both the Embed API and the Solr API — no backend server needed at all.

What is CORS? By default, browsers block web pages from making requests to a different domain (security feature). CORS lets the Opensolr server explicitly allow your domain. Contact Opensolr support to get your domain whitelisted.


JS Example 1: Pure JavaScript — Full Hybrid Search

This is the real deal. A single HTML file with full hybrid vector + keyword search, all parameters included.

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>My Hybrid Search</title>
    <style>
        body { font-family: -apple-system, BlinkMacSystemFont, sans-serif; max-width: 800px; margin: 40px auto; padding: 0 20px; }
        .search-box { display: flex; gap: 8px; margin-bottom: 24px; }
        .search-box input { flex: 1; padding: 12px 16px; font-size: 16px; border: 2px solid #ddd; border-radius: 6px; }
        .search-box input:focus { border-color: #e8650a; outline: none; }
        .search-box button { padding: 12px 28px; font-size: 16px; background: #e8650a; color: white; border: none; border-radius: 6px; cursor: pointer; font-weight: 600; }
        .result { margin-bottom: 20px; padding-bottom: 20px; border-bottom: 1px solid #eee; }
        .result h3 { margin: 0 0 4px; } .result h3 a { color: #1a0dab; text-decoration: none; }
        .result .url { color: #006621; font-size: 13px; margin-bottom: 4px; }
        .result .snippet { color: #545454; font-size: 14px; line-height: 1.5; }
        .result em { font-weight: bold; font-style: normal; }
        .info { color: #666; font-size: 14px; margin-bottom: 16px; }
        .spellcheck a { color: #e8650a; font-weight: 600; text-decoration: none; }
    </style>
</head>
<body>
    <h1>Search</h1>
    <div class="search-box">
        <input type="text" id="q" placeholder="Search..." autofocus>
        <button onclick="search()">Search</button>
    </div>
    <div id="results"></div>

<script>
// =====================================================================
// CONFIGURATION — Replace these with YOUR Opensolr index details
// =====================================================================
var SOLR_URL  = "https://YOUR_HOST/solr/YOUR_INDEX/select";
var SOLR_USER = "opensolr";
var SOLR_PASS = "YOUR_API_KEY";
var EMBED_URL = "https://api.opensolr.com/solr_manager/api/embed";

document.getElementById("q").addEventListener("keydown", function(e) {
    if (e.key === "Enter") search();
});

function search(startOffset) {
    var query = document.getElementById("q").value.trim();
    if (!query) return;
    var start = startOffset || 0;

    document.getElementById("results").innerHTML = "<p class="info">Searching...</p>";

    // Step 1: Get the embedding vector for the search query
    fetch(EMBED_URL + "?payload=" + encodeURIComponent(query))
    .then(function(r) { return r.json(); })
    .then(function(embedData) {
        var vector = embedData.embedding;
        if (!vector || !vector.length) {
            // Fallback to keyword-only if embedding fails
            return keywordOnlySearch(query, start);
        }

        var vectorStr = "[" + vector.join(",") + "]";

        // Step 2: Build the FULL hybrid search query with ALL parameters
        var params = new URLSearchParams();

        // --- Core hybrid query ---
        params.set("q",
            "{!func}sum(" +
                "product(1, query($vectorQuery)), " +
                "product(1, div(log(sum(1, query($lexicalQuery))), sum(log(sum(1, query($lexicalQuery))), 20)))" +
            ")"
        );
        params.set("vectorQuery", "{!knn f=embeddings topK=250}" + vectorStr);
        params.set("lexicalQuery",
            "{!edismax " +
                "qf="title^5 description^4 uri^0.5 text^0.01" " +
                "pf="title^10 description^8 uri^1 text^0.02" " +
                "pf2="title^5 description^4 uri^0.5 text^0.01" " +
                "pf3="title^2.5 description^2 uri^0.25 text^0.005" " +
                "ps=0 ps2=1 ps3=2 " +
                "mm="2<-1 5<-2"" +
            "}" + query
        );
        params.set("df", "title");
        params.set("rows", "10");
        params.set("start", start);
        params.set("wt", "json");

        // --- Filter queries ---
        params.append("fq", "content_type:text*");

        // --- Field list ---
        params.set("fl", "id,uri,title,description,text,og_image,meta_icon,content_type,creation_date,timestamp,meta_domain,meta_*,score,price_f,currency_s");

        // --- Highlighting (all 15 params) ---
        params.set("hl", "true");
        params.set("hl.q", "{!edismax qf="title^5 description^4 uri^0.5 text^0.01"}" + query);
        params.set("hl.fl", "uri,title,description,text");
        params.set("hl.method", "unified");
        params.set("hl.fragsize", "200");
        params.set("hl.snippets", "1");
        params.set("hl.bs.type", "SENTENCE");
        params.set("hl.defaultSummary", "false");
        params.set("hl.fragAlignRatio", "0.33");
        params.set("hl.tag.pre", "<em>");
        params.set("hl.tag.post", "</em>");
        params.set("hl.tag.ellipsis", "... ");
        params.set("hl.requireFieldMatch", "false");
        params.set("hl.highlightMultiTerm", "true");
        params.set("hl.usePhraseHighlighter", "true");
        params.set("hl.maxAnalyzedChars", "10000");

        // --- Faceting ---
        params.set("facet", "true");
        params.append("facet.field", "meta_detected_language");
        params.append("facet.field", "currency_s");
        params.set("facet.mincount", "1");
        params.set("facet.sort", "index");

        // --- Stats ---
        params.set("stats", "true");
        params.set("stats.field", "price_f");

        // --- Spellcheck ---
        params.set("spellcheck", "true");
        params.set("spellcheck.q", query);
        params.set("spellcheck.onlyMorePopular", "false");
        params.set("spellcheck.extendedResults", "false");
        params.set("spellcheck.count", "5");
        params.set("spellcheck.collate", "true");
        params.set("spellcheck.collateExtendedResults", "false");
        params.set("spellcheck.maxCollationTries", "15");
        params.set("spellcheck.maxCollations", "3");

        // Step 3: Send to Solr
        return fetch(SOLR_URL + "?" + params.toString(), {
            headers: { "Authorization": "Basic " + btoa(SOLR_USER + ":" + SOLR_PASS) }
        })
        .then(function(r) { return r.json(); })
        .then(function(data) { renderResults(data, query); });
    })
    .catch(function(err) {
        document.getElementById("results").innerHTML = "<p>Error: " + err.message + "</p>";
    });
}

function renderResults(data, query) {
    var docs = data.response.docs;
    var hl = data.highlighting || {};
    var total = data.response.numFound;
    var html = "<p class="info">Found " + total.toLocaleString() + " results for <strong>" + esc(query) + "</strong></p>";

    docs.forEach(function(doc) {
        var h = hl[doc.id] || {};
        var title = (h.title && h.title[0]) || esc(doc.title || "Untitled");
        var snippet = (h.description && h.description[0]) || (h.text && h.text[0]) || esc(doc.description || "");

        html += "<div class="result">"
             +  "<h3><a href="" + esc(doc.uri) + "" target="_blank">" + title + "</a></h3>"
             +  "<div class="url">" + esc(doc.uri) + "</div>"
             +  "<div class="snippet">" + snippet + "</div>"
             +  "</div>";
    });

    document.getElementById("results").innerHTML = html;
}

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

This is a real, working hybrid search. The JavaScript calls the Embed API to get the vector, then sends the full hybrid query to Solr with all 40+ parameters. Semantic search works out of the box.


PHP Example 2: PHP — Full Hybrid Search

Complete PHP backend with all hybrid search parameters. Save as search.php and call it with search.php?q=your+query.

<?php
// search.php — Full Hybrid Search with Opensolr Web Crawler Index

// =====================================================================
// CONFIGURATION — Replace these with YOUR Opensolr index details
// =====================================================================
$solr_url  = "https://YOUR_HOST/solr/YOUR_INDEX/select";
$solr_user = "opensolr";
$solr_pass = "YOUR_API_KEY";
$embed_url = "https://api.opensolr.com/solr_manager/api/embed";

$query = trim($_GET["q"] ?? "");
$page  = max(1, intval($_GET["page"] ?? 1));
$rows  = 10;
$start = ($page - 1) * $rows;

if (empty($query)) {
    header("Content-Type: application/json");
    echo json_encode(["error" => "No query provided"]);
    exit;
}

// Step 1: Get the embedding vector
$embed_response = file_get_contents($embed_url . "?payload=" . urlencode($query));
$embed_data = json_decode($embed_response, true);
$vector = $embed_data["embedding"] ?? null;

// Step 2: Build the FULL hybrid query with ALL parameters
$params = [];

if ($vector && is_array($vector) && count($vector) === 1024) {
    $vector_str = "[" . implode(",", $vector) . "]";

    // Core hybrid scoring formula
    $params["q"] = "{!func}sum("
        . "product(1, query($vectorQuery)), "
        . "product(1, div(log(sum(1, query($lexicalQuery))), sum(log(sum(1, query($lexicalQuery))), 20)))"
        . ")";
    $params["vectorQuery"] = "{!knn f=embeddings topK=250}" . $vector_str;
    $params["lexicalQuery"] = "{!edismax"
        . " qf="title^5 description^4 uri^0.5 text^0.01""
        . " pf="title^10 description^8 uri^1 text^0.02""
        . " pf2="title^5 description^4 uri^0.5 text^0.01""
        . " pf3="title^2.5 description^2 uri^0.25 text^0.005""
        . " ps=0 ps2=1 ps3=2"
        . " mm="2<-1 5<-2""
        . "}" . $query;

    // Highlighting uses the lexical query only (not the vector)
    $params["hl.q"] = "{!edismax qf="title^5 description^4 uri^0.5 text^0.01"}" . $query;
} else {
    // Fallback: keyword-only if embedding fails
    $params["q"] = $query;
    $params["defType"] = "edismax";
    $params["qf"] = "title^5 description^4 uri^0.5 text^0.01";
    $params["pf"] = "title^10 description^8 uri^1 text^0.02";
    $params["mm"] = "2<-1 5<-2";
    $params["hl.q"] = $query;
}

// Common parameters (same for hybrid and fallback)
$params["df"] = "title";
$params["rows"] = $rows;
$params["start"] = $start;
$params["wt"] = "json";

// Filter queries
$params["fq"] = "content_type:text*";

// Field list
$params["fl"] = "id,uri,title,description,text,og_image,meta_icon,content_type,creation_date,timestamp,meta_domain,meta_*,score,price_f,currency_s";

// Highlighting — all 15 parameters
$params["hl"]                       = "true";
$params["hl.fl"]                    = "uri,title,description,text";
$params["hl.method"]                = "unified";
$params["hl.fragsize"]              = 200;
$params["hl.snippets"]              = 1;
$params["hl.bs.type"]               = "SENTENCE";
$params["hl.defaultSummary"]        = "false";
$params["hl.fragAlignRatio"]        = "0.33";
$params["hl.tag.pre"]               = "<em>";
$params["hl.tag.post"]              = "</em>";
$params["hl.tag.ellipsis"]          = "... ";
$params["hl.requireFieldMatch"]     = "false";
$params["hl.highlightMultiTerm"]    = "true";
$params["hl.usePhraseHighlighter"]  = "true";
$params["hl.maxAnalyzedChars"]      = 10000;

// Faceting
$params["facet"]           = "true";
$params["facet.mincount"]  = 1;
$params["facet.sort"]      = "index";

// Stats
$params["stats"]       = "true";
$params["stats.field"] = "price_f";

// Spellcheck
$params["spellcheck"]                         = "true";
$params["spellcheck.q"]                       = $query;
$params["spellcheck.onlyMorePopular"]         = "false";
$params["spellcheck.extendedResults"]         = "false";
$params["spellcheck.count"]                   = 5;
$params["spellcheck.collate"]                 = "true";
$params["spellcheck.collateExtendedResults"]  = "false";
$params["spellcheck.maxCollationTries"]       = 15;
$params["spellcheck.maxCollations"]           = 3;

// Build URL — http_build_query + manually append multi-value params
$url = $solr_url . "?" . http_build_query($params)
     . "&facet.field=meta_detected_language"
     . "&facet.field=currency_s";

// Step 3: Query Solr
$ch = curl_init($url);
curl_setopt($ch, CURLOPT_USERPWD, $solr_user . ":" . $solr_pass);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_TIMEOUT, 10);
$response = curl_exec($ch);
$http_code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);

header("Content-Type: application/json");
echo $response;

N.js Example 3: Node.js (Express) — Full Hybrid Search

// server.js — Node.js Full Hybrid Search with Opensolr
const express = require("express");
const app = express();

// =====================================================================
// CONFIGURATION — Replace these with YOUR Opensolr index details
// =====================================================================
const SOLR_URL  = "https://YOUR_HOST/solr/YOUR_INDEX/select";
const SOLR_AUTH = "Basic " + Buffer.from("opensolr:YOUR_API_KEY").toString("base64");
const EMBED_URL = "https://api.opensolr.com/solr_manager/api/embed";

app.get("/search", async (req, res) => {
    const query = (req.query.q || "").trim();
    const start = parseInt(req.query.start) || 0;
    const rows  = parseInt(req.query.rows) || 10;

    if (!query) return res.json({ error: "No query provided" });

    try {
        // Step 1: Get the embedding vector
        const embedResp = await fetch(EMBED_URL + "?payload=" + encodeURIComponent(query));
        const embedData = await embedResp.json();
        const vector = embedData.embedding;

        // Step 2: Build ALL hybrid search parameters
        const params = new URLSearchParams();

        if (vector && vector.length === 1024) {
            const vectorStr = "[" + vector.join(",") + "]";

            params.set("q",
                "{!func}sum(" +
                    "product(1, query($vectorQuery)), " +
                    "product(1, div(log(sum(1, query($lexicalQuery))), " +
                    "sum(log(sum(1, query($lexicalQuery))), 20))))"
            );
            params.set("vectorQuery", "{!knn f=embeddings topK=250}" + vectorStr);
            params.set("lexicalQuery",
                "{!edismax " +
                    "qf="title^5 description^4 uri^0.5 text^0.01" " +
                    "pf="title^10 description^8 uri^1 text^0.02" " +
                    "pf2="title^5 description^4 uri^0.5 text^0.01" " +
                    "pf3="title^2.5 description^2 uri^0.25 text^0.005" " +
                    "ps=0 ps2=1 ps3=2 " +
                    "mm="2<-1 5<-2"" +
                "}" + query
            );
            params.set("hl.q", "{!edismax qf="title^5 description^4 uri^0.5 text^0.01"}" + query);
        } else {
            // Fallback: keyword-only
            params.set("q", query);
            params.set("defType", "edismax");
            params.set("qf", "title^5 description^4 uri^0.5 text^0.01");
            params.set("pf", "title^10 description^8 uri^1 text^0.02");
            params.set("mm", "2<-1 5<-2");
            params.set("hl.q", query);
        }

        // Common params
        params.set("df", "title");
        params.set("rows", rows);
        params.set("start", start);
        params.set("wt", "json");
        params.append("fq", "content_type:text*");
        params.set("fl", "id,uri,title,description,text,og_image,meta_icon,content_type,creation_date,timestamp,meta_domain,meta_*,score,price_f,currency_s");

        // Highlighting — all 15 parameters
        params.set("hl", "true");
        params.set("hl.fl", "uri,title,description,text");
        params.set("hl.method", "unified");
        params.set("hl.fragsize", "200");
        params.set("hl.snippets", "1");
        params.set("hl.bs.type", "SENTENCE");
        params.set("hl.defaultSummary", "false");
        params.set("hl.fragAlignRatio", "0.33");
        params.set("hl.tag.pre", "<em>");
        params.set("hl.tag.post", "</em>");
        params.set("hl.tag.ellipsis", "... ");
        params.set("hl.requireFieldMatch", "false");
        params.set("hl.highlightMultiTerm", "true");
        params.set("hl.usePhraseHighlighter", "true");
        params.set("hl.maxAnalyzedChars", "10000");

        // Faceting
        params.set("facet", "true");
        params.append("facet.field", "meta_detected_language");
        params.append("facet.field", "currency_s");
        params.set("facet.mincount", "1");
        params.set("facet.sort", "index");

        // Stats
        params.set("stats", "true");
        params.set("stats.field", "price_f");

        // Spellcheck
        params.set("spellcheck", "true");
        params.set("spellcheck.q", query);
        params.set("spellcheck.onlyMorePopular", "false");
        params.set("spellcheck.extendedResults", "false");
        params.set("spellcheck.count", "5");
        params.set("spellcheck.collate", "true");
        params.set("spellcheck.collateExtendedResults", "false");
        params.set("spellcheck.maxCollationTries", "15");
        params.set("spellcheck.maxCollations", "3");

        // Step 3: Query Solr
        const solrResp = await fetch(SOLR_URL + "?" + params.toString(), {
            headers: { "Authorization": SOLR_AUTH }
        });
        const data = await solrResp.json();
        res.json(data);
    } catch (err) {
        res.status(500).json({ error: err.message });
    }
});

app.listen(3000, () => console.log("Hybrid search server running on :3000"));

Py Example 4: Python — Full Hybrid Search

# search.py — Python Full Hybrid Search with Opensolr
import requests
import json
from urllib.parse import urlencode

# =====================================================================
# CONFIGURATION — Replace these with YOUR Opensolr index details
# =====================================================================
SOLR_URL  = "https://YOUR_HOST/solr/YOUR_INDEX/select"
SOLR_USER = "opensolr"
SOLR_PASS = "YOUR_API_KEY"
EMBED_URL = "https://api.opensolr.com/solr_manager/api/embed"

def hybrid_search(query, start=0, rows=10):
    # Step 1: Get the embedding vector
    embed_resp = requests.get(EMBED_URL, params={"payload": query})
    embed_data = embed_resp.json()
    vector = embed_data.get("embedding")

    # Step 2: Build ALL hybrid search parameters
    params = []

    if vector and len(vector) == 1024:
        vector_str = "[" + ",".join(str(v) for v in vector) + "]"

        params.append(("q",
            "{!func}sum("
            "product(1, query($vectorQuery)), "
            "product(1, div(log(sum(1, query($lexicalQuery))), "
            "sum(log(sum(1, query($lexicalQuery))), 20))))"
        ))
        params.append(("vectorQuery", "{!knn f=embeddings topK=250}" + vector_str))
        params.append(("lexicalQuery",
            "{!edismax "
            "qf="title^5 description^4 uri^0.5 text^0.01" "
            "pf="title^10 description^8 uri^1 text^0.02" "
            "pf2="title^5 description^4 uri^0.5 text^0.01" "
            "pf3="title^2.5 description^2 uri^0.25 text^0.005" "
            "ps=0 ps2=1 ps3=2 "
            "mm="2<-1 5<-2""
            "}" + query
        ))
        params.append(("hl.q", "{!edismax qf="title^5 description^4 uri^0.5 text^0.01"}" + query))
    else:
        # Fallback: keyword-only
        params.append(("q", query))
        params.append(("defType", "edismax"))
        params.append(("qf", "title^5 description^4 uri^0.5 text^0.01"))
        params.append(("pf", "title^10 description^8 uri^1 text^0.02"))
        params.append(("mm", "2<-1 5<-2"))
        params.append(("hl.q", query))

    # Common params
    params += [
        ("df", "title"), ("rows", rows), ("start", start), ("wt", "json"),
        ("fq", "content_type:text*"),
        ("fl", "id,uri,title,description,text,og_image,meta_icon,content_type,creation_date,timestamp,meta_domain,meta_*,score,price_f,currency_s"),
        # Highlighting
        ("hl", "true"), ("hl.fl", "uri,title,description,text"), ("hl.method", "unified"),
        ("hl.fragsize", 200), ("hl.snippets", 1), ("hl.bs.type", "SENTENCE"),
        ("hl.defaultSummary", "false"), ("hl.fragAlignRatio", "0.33"),
        ("hl.tag.pre", "<em>"), ("hl.tag.post", "</em>"), ("hl.tag.ellipsis", "... "),
        ("hl.requireFieldMatch", "false"), ("hl.highlightMultiTerm", "true"),
        ("hl.usePhraseHighlighter", "true"), ("hl.maxAnalyzedChars", 10000),
        # Faceting
        ("facet", "true"), ("facet.field", "meta_detected_language"),
        ("facet.field", "currency_s"), ("facet.mincount", 1), ("facet.sort", "index"),
        # Stats
        ("stats", "true"), ("stats.field", "price_f"),
        # Spellcheck
        ("spellcheck", "true"), ("spellcheck.q", query),
        ("spellcheck.onlyMorePopular", "false"), ("spellcheck.extendedResults", "false"),
        ("spellcheck.count", 5), ("spellcheck.collate", "true"),
        ("spellcheck.collateExtendedResults", "false"),
        ("spellcheck.maxCollationTries", 15), ("spellcheck.maxCollations", 3),
    ]

    # Step 3: Query Solr
    resp = requests.get(SOLR_URL, params=params, auth=(SOLR_USER, SOLR_PASS), timeout=10)
    return resp.json()

# Usage
if __name__ == "__main__":
    import sys
    query = " ".join(sys.argv[1:]) or "test"
    data = hybrid_search(query)
    print(f"Found {data["response"]["numFound"]} results")
    for doc in data["response"]["docs"][:5]:
        print(f"  {doc.get("score", 0):.4f}  {doc.get("title", "N/A")}")
        print(f"         {doc.get("uri", "")}")

$_ Example 5: curl — Full Hybrid Search (Command Line)

For quick testing. First get the embedding, then search:

# Step 1: Get the embedding vector and save to file
curl -s "https://api.opensolr.com/solr_manager/api/embed?payload=warm+beverage+for+cold+weather" \
  | python3 -c "import sys,json; print(json.dumps(json.load(sys.stdin)["embedding"]))" > /tmp/vector.json

# Step 2: Build and execute the hybrid search
VECTOR=$(cat /tmp/vector.json)

curl -s -u "opensolr:YOUR_API_KEY" \
  --data-urlencode "q={!func}sum(product(1, query($vectorQuery)), product(1, div(log(sum(1, query($lexicalQuery))), sum(log(sum(1, query($lexicalQuery))), 20))))" \
  --data-urlencode "vectorQuery={!knn f=embeddings topK=250}${VECTOR}" \
  --data-urlencode "lexicalQuery={!edismax qf="title^5 description^4 uri^0.5 text^0.01" pf="title^10 description^8 uri^1 text^0.02" pf2="title^5 description^4 uri^0.5 text^0.01" pf3="title^2.5 description^2 uri^0.25 text^0.005" ps=0 ps2=1 ps3=2 mm="2<-1 5<-2"}warm beverage for cold weather" \
  --data-urlencode "df=title" \
  --data-urlencode "rows=5" \
  --data-urlencode "wt=json" \
  --data-urlencode "fq=content_type:text*" \
  --data-urlencode "fl=id,uri,title,description,score" \
  --data-urlencode "hl=true" \
  --data-urlencode "hl.fl=title,description,text" \
  --data-urlencode "hl.method=unified" \
  -G "https://YOUR_HOST/solr/YOUR_INDEX/select" \
  | python3 -m json.tool

Quick Reference: What to Replace

In every example above, replace these placeholders:

Placeholder Where to Find It
YOUR_HOST Your Index Control Panel → "Hostname"
YOUR_INDEX Your Index name
YOUR_API_KEY Control Panel → Dashboard → "Secret API Key"

The Embed API endpoint (api.opensolr.com/solr_manager/api/embed) is the same for everyone — no configuration needed.

Read Full Answer

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.

Read Full Answer