_host = $host; $this->_port = $port; $this->_index = $index; $this->_sphinx = new SphinxClient(); $this->_sphinx->SetServer($this->_host, $this->_port); $this->_sphinx->SetMatchMode(SPH_MATCH_EXTENDED2); } public function setSnippetSize(int $size): void { if ($size > 0) $this->_snippetSize = $size; } public function setIndexTags(string $tags): void { /* Handled by DokuWiki Parser */ } public function search(int $start, int $resultsPerPage = 10): bool { $this->_resultsPerPage = $resultsPerPage; $this->_sphinx->SetFieldWeights([ 'namespace' => $this->_namespacePriority, 'pagename' => $this->_pagenamePriority, 'title' => $this->_titlePriority, 'body' => $this->_bodyPriority ]); $this->_sphinx->SetLimits($start, $resultsPerPage + 50); $res = $this->_sphinx->Query($this->_query, $this->_index); $this->_result = $res ?: []; return !empty($this->_result['matches']); } public function getPages(string $keywords): array|false { if (empty($this->_result['matches'])) return false; $ids = $this->getPagesIds(); $results = []; $cleanK = $this->removeStars($keywords); foreach ($ids as $id => $data) { $this->_offset++; if (auth_quickaclcheck($data['page']) >= AUTH_READ) { $results[$id] = [ 'page' => $data['page'], 'bodyExcerpt' => $this->_generateDokuWikiSnippet($data['page'], $cleanK), 'titleTextExcerpt' => !empty($data['title_text']) ? $data['title_text'] : $data['page'], 'hid' => $data['hid'] ?? '', ]; if (count($results) >= $this->_resultsPerPage) break; } } return $results; } /** * Mirrors DokuWiki blocks by slicing instructions and re-rendering them. * This preserves blue tabs, yellow borders, and syntax colors. */ private function _generateDokuWikiSnippet(string $pageId, string $query): string { $rawText = rawWiki($pageId); if (empty($rawText)) return ''; $instructions = p_get_instructions($rawText); $words = preg_split('/\s+/', $query, -1, PREG_SPLIT_NO_EMPTY); $matchInstruction = null; // 1. Find the instruction block containing keywords foreach ($instructions as $instr) { $type = $instr[0]; $content = ''; if ($type === 'code' || $type === 'file') $content = $instr[1][0]; elseif ($type === 'cdata') $content = $instr[1][0]; foreach ($words as $word) { if (mb_stripos($content, $word) !== false) { $matchInstruction = $instr; break 2; } } } if (!$matchInstruction) { // Fallback for simple text matches $clean = strip_tags(p_render('xhtml', $instructions, $info)); return '
... ' . hsc(mb_substr($clean, 0, 250)) . ' ...
'; } $type = $matchInstruction[0]; $snippetWiki = ''; // 2. Line-based slicing for Code/File blocks if ($type === 'code' || $type === 'file') { $codeText = $matchInstruction[1][0]; $lang = $matchInstruction[1][1]; $file = ($type === 'file') ? $matchInstruction[1][2] : ''; $lines = explode("\n", $codeText); $matchIdx = 0; foreach ($lines as $idx => $line) { foreach ($words as $word) { if (mb_stripos($line, $word) !== false) { $matchIdx = $idx; break 2; } } } // Slice window: 3 lines before, 10 lines after match $start = max(0, $matchIdx - 3); $slice = array_slice($lines, $start, 15); $finalCode = ($start > 0 ? "... \n" : "") . implode("\n", $slice) . ($start + 15 < count($lines) ? "\n ..." : ""); if ($type === 'file') $snippetWiki = "\n$finalCode\n";
}
// 3. Media Preservation
elseif (in_array($type, ['internalmedia', 'externalmedia'])) {
// If it's a video/image, we render the whole instruction
$snippetWiki = p_render('xhtml', [$matchInstruction], $info);
return $snippetWiki;
}
else {
$snippetWiki = $matchInstruction[1][0];
}
// 4. Re-render the slice using DokuWiki engine
$rendered = p_render('xhtml', p_get_instructions($snippetWiki), $info);
// Safe Highlight
foreach ($words as $word) {
if (mb_strlen($word) < 2) continue;
$q = preg_quote(hsc($word), '/');
$rendered = preg_replace("/(?![^<]*>)$q/iu", '$0', $rendered);
}
return $rendered;
}
public function getExcerpt(array $data, string $query): array {
$words = preg_split('/\s+/', $this->removeStars($query), -1, PREG_SPLIT_NO_EMPTY);
$res = [];
foreach ($data as $text) {
$out = hsc($text);
foreach ($words as $word) {
if (mb_strlen($word) < 2) continue;
$q = preg_quote(hsc($word), '/');
$out = preg_replace("/($q)/iu", '$1', $out);
}
$res[] = $out;
}
return $res;
}
public function getPagesIds(): array { return (new PageMapper())->getByCrc(array_keys($this->_result['matches'] ?? [])); }
public function removeStars(string $query): string { return trim(str_replace('*', '', $query)); }
public function starQuery(string $query): string {
$words = preg_split('/\s+/', $this->removeStars($query), -1, PREG_SPLIT_NO_EMPTY);
$starred = [];
foreach ($words as $w) $starred[] = (str_starts_with($w, '-') || mb_strlen($w) < 3 || str_contains($w, '"')) ? $w : "*$w*";
return implode(" ", $starred);
}
public function getOffset(): int { return $this->_offset; }
public function getError(): string { return $this->_sphinx->GetLastError(); }
public function getTotalFound(): int { return (int)($this->_result['total_found'] ?? 0); }
public function setNamespacePriority(int $p): void { $this->_namespacePriority = $p; }
public function setPagenamePriority(int $p): void { $this->_pagenamePriority = $p; }
public function setTitlePriority(int $p): void { $this->_titlePriority = $p; }
public function setBodyPriority(int $p): void { $this->_bodyPriority = $p; }
public function setSearchAllQuery(string $k, string $c): void {
$esc = $this->_sphinx->EscapeString($k);
$this->_query = "(@(body,title) $esc) | (@(namespace,pagename) " . $this->starQuery($esc) . ")";
}
public function setSearchAllQueryWithCategoryFilter(string $k, string $c): void {
$esc = $this->_sphinx->EscapeString($k);
$cat = $this->_sphinx->EscapeString($c);
$f = str_starts_with($c, "-") ? '-"' . substr($cat, 1) . '"' : '"' . $cat . '"';
$this->_query = "(@(namespace,pagename) $f) & ((@(body,title) $esc) | (@(namespace,pagename) " . $this->starQuery($esc) . "))";
}
public function setSearchOnlyPagename(): void { $this->_query = "(@pagename {$this->_query})"; }
}