<?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;