Code Examples

Opensolr Code Examples — find answers to your questions

Code Examples: Build Your Own Search UI

Your own search page or app can run the same hybrid search as the hosted search page: the meaning vector of what the visitor typed, then one Solr request that ranks by words and meaning together.

01 · The flow

01

Your page sends the search to your server. The API key and the password of the index stay on the server.

02

Your server asks for the meaning vector: GET https://api.opensolr.com/solr_manager/api/embed with email, api_key, index_name, is_query=1 and the text in payload. The answer is a JSON array of 1,024 numbers. The embed endpoint.

03

Your server sends one search to Solr: a POST to the /select URL of your index with the {!hybrid} parser and the vector.

04

Your server answers your page with the JSON of Solr, or only the fields the page shows.

is_query=1 marks the text as a search, the way the hosted search page embeds searches. Each vector counts as an AI request of the index owner, except text that was embedded before: what site search uses from your plan.

02 · When there is no vector

The embed endpoint answers an object instead of an array when it cannot make a vector, for example {"status":false,"msg":"VECTOR_NOT_ALLOWED"} when the plan of the index owner has no AI. Search with words alone then: the same edismax query, without the vector. Every example below does this on its own.

03 · The examples

PHP

search.php: an endpoint that answers JSON.

Node.js

server.mjs: a small server, no packages.

Python

search.py: a function you can run from the command line.

curl

Two commands to try it from a terminal.

Complete search page

The HTML page that shows results, highlights, facets and pages from any of these endpoints.

04 · Shortcuts

The exact request of your own search page, with its Search Tuning: the Query Inspector. The vector and the search in one call: embed_and_search.

Read Full Answer

Complete Search Page: End-to-End Example

One HTML file with a search box, highlighted results, a language facet, Did you mean and pages. It talks only to your own server: the endpoint of the PHP, Node.js or Python example, which holds the keys and runs the hybrid search.

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Search</title>
<style>
body { margin: 0; font-family: system-ui, sans-serif; color: #111111; background: #ffffff; }
.wrap { max-width: 1000px; margin: 0 auto; padding: 24px 16px; }
form { display: flex; gap: 8px; margin-bottom: 12px; }
input[type=search] { flex: 1; min-width: 0; font-size: 17px; padding: 10px 12px; border: 1px solid #b9b3a9; border-radius: 2px; }
button { font: inherit; cursor: pointer; }
form button { font-size: 16px; padding: 10px 18px; border: 0; border-radius: 2px; background: #c05520; color: #ffffff; }
.layout { display: grid; grid-template-columns: 200px minmax(0, 1fr); gap: 32px; }
@media (max-width: 720px) { .layout { grid-template-columns: minmax(0, 1fr); } }
.link { border: 0; background: none; padding: 4px 0; color: #c05520; font-size: 15px; text-align: left; }
.link.active { color: #111111; font-weight: 700; }
#facets .link { display: block; }
.result { padding: 14px 0; border-bottom: 1px solid #d9d4cc; }
.result a { font-size: 18px; color: #111111; }
.result .url { font-size: 14px; color: #4a4540; word-break: break-all; }
.result em { font-style: normal; font-weight: 700; }
.pages { display: flex; gap: 16px; align-items: center; margin-top: 16px; }
</style>
</head>
<body>
<div class="wrap">
  <form id="form"><input type="search" id="q" placeholder="Search" autocomplete="off"><button type="submit">Search</button></form>
  <p id="info"></p>
  <p id="didyoumean" hidden>Did you mean <button type="button" class="link" id="suggestion"></button>?</p>
  <div class="layout">
    <aside id="facets"></aside>
    <main><div id="results"></div><div class="pages" id="pages"></div></main>
  </div>
</div>
<script>
var BACKEND = '/search.php';
var ROWS = 10;
var LANGUAGES = { en: 'English', de: 'German', fr: 'French', es: 'Spanish', it: 'Italian', nl: 'Dutch', pt: 'Portuguese', ro: 'Romanian' };
var state = { q: '', page: 1, lang: '' };

function el(tag, text, cls) {
  var e = document.createElement(tag);
  if (text != null) e.textContent = text;
  if (cls) e.className = cls;
  return e;
}

function run() {
  if (!state.q) return;
  var url = BACKEND + '?' + new URLSearchParams({ q: state.q, page: state.page, lang: state.lang });
  fetch(url)
    .then(function (r) { return r.json(); })
    .then(render)
    .catch(function () { document.getElementById('info').textContent = 'Search is not available right now.'; });
}

function render(data) {
  var results = document.getElementById('results');
  results.replaceChildren();
  if (!data.response) {
    document.getElementById('info').textContent = 'Search is not available right now.';
    return;
  }
  var total = data.response.numFound;
  document.getElementById('info').textContent = total + (total === 1 ? ' result' : ' results');
  var hl = data.highlighting || {};
  data.response.docs.forEach(function (doc) {
    var h = hl[doc.id] || {};
    var box = el('div', null, 'result');
    var a = el('a');
    if (/^https?:\/\//i.test(doc.uri || '')) a.href = doc.uri;
    // Highlighted text is HTML-escaped by Solr (hl.encoder=html): only its <em> tags are markup
    if (h.title && h.title[0]) a.innerHTML = h.title[0]; else a.textContent = doc.title || doc.uri || '';
    var snippet = el('p');
    var s = (h.description && h.description[0]) || (h.text && h.text[0]);
    if (s) snippet.innerHTML = s; else snippet.textContent = doc.description || '';
    box.append(a, el('div', doc.uri || '', 'url'), snippet);
    results.appendChild(box);
  });
  renderDidYouMean(data, total);
  renderFacets(data);
  renderPages(total);
}

function renderDidYouMean(data, total) {
  var box = document.getElementById('didyoumean');
  var c = data.spellcheck && data.spellcheck.collations;
  box.hidden = true;
  if (total < 5 && c && c.length >= 2) {
    document.getElementById('suggestion').textContent = c[1];
    box.hidden = false;
  }
}

function renderFacets(data) {
  var aside = document.getElementById('facets');
  aside.replaceChildren();
  var pairs = (data.facet_counts && data.facet_counts.facet_fields.meta_detected_language) || [];
  if (!pairs.length) return;
  aside.appendChild(el('strong', 'Language'));
  for (var i = 0; i < pairs.length; i += 2) {
    var code = String(pairs[i]);
    var b = el('button', (LANGUAGES[code] || code) + ' (' + pairs[i + 1] + ')', 'link' + (state.lang === code ? ' active' : ''));
    b.type = 'button';
    b.dataset.lang = code;
    aside.appendChild(b);
  }
}

function renderPages(total) {
  var nav = document.getElementById('pages');
  nav.replaceChildren();
  var last = Math.min(100, Math.ceil(total / ROWS));
  if (last < 2) return;
  if (state.page > 1) nav.appendChild(pageButton('Previous', state.page - 1));
  nav.appendChild(el('span', 'Page ' + state.page + ' of ' + last));
  if (state.page < last) nav.appendChild(pageButton('Next', state.page + 1));
}

function pageButton(label, page) {
  var b = el('button', label, 'link');
  b.type = 'button';
  b.dataset.page = page;
  return b;
}

document.getElementById('form').addEventListener('submit', function (e) {
  e.preventDefault();
  state = { q: document.getElementById('q').value.trim(), page: 1, lang: '' };
  run();
});
document.getElementById('facets').addEventListener('click', function (e) {
  var lang = e.target.dataset && e.target.dataset.lang;
  if (!lang) return;
  state.lang = state.lang === lang ? '' : lang;
  state.page = 1;
  run();
});
document.getElementById('pages').addEventListener('click', function (e) {
  var page = e.target.dataset && e.target.dataset.page;
  if (!page) return;
  state.page = parseInt(page, 10);
  run();
  window.scrollTo(0, 0);
});
document.getElementById('suggestion').addEventListener('click', function () {
  var text = this.textContent;
  document.getElementById('q').value = text;
  state = { q: text, page: 1, lang: '' };
  run();
});
</script>
</body>
</html>

01 · To use it

01

Put the PHP endpoint on your server as search.php, next to this file, or set BACKEND to the address of the Node.js or Python one.

02

Open the page and search.

02 · Why it is safe

No API key and no password in the page: the browser only calls your endpoint.

Highlighted text goes in as HTML because the endpoint asks Solr for hl.encoder=html, which escapes everything except the <em> marks. Every other value goes in as text.

Links open only http and https addresses.

Suggestions while typing: autocomplete. No page to build at all: put the hosted search page on your site.

Read Full Answer

Hybrid Search Example in PHP

A single PHP file you put on your server and call as search.php?q=solar+panels&page=1&lang=en. It runs the flow of Build your own search UI: the meaning vector of the search, one {!hybrid} request to Solr, and words alone when there is no vector. It answers the JSON of Solr with highlights (hl.encoder=html), language facets and Did you mean.

Keep the API key and the password of the index on your server. A web page shows its JavaScript to every visitor, and the password of a new index is your API key: the username and password of your index.
<?php
// search.php?q=solar+panels&page=1&lang=en : hybrid search on an Opensolr Index, answers JSON.
// Needs PHP 7.4 or newer with the curl extension. Keep this file on your server only.

const OPENSOLR_EMAIL   = 'YOUR_EMAIL';
const OPENSOLR_API_KEY = 'YOUR_API_KEY';
const INDEX_NAME       = 'YOUR_INDEX';
const INDEX_URL        = 'https://YOUR_INDEX_HOST/solr/YOUR_INDEX/select';
const INDEX_USER       = 'opensolr';
const INDEX_PASSWORD   = 'YOUR_API_KEY';
const ROWS             = 10;
const QF               = 'title^0.2 description^0.05 uri^0.01 text^0.01 text_t^0.01';
const MM               = '2<65% 4<50% 8<40%';

header('Content-Type: application/json');

$q    = mb_substr(trim((string)($_GET['q'] ?? '')), 0, 200);
$page = max(1, min(100, (int)($_GET['page'] ?? 1)));
$lang = (string)($_GET['lang'] ?? '');
if ($q === '') {
    http_response_code(400);
    echo json_encode(['error' => 'Missing q']);
    exit;
}
if (!preg_match('/^[a-z]{2,3}(-[a-z0-9]{2,8})?$/i', $lang)) {
    $lang = '';
}

// Step 1: the meaning vector of the search, or null when there is none
function embed_query(string $text): ?array
{
    if (mb_strlen($text) < 2) {
        return null;
    }
    $url = 'https://api.opensolr.com/solr_manager/api/embed?' . http_build_query([
        'email'      => OPENSOLR_EMAIL,
        'api_key'    => OPENSOLR_API_KEY,
        'index_name' => INDEX_NAME,
        'is_query'   => 1,
        'payload'    => $text,
    ]);
    $ch = curl_init($url);
    curl_setopt_array($ch, [CURLOPT_RETURNTRANSFER => true, CURLOPT_TIMEOUT => 5]);
    $body = curl_exec($ch);
    $code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
    if ($body === false || $code !== 200) {
        return null;
    }
    $vector = json_decode($body, true);
    if (!is_array($vector) || count($vector) !== 1024 || !isset($vector[0]) || !is_numeric($vector[0])) {
        return null;
    }
    return array_map('floatval', $vector);
}

// Step 2: one Solr request, hybrid when there is a vector, words only when there is not
$lexical = '{!edismax qf="' . QF . '" mm="' . MM . '" v=$uq}';
$params = [
    ['uq', $q],
    ['fl', 'id,uri,title,description,og_image,creation_date,score'],
    ['rows', ROWS],
    ['start', ($page - 1) * ROWS],
    ['fq', 'content_type:text*'],
    ['hl', 'true'],
    ['hl.method', 'unified'],
    ['hl.encoder', 'html'],
    ['hl.q', '{!edismax qf="title description uri text" mm=0 v=$uq}'],
    ['hl.fl', 'title,description,text'],
    ['hl.fragsize', 300],
    ['hl.snippets', 1],
    ['hl.defaultSummary', 'true'],
    ['f.title.hl.fragsize', 0],
    ['facet', 'true'],
    ['facet.field', '{!ex=lang}meta_detected_language'],
    ['facet.mincount', 1],
    ['spellcheck', 'true'],
    ['spellcheck.q', $q],
    ['spellcheck.count', 5],
    ['spellcheck.collate', 'true'],
    ['spellcheck.maxCollations', 3],
    ['spellcheck.maxCollationTries', 15],
    ['echoParams', 'none'],
    ['wt', 'json'],
];
if ($lang !== '') {
    $params[] = ['fq', '{!tag=lang}{!term f=meta_detected_language}' . $lang];
}
$vector = embed_query($q);
if ($vector !== null) {
    $params[] = ['q', '{!hybrid lexical=$lexicalQuery vector=$vectorQuery mode=union alpha=0.85 topN=500}'];
    $params[] = ['lexicalQuery', $lexical];
    $params[] = ['vectorQuery', '{!knn f=embeddings topK=500}[' . implode(',', $vector) . ']'];
} else {
    $params[] = ['q', $lexical];
}

// POST, because the vector makes the request too long for a URL
$body = implode('&', array_map(function ($p) {
    return rawurlencode($p[0]) . '=' . rawurlencode((string)$p[1]);
}, $params));
$ch = curl_init(INDEX_URL);
curl_setopt_array($ch, [
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_POST           => true,
    CURLOPT_POSTFIELDS     => $body,
    CURLOPT_HTTPHEADER     => ['Content-Type: application/x-www-form-urlencoded'],
    CURLOPT_USERPWD        => INDEX_USER . ':' . INDEX_PASSWORD,
    CURLOPT_TIMEOUT        => 10,
]);
$result = curl_exec($ch);
$code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
if ($result === false || $code !== 200) {
    http_response_code(502);
    echo json_encode(['error' => 'Search failed']);
    exit;
}
echo $result;
ReplaceWith
YOUR_EMAILThe email of your Opensolr account.
YOUR_API_KEYYour API key (Account › API Keys). The examples also use it as the password of the index, which is what a new index starts with; if you changed the username or password of the index, put yours: the username and password of your index.
YOUR_INDEXThe name of your Opensolr Index.
YOUR_INDEX_HOSTThe host shown in the Control Panel of the index.

The field weights, mm and alpha are the defaults; the Query Inspector of your hosted search page shows the values of your own index. The complete search page shows the results in a browser.

Read Full Answer

Loading more articles...