Index: trunk/extensions/LuceneSearch.php |
— | — | @@ -21,7 +21,7 @@ |
22 | 22 | |
23 | 23 | function wfLuceneSearch() { |
24 | 24 | global $IP; |
25 | | -require_once( "$IP/includes/SpecialPage.php" ); |
| 25 | +require_once("$IP/includes/SpecialPage.php"); |
26 | 26 | |
27 | 27 | class LuceneSearch extends SpecialPage |
28 | 28 | { |
— | — | @@ -42,8 +42,8 @@ |
43 | 43 | return $link; |
44 | 44 | } |
45 | 45 | |
46 | | - function execute( $par ) { |
47 | | - global $wgRequest, $wgOut, $wgTitle, $wgContLang; |
| 46 | + function execute($par) { |
| 47 | + global $wgRequest, $wgOut, $wgTitle, $wgContLang, $wgUser; |
48 | 48 | |
49 | 49 | $this->setHeaders(); |
50 | 50 | |
— | — | @@ -56,88 +56,136 @@ |
57 | 57 | |
58 | 58 | $q = $wgRequest->getText('search'); |
59 | 59 | |
60 | | - if ($wgRequest->getText('go') === 'Go') { |
61 | | - $t = SearchEngine::getNearMatch($q); |
62 | | - if(!is_null($t)) { |
63 | | - $wgOut->redirect($t->getFullURL()); |
64 | | - return; |
| 60 | + if ($wgRequest->getText('gen') == 'titlematch') { |
| 61 | + $limit = $wgRequest->getInt("limit"); |
| 62 | + if ($limit < 1 || $limit > 50) |
| 63 | + $limit = 20; |
| 64 | + header("Content-Type: text/plain; charset=ISO_8859-1"); |
| 65 | + if (strlen($q) < 1) |
| 66 | + wfAbruptExit(); |
| 67 | + |
| 68 | + $db =& wfGetDB(DB_SLAVE); |
| 69 | + $page = $db->tableName('page'); |
| 70 | + $sql = "SELECT page_title FROM $page WHERE page_namespace=0 AND " |
| 71 | + . "lower(page_title) LIKE lower('".wfStrEncode($q)."%') " |
| 72 | + . "LIMIT $limit"; |
| 73 | + $res = $db->query($sql, 'LuceneSearch::execute'); |
| 74 | + while ($row = $db->fetchObject($res)) { |
| 75 | + $t = Title::makeTitle(0, $row->page_title); |
| 76 | + echo $t->getPrefixedDBKey() . "\n"; |
65 | 77 | } |
| 78 | + wfAbruptExit(); |
66 | 79 | } |
67 | 80 | |
68 | | - $r = $this->doLuceneSearch($q); |
69 | | - $numresults = $r[0]; |
70 | | - $results = $r[1]; |
71 | | - |
72 | | - $wgOut->setSubtitle(wfMsg('searchquery', htmlspecialchars($q))); |
73 | 81 | $wgOut->addWikiText(wfMsg('searchresulttext')); |
74 | 82 | $wgOut->addHTML($this->showShortDialog($q)); |
75 | 83 | |
76 | | - if ($numresults < 1) { |
77 | | - $wgOut->addWikiText(wfMsg("searchnoresults")); |
| 84 | + if ($q != null && strlen($q) > 1) { |
| 85 | + if ($wgRequest->getText('go') === 'Go') { |
| 86 | + $t = SearchEngine::getNearMatch($q); |
| 87 | + if(!is_null($t)) { |
| 88 | + $wgOut->redirect($t->getFullURL()); |
| 89 | + return; |
| 90 | + } |
| 91 | + } |
| 92 | + |
| 93 | + $r = $this->doLuceneSearch($q); |
| 94 | + $numresults = $r[0]; |
| 95 | + $results = $r[1]; |
| 96 | + |
| 97 | + $limit = $wgRequest->getInt('limit'); |
| 98 | + $offset = $wgRequest->getInt('offset'); |
| 99 | + |
| 100 | + $wgOut->setSubtitle(wfMsg('searchquery', htmlspecialchars($q))); |
| 101 | + |
78 | 102 | $suggestion = trim($results); |
79 | 103 | if ($numresults == -1 && strlen($suggestion) > 0) { |
80 | | - $wgOut->addHTML(wfMsg("searchdidyoumean", |
| 104 | + $o .= " " . wfMsg("searchdidyoumean", |
81 | 105 | $this->makelink($suggestion, $offset, $limit), |
82 | | - htmlspecialchars($suggestion))); |
| 106 | + htmlspecialchars($suggestion)); |
83 | 107 | } |
84 | | - } else { |
85 | | - $limit = $wgRequest->getInt('limit'); |
86 | | - $offset = $wgRequest->getInt('offset'); |
87 | | - if ($limit == 0 || $limit > 100) |
88 | | - $limit = LS_PER_PAGE; |
89 | | - |
90 | | - $showresults = min($limit, count($results)-$numresults); |
91 | | - $i = $offset; |
92 | | - $resq = trim(preg_replace("/[] \\|[()\"{}]+/", " ", $q)); |
93 | | - $contextWords = implode("|", |
| 108 | + $wgOut->addHTML("<div style='text-align: center'>".$o."</div>"); |
| 109 | + |
| 110 | + $nmtext = ""; |
| 111 | + if ($offset == 0) { |
| 112 | + $titles = $this->doTitleMatches($q); |
| 113 | + if (count($titles) > 0) { |
| 114 | + $sk =& $wgUser->getSkin(); |
| 115 | + $nmtext = "<p>".wfMsg('searchnearmatches'); |
| 116 | + $i = 0; |
| 117 | + $wgOut->addHTML("<ul>"); |
| 118 | + foreach ($titles as $title) { |
| 119 | + if (++$i > 5) break; |
| 120 | + $nmtext .= wfMsg('searchnearmatch', |
| 121 | + $sk->makeKnownLinkObj($title, '')); |
| 122 | + } |
| 123 | + $nmtext .= "</ul>"; |
| 124 | + $nmtext .= "<hr/></p>"; |
| 125 | + } |
| 126 | + } |
| 127 | + |
| 128 | + $wgOut->addHTML($nmtext); |
| 129 | + |
| 130 | + if ($numresults < 1) { |
| 131 | + $o = wfMsg("searchnoresults"); |
| 132 | + $wgOut->addHTML($o); |
| 133 | + } else { |
| 134 | + if ($limit == 0 || $limit > 100) |
| 135 | + $limit = LS_PER_PAGE; |
| 136 | + |
| 137 | + $showresults = min($limit, count($results)-$numresults); |
| 138 | + $i = $offset; |
| 139 | + $resq = trim(preg_replace("/[] \\|[()\"{}]+/", " ", $q)); |
| 140 | + $contextWords = implode("|", |
94 | 141 | $wgContLang->convertForSearchResult(split(" ", $resq))); |
95 | 142 | |
96 | | - $top = wfMsg("searchnumber", $offset + 1, |
97 | | - min($numresults, $offset+$limit), $numresults); |
98 | | - $out = "<ul start=".($offset + 1).">"; |
99 | | - $chunks = array_chunk($results, $limit); |
100 | | - $numchunks = ceil($numresults / $limit); |
101 | | - $whichchunk = $offset / $limit; |
102 | | - $prevnext = ""; |
103 | | - if ($whichchunk > 0) |
104 | | - $prevnext .= "<a href=\"". |
105 | | - $this->makelink($q, $offset-$limit, $limit)."\">". |
106 | | - wfMsg("searchprev")."</a> "; |
107 | | - $first = max($whichchunk - 11, 0); |
108 | | - $last = min($numchunks, $whichchunk + 11); |
109 | | - //$wgOut->addWikiText("whichchunk=$whichchunk numchunks=$numchunks first=$first last=$last num=".count($chunks)." limit=$limit offset=$offset results=".count($results)."\n\n"); |
110 | | - for($i = $first; $i < $last; $i++) { |
111 | | - if ($i === $whichchunk) |
112 | | - $prevnext .= "<strong>".($i+1)."</strong> "; |
113 | | - else |
| 143 | + $top = wfMsg("searchnumber", $offset + 1, |
| 144 | + min($numresults, $offset+$limit), $numresults); |
| 145 | + $out = "<ul start=".($offset + 1).">"; |
| 146 | + $chunks = array_chunk($results, $limit); |
| 147 | + $numchunks = ceil($numresults / $limit); |
| 148 | + $whichchunk = $offset / $limit; |
| 149 | + $prevnext = ""; |
| 150 | + if ($whichchunk > 0) |
114 | 151 | $prevnext .= "<a href=\"". |
115 | | - $this->makelink($q, $limit*$i, |
116 | | - $limit)."\">".($i+1)."</a> "; |
| 152 | + $this->makelink($q, $offset-$limit, $limit)."\">". |
| 153 | + wfMsg("searchprev")."</a> "; |
| 154 | + $first = max($whichchunk - 11, 0); |
| 155 | + $last = min($numchunks, $whichchunk + 11); |
| 156 | + //$wgOut->addWikiText("whichchunk=$whichchunk numchunks=$numchunks first=$first last=$last num=".count($chunks)." limit=$limit offset=$offset results=".count($results)."\n\n"); |
| 157 | + for($i = $first; $i < $last; $i++) { |
| 158 | + if ($i === $whichchunk) |
| 159 | + $prevnext .= "<strong>".($i+1)."</strong> "; |
| 160 | + else |
| 161 | + $prevnext .= "<a href=\"". |
| 162 | + $this->makelink($q, $limit*$i, |
| 163 | + $limit)."\">".($i+1)."</a> "; |
| 164 | + } |
| 165 | + if ($whichchuck < $numchunks) |
| 166 | + $prevnext .= "<a href=\"". |
| 167 | + $this->makelink($q, $offset + $limit, $limit)."\">". |
| 168 | + wfMsg("searchnext")."</a> "; |
| 169 | + $prevnext = "<div style='text-align: center'>$prevnext</div>"; |
| 170 | + $top .= $prevnext; |
| 171 | + foreach ($chunks[$whichchunk] as $result) { |
| 172 | + $out .= $this->showHit($result[0], $result[1], $contextWords); |
| 173 | + } |
| 174 | + $out .= "</ol>"; |
117 | 175 | } |
118 | | - if ($whichchuck < $numchunks) |
119 | | - $prevnext .= "<a href=\"". |
120 | | - $this->makelink($q, $offset + $limit, $limit)."\">". |
121 | | - wfMsg("searchnext")."</a> "; |
122 | | - $prevnext = "<div style='text-align: center'>$prevnext</div>"; |
123 | | - $top .= $prevnext; |
124 | | - foreach ($chunks[$whichchunk] as $result) { |
125 | | - $out .= $this->showHit($result[0], $result[1], $contextWords); |
126 | | - } |
127 | | - $out .= "</ol>"; |
| 176 | + $wgOut->addHTML("<hr/>" . $top . $out); |
| 177 | + $wgOut->addHTML("<hr/>" . $prevnext); |
| 178 | + $wgOut->addHTML($this->showFullDialog($q)); |
128 | 179 | } |
129 | | - $wgOut->addHTML("<hr/>" . $top . $out); |
130 | | - $wgOut->addHTML("<hr/>" . $prevnext); |
131 | | - $wgOut->addHTML($this->showFullDialog($q)); |
132 | 180 | $wgOut->setRobotpolicy('noindex,nofollow'); |
133 | 181 | } |
134 | 182 | |
135 | 183 | function showHit($score, $t, $terms) { |
136 | 184 | $fname = 'LuceneSearch::showHit'; |
137 | | - wfProfileIn( $fname ); |
| 185 | + wfProfileIn($fname); |
138 | 186 | global $wgUser, $wgContLang; |
139 | 187 | |
140 | 188 | if(is_null($t)) { |
141 | | - wfProfileOut( $fname ); |
| 189 | + wfProfileOut($fname); |
142 | 190 | return "<!-- Broken link in search result -->\n"; |
143 | 191 | } |
144 | 192 | $sk =& $wgUser->getSkin(); |
— | — | @@ -159,7 +207,7 @@ |
160 | 208 | |
161 | 209 | $lines = explode("\n", $text); |
162 | 210 | |
163 | | - $max = IntVal( $contextchars ) + 1; |
| 211 | + $max = IntVal($contextchars) + 1; |
164 | 212 | $pat1 = "/(.*)($terms)(.{0,$max})/i"; |
165 | 213 | |
166 | 214 | $lineno = 0; |
— | — | @@ -189,8 +237,8 @@ |
190 | 238 | |
191 | 239 | $extract .= "<br /><small>{$line}</small>\n"; |
192 | 240 | } |
193 | | - wfProfileOut( "$fname-extract" ); |
194 | | - wfProfileOut( $fname ); |
| 241 | + wfProfileOut("$fname-extract"); |
| 242 | + wfProfileOut($fname); |
195 | 243 | $date = $wgContLang->timeanddate($rev->getTimestamp()); |
196 | 244 | $percent = sprintf("%2.1f%%", $score * 100); |
197 | 245 | //$score = wfMsg("searchscore", $percent); |
— | — | @@ -218,13 +266,34 @@ |
219 | 267 | return $text; |
220 | 268 | } |
221 | 269 | |
222 | | - function doLuceneSearch( $query ) { |
| 270 | + function doTitleMatches($query) { |
223 | 271 | global $wgLuceneHost, $wgLucenePort; |
224 | 272 | $sock = socket_create(AF_INET, SOCK_STREAM, SOL_TCP); |
225 | 273 | $conn = socket_connect($sock, $wgLuceneHost, $wgLucenePort); |
226 | | - socket_write($sock, urlencode($query) . "\n"); |
| 274 | + socket_write($sock, "TITLEMATCH\n" . urlencode($query) . "\n"); |
227 | 275 | $results = array(); |
| 276 | + while (($result = @socket_read($sock, 1024, PHP_NORMAL_READ)) != FALSE) { |
| 277 | + $result = chop($result); |
| 278 | + list($score, $namespace, $title) = split(" ", $result); |
| 279 | + if (!in_array($namespace, $this->namespaces)) { |
| 280 | + continue; |
| 281 | + } |
| 282 | + $fulltitle = Title::makeTitle($namespace, $title); |
| 283 | + if ($fulltitle === null) { |
| 284 | + continue; |
| 285 | + } |
| 286 | + $results[] = $fulltitle; |
| 287 | + } |
| 288 | + return $results; |
| 289 | + } |
228 | 290 | |
| 291 | + function doLuceneSearch($query) { |
| 292 | + global $wgLuceneHost, $wgLucenePort; |
| 293 | + $sock = socket_create(AF_INET, SOCK_STREAM, SOL_TCP); |
| 294 | + $conn = socket_connect($sock, $wgLuceneHost, $wgLucenePort); |
| 295 | + socket_write($sock, "SEARCH\n" . urlencode($query) . "\n"); |
| 296 | + $results = array(); |
| 297 | + |
229 | 298 | $numresults = @socket_read($sock, 1024, PHP_NORMAL_READ); |
230 | 299 | wfDebug("total [$numresults] hits\n"); |
231 | 300 | if ($numresults === FALSE) |
— | — | @@ -257,14 +326,20 @@ |
258 | 327 | } |
259 | 328 | |
260 | 329 | function showShortDialog($term) { |
261 | | - $searchField = "<input type='text' name=\"search\" value=\"" . |
262 | | - htmlspecialchars($term) ."\" width=\"80\" />\n"; |
263 | | - |
264 | 330 | $searchButton = '<input type="submit" name="searchx" value="' . |
265 | 331 | htmlspecialchars(wfMsg('powersearch')) . "\" />\n"; |
266 | | - $ret = $searchField . $searchButton; |
267 | | - return "<form id=\"search\" method=\"get\" " . |
268 | | - "action=\"$action\">\n<div style='text-align: center'>{$ret}</div>\n</form>\n"; |
| 332 | + $searchField = "<div><input type='text' id='lsearchbox' onkeyup=\"resultType()\" " |
| 333 | + . "name=\"search\" value=\"" |
| 334 | + . htmlspecialchars($term) ."\" style='width: 50%;margin-left: 25%' " |
| 335 | + . " autocomplete=\"off\" />\n" |
| 336 | + . "<span id='loadStatus'></span>" |
| 337 | + . $searchButton |
| 338 | + . "<div id='results'></div></div>"; |
| 339 | + |
| 340 | + $ret = $searchField /*. $searchButton*/; |
| 341 | + return $this->makeSuggestJS() |
| 342 | + . "<form id=\"search\" method=\"get\" " |
| 343 | + . "action=\"$action\">\n<div>{$ret}</div>\n</form>\n"; |
269 | 344 | } |
270 | 345 | |
271 | 346 | function showFullDialog($term) { |
— | — | @@ -274,7 +349,7 @@ |
275 | 350 | $checked = in_array($ns, $this->namespaces) |
276 | 351 | ? ' checked="checked"' |
277 | 352 | : ''; |
278 | | - $name = str_replace( '_', ' ', $name ); |
| 353 | + $name = str_replace('_', ' ', $name); |
279 | 354 | if('' == $name) { |
280 | 355 | $name = wfMsg('blanknamespace'); |
281 | 356 | } |
— | — | @@ -283,7 +358,7 @@ |
284 | 359 | } |
285 | 360 | |
286 | 361 | $searchField = "<input type='text' name=\"search\" value=\"" . |
287 | | - htmlspecialchars( $term ) ."\" width=\"80\" />\n"; |
| 362 | + htmlspecialchars($term) ."\" width=\"80\" />\n"; |
288 | 363 | |
289 | 364 | $searchButton = '<input type="submit" name="searchx" value="' . |
290 | 365 | htmlspecialchars(wfMsg('powersearch')) . "\" />\n"; |
— | — | @@ -291,24 +366,104 @@ |
292 | 367 | $ret = wfMsg('lucenepowersearchtext', |
293 | 368 | $namespaces, $redirect, $searchField, |
294 | 369 | '', '', '', '', '', # Dummy placeholders |
295 | | - $searchButton ); |
| 370 | + $searchButton); |
296 | 371 | |
297 | | - $title = Title::makeTitle( NS_SPECIAL, 'Search' ); |
| 372 | + $title = Title::makeTitle(NS_SPECIAL, 'Search'); |
298 | 373 | $action = $title->escapeLocalURL(); |
299 | 374 | return "<br /><br />\n<form id=\"powersearch\" method=\"get\" " . |
300 | 375 | "action=\"$action\">\n{$ret}\n</form>\n"; |
301 | 376 | } |
| 377 | + |
| 378 | + function makeSuggestJS() { |
| 379 | + global $wgScript; |
| 380 | + return <<<___EOF___ |
| 381 | +<script language="javascript"> |
| 382 | + |
| 383 | +var xmlHttp = (window.XMLHttpRequest) ? new XMLHttpRequest : new ActiveXObject("Microsoft.XMLHTTP"); |
| 384 | +var searchCache = {}; |
| 385 | +var searchStr; |
| 386 | +var searchTimeout; |
| 387 | + |
| 388 | +function getResults() |
| 389 | +{ |
| 390 | +// alert(searchStr) |
| 391 | + //xmlHttp.open("GET", "$wgScript?title=Special:Search?gen=titlematch&ns0=0&limit=10&search=" + escape(searchStr), true); |
| 392 | + var encStr = escape(searchStr.replace(/ /g, '_')); |
| 393 | + xmlHttp.open("GET", "/w/Special:Search?gen=titlematch&ns0=0&limit=10&search=" + encStr, true); |
| 394 | + |
| 395 | + xmlHttp.onreadystatechange = parseResults; |
| 396 | + xmlHttp.send(null); |
| 397 | + //document.getElementById("results").innerHTML = "Loading..."; |
302 | 398 | } |
303 | 399 | |
| 400 | +function parseResults() |
| 401 | +{ |
| 402 | + if (xmlHttp.readyState > 3) |
| 403 | + { |
| 404 | + document.getElementById("loadStatus").innerHTML = ""; |
| 405 | + var resultArr = xmlHttp.responseText.split("\\n"); |
| 406 | + searchCache[searchStr.toLowerCase()] = resultArr; |
| 407 | + showResults(resultArr); |
| 408 | + } |
| 409 | +} |
| 410 | + |
| 411 | +function showResults(resultArr) |
| 412 | +{ |
| 413 | + var returnStr = ""; |
| 414 | + var resultsEl = document.getElementById("results"); |
| 415 | + |
| 416 | + if (resultArr.length < 2) |
| 417 | + resultsEl.innerHTML = "No results"; |
| 418 | + else |
| 419 | + { |
| 420 | + resultsEl.innerHTML = ""; |
| 421 | + |
| 422 | + for (var i=0; i < resultArr.length; i++) |
| 423 | + { |
| 424 | + var linkEl = document.createElement("a"); |
| 425 | + linkEl.href = "$wgScript?title=" + resultArr[i]; |
| 426 | + var textEl = document.createTextNode(resultArr[i].replace(/_/g, ' ')); |
| 427 | + linkEl.appendChild(textEl); |
| 428 | + resultsEl.appendChild(linkEl); |
| 429 | + } |
| 430 | + } |
| 431 | + |
| 432 | + resultsEl.style.display = "block"; |
| 433 | +} |
| 434 | + |
| 435 | +function resultType() |
| 436 | +{ |
| 437 | + searchStr = document.getElementById("lsearchbox").value; |
| 438 | + if (searchTimeout) clearTimeout(searchTimeout); |
| 439 | + |
| 440 | + if (searchStr != "") |
| 441 | + { |
| 442 | + if (searchCache[searchStr.toLowerCase()]) |
| 443 | + showResults(searchCache[searchStr.toLowerCase()]) |
| 444 | + else |
| 445 | + searchTimeout = setTimeout(getResults, 500); |
| 446 | + } |
| 447 | + else |
| 448 | + { |
| 449 | + document.getElementById("results").style.display = "none"; |
| 450 | + } |
| 451 | +} |
| 452 | +</script> |
| 453 | +___EOF___; |
| 454 | + } |
| 455 | +} |
| 456 | + |
304 | 457 | global $wgMessageCache; |
305 | | -SpecialPage::addPage( new LuceneSearch ); |
| 458 | +SpecialPage::addPage(new LuceneSearch); |
306 | 459 | $wgMessageCache->addMessage("searchnumber", "<strong>Results $1-$2 of $3</strong>"); |
307 | 460 | $wgMessageCache->addMessage("searchprev", "« <span style='font-size: small'>Prev</span>"); |
308 | 461 | $wgMessageCache->addMessage("searchnext", "<span style='font-size: small'>Next</span> »"); |
309 | 462 | $wgMessageCache->addMessage("searchscore", "Relevancy: $1"); |
310 | 463 | $wgMessageCache->addMessage("searchsize", "$1k ($2 words)"); |
311 | | -$wgMessageCache->addMessage("searchdidyoumean", "Did you mean \"<a href=\"$1\">$2</a>\"?"); |
312 | | -$wgMessageCache->addMessage("searchnoresults", "Sorry, there were no matches to your query."); |
| 464 | +$wgMessageCache->addMessage("searchdidyoumean", "Did you mean: \"<a href=\"$1\">$2</a>\"?"); |
| 465 | +$wgMessageCache->addMessage("searchnoresults", "Sorry, there were no exact matches to your query."); |
| 466 | +$wgMessageCache->addMessage("searchnearmatches", "<b>These pages have similar titles to your query:</b>\n"); |
| 467 | +$wgMessageCache->addMessage("searchnearmatch", "<li>$1</li>\n"); |
313 | 468 | $wgMessageCache->addMessage("lucenepowersearchtext", " |
314 | 469 | Search in namespaces:\n |
315 | 470 | $1\n |