Hybrid Search Example in PHP

Code Examples

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.