]/', ' ', $rawKw); return trim(preg_replace('/\s+/', ' ', $clean)); } /** * Advanced Multi-Pass Gated Fallback Search Engine * Protects Sphinx from executing massive, low-selectivity wildcard lookups on short keywords */ function sphinx_executeMultiPassSearch($search, string $keywords, string $cats, int $start, int $maxResults): array { $words = preg_split('/\s+/', $keywords, -1, PREG_SPLIT_NO_EMPTY); $totalWords = count($words); $res = false; $total = 0; $finalQuery = ''; $passUsed = ''; // ---------------------------------------------------- // PASS 1: Holistic Exact Match (Blazing fast for "Eva") // ---------------------------------------------------- if (!$res) { $finalQuery = $keywords; $passUsed = "Pass 1 (Holistic Exact Match)"; if (empty($cats)) $search->setSearchAllQuery($finalQuery, ''); else $search->setSearchAllQueryWithCategoryFilter($finalQuery, $cats); $res = $search->search($start, $maxResults); $total = $search->getTotalFound(); } // ---------------------------------------------------- // PASS 2: Latency-Gated Wildcard & Morphological Permutation // ---------------------------------------------------- if (!$res || $total == 0) { $fallbackParts = []; $hasWildcard = false; foreach ($words as $w) { $len = mb_strlen($w); // SECURITY GATE: Only expand tokens into wildcards if they are 4+ characters. // This prevents "Eva" from expanding to *Eva*, completely bypassing the 30s crash. if ($len >= 4) { $hasWildcard = true; $variants = ["\"$w\"", "*$w*"]; // Consonant collapsing mapping (e.g., webber -> weber) $collapsed = preg_replace('/(.)\1/u', '$1', $w); if ($collapsed !== $w) { $variants[] = "\"$collapsed\""; $variants[] = "*$collapsed*"; } // Root cluster prefix truncate $truncated = mb_substr($w, 0, max(3, $len - 3)); $variants[] = "*$truncated*"; $fallbackParts[] = '(' . implode(' | ', array_unique($variants)) . ')'; } else { // Short tokens stay bounded exactly as strict words to keep search times under 10ms $fallbackParts[] = '"' . $w . '"'; } } // Execute Pass 2 ONLY if we have safe wildcards to run if ($hasWildcard) { $finalQuery = implode(' ', $fallbackParts); $passUsed = "Pass 2 (Gated Typo Permutation)"; if (empty($cats)) $search->setSearchAllQuery($finalQuery, ''); else $search->setSearchAllQueryWithCategoryFilter($finalQuery, $cats); $res = $search->search($start, $maxResults); $total = $search->getTotalFound(); } } // ---------------------------------------------------- // PASS 3: Holistic Quorum Isolation (Safe strict fallback) // ---------------------------------------------------- if (!$res || $total == 0) { $finalQuery = sprintf('"%s"/%d', $keywords, max(1, $totalWords - 1)); $passUsed = "Pass 3 (Holistic Quorum Isolation)"; if (empty($cats)) $search->setSearchAllQuery($finalQuery, ''); else $search->setSearchAllQueryWithCategoryFilter($finalQuery, $cats); $res = $search->search($start, $maxResults); $total = $search->getTotalFound(); } // ---------------------------------------------------- // PHASE 2: Split Token Optimization (Loops 4, 5, 6) // ---------------------------------------------------- if (!$res || $total == 0) { // Pass 4: Split Word Exact Match Loop $splitQueryParts = []; foreach ($words as $w) { $splitQueryParts[] = '"' . $w . '"'; } $finalQuery = implode(' | ', $splitQueryParts); $passUsed = "Pass 4 (Split Word Exact Match Loop)"; if (empty($cats)) $search->setSearchAllQuery($finalQuery, ''); else $search->setSearchAllQueryWithCategoryFilter($finalQuery, $cats); $res = $search->search($start, $maxResults); $total = $search->getTotalFound(); // Pass 5: Gated Split Word Wildcard Loop if (!$res || $total == 0) { $splitQueryParts = []; $hasSplitWildcard = false; foreach ($words as $w) { if (mb_strlen($w) >= 4) { $hasSplitWildcard = true; $splitQueryParts[] = sprintf('(*%s* | "%s")', $w, $w); } else { $splitQueryParts[] = '"' . $w . '"'; } } if ($hasSplitWildcard) { $finalQuery = implode(' | ', $splitQueryParts); $passUsed = "Pass 5 (Split Word Wildcard Loop)"; if (empty($cats)) $search->setSearchAllQuery($finalQuery, ''); else $search->setSearchAllQueryWithCategoryFilter($finalQuery, $cats); $res = $search->search($start, $maxResults); $total = $search->getTotalFound(); } } // Pass 6: Split Word Quorum Loop if (!$res || $total == 0) { $splitQueryParts = []; foreach ($words as $w) { $splitQueryParts[] = sprintf('"%s"/1', $w); } $finalQuery = implode(' | ', $splitQueryParts); $passUsed = "Pass 6 (Split Word Quorum Loop)"; if (empty($cats)) $search->setSearchAllQuery($finalQuery, ''); else $search->setSearchAllQueryWithCategoryFilter($finalQuery, $cats); $res = $search->search($start, $maxResults); $total = $search->getTotalFound(); } } return [ 'res' => $res, 'total' => $total, 'finalQuery' => $finalQuery, 'passUsed' => $passUsed ]; } /** * Advanced Multi-Metric Bitmask and Density Sorting Processor * Guarantees holistic multi-word concepts (even fuzzed) always cluster above single-token matches * * @param array $pages * @param string $keywords * @return array */ function sphinx_sortPagesByLevenshtein(array $pages, string $keywords): array { uasort($pages, function($a, $b) use ($keywords) { $titleA = !empty($a['titleTextExcerpt']) ? $a['titleTextExcerpt'] : ''; $titleB = !empty($b['titleTextExcerpt']) ? $b['titleTextExcerpt'] : ''; $pageIdA = !empty($a['page']) ? $a['page'] : ''; $pageIdB = !empty($b['page']) ? $b['page'] : ''; // 1. Normalize query and isolate independent tokens $cleanKw = mb_strtolower(trim($keywords)); $kwTokens = preg_split('/\s+/', $cleanKw, -1, PREG_SPLIT_NO_EMPTY); $uniqueKwTokens = array_unique($kwTokens); $cleanTitleA = mb_strtolower(trim(strip_tags($titleA))); $cleanTitleB = mb_strtolower(trim(strip_tags($titleB))); $cleanIdA = mb_strtolower(trim(str_replace(':', ' ', $pageIdA))); $cleanIdB = mb_strtolower(trim(str_replace(':', ' ', $pageIdB))); $bodyA = mb_strtolower(trim(strip_tags($a['bodyExcerpt'] ?? ''))); $bodyB = mb_strtolower(trim(strip_tags($b['bodyExcerpt'] ?? ''))); // 2. Compute explicit semantic token intersection bits $bitmaskA = 0; $bitmaskB = 0; $uniqueMatchedTokensA = 0; $uniqueMatchedTokensB = 0; foreach ($uniqueKwTokens as $index => $token) { $bitFlag = 1 << $index; $collapsedToken = preg_replace('/(.)\1/u', '$1', $token); // Handle fuzzy variations like webber -> weber // Check Title, Page Identifier, or Body Excerpt context boundaries $inTitleA = (mb_strpos($cleanTitleA, $token) !== false || mb_strpos($cleanTitleA, $collapsedToken) !== false); $inIdA = (mb_strpos($cleanIdA, $token) !== false || mb_strpos($cleanIdA, $collapsedToken) !== false); $inBodyA = (mb_strpos($bodyA, $token) !== false || mb_strpos($bodyA, $collapsedToken) !== false); if ($inTitleA || $inIdA || $inBodyA) { $bitmaskA |= $bitFlag; $uniqueMatchedTokensA++; } $inTitleB = (mb_strpos($cleanTitleB, $token) !== false || mb_strpos($cleanTitleB, $collapsedToken) !== false); $inIdB = (mb_strpos($cleanIdB, $token) !== false || mb_strpos($cleanIdB, $collapsedToken) !== false); $inBodyB = (mb_strpos($bodyB, $token) !== false || mb_strpos($bodyB, $collapsedToken) !== false); if ($inTitleB || $inIdB || $inBodyB) { $bitmaskB |= $bitFlag; $uniqueMatchedTokensB++; } } // TIER 1: Concept Richness Priority (Documents matching MORE unique semantic items ALWAYS win) if ($uniqueMatchedTokensA !== $uniqueMatchedTokensB) { return $uniqueMatchedTokensB <=> $uniqueMatchedTokensA; } // TIER 2: Title & Identity Prominence Bonus // If two documents match both keywords, the one containing them in the TITLE or Page ID wins over the BODY $scoreWeightsA = 0; $scoreWeightsB = 0; foreach ($uniqueKwTokens as $token) { $collapsed = preg_replace('/(.)\1/u', '$1', $token); if (mb_strpos($cleanTitleA, $token) !== false || mb_strpos($cleanTitleA, $collapsed) !== false) $scoreWeightsA += 10; if (mb_strpos($cleanIdA, $token) !== false || mb_strpos($cleanIdA, $collapsed) !== false) $scoreWeightsA += 8; if (mb_strpos($cleanTitleB, $token) !== false || mb_strpos($cleanTitleB, $collapsed) !== false) $scoreWeightsB += 10; if (mb_strpos($cleanIdB, $token) !== false || mb_strpos($cleanIdB, $collapsed) !== false) $scoreWeightsB += 8; } if ($scoreWeightsA !== $scoreWeightsB) { return $scoreWeightsB <=> $scoreWeightsA; } // TIER 3: Absolute continuous string sequence priority $seqA = (mb_strpos($cleanTitleA, $cleanKw) !== false || mb_strpos($cleanIdA, $cleanKw) !== false) ? 1 : 0; $seqB = (mb_strpos($cleanTitleB, $cleanKw) !== false || mb_strpos($cleanIdB, $cleanKw) !== false) ? 1 : 0; if ($seqA !== $seqB) { return $seqB <=> $seqA; } // TIER 4: Normalized structural character distance matrix fallback $scoreTitleA = levenshtein($cleanKw, mb_substr($cleanTitleA, 0, 255)); $scoreTitleB = levenshtein($cleanKw, mb_substr($cleanTitleB, 0, 255)); if ($scoreTitleA === $scoreTitleB) { $scoreIdA = levenshtein($cleanKw, mb_substr($cleanIdA, 0, 255)); $scoreIdB = levenshtein($cleanKw, mb_substr($cleanIdB, 0, 255)); return $scoreIdA <=> $scoreIdB; } return $scoreTitleA <=> $scoreTitleB; }); return $pages; }