PageRenderTime 28ms CodeModel.GetById 19ms RepoModel.GetById 0ms app.codeStats 1ms

/extensions/MWSearch/MWSearch_body.php

https://github.com/ChuguluGames/mediawiki-svn
PHP | 740 lines | 485 code | 92 blank | 163 comment | 90 complexity | 4ab290cb726983fe940747c71896ea4a MD5 | raw file
  1. <?php
  2. class LuceneSearch extends SearchEngine {
  3. /**
  4. * Perform a full text search query and return a result set.
  5. *
  6. * @param string $term - Raw search term
  7. * @return LuceneSearchSet
  8. * @access public
  9. */
  10. function searchText( $term ) {
  11. return LuceneSearchSet::newFromQuery( isset($this->related)? 'related' : 'search',
  12. $term, $this->namespaces, $this->limit, $this->offset, $this->searchingEverything() );
  13. }
  14. /**
  15. * Perform a title-only search query and return a result set.
  16. *
  17. * @param string $term - Raw search term
  18. * @return LuceneSearchSet
  19. * @access public
  20. */
  21. function searchTitle( $term ) {
  22. return null;
  23. }
  24. /**
  25. * PrefixSearchBackend override for OpenSearch results
  26. */
  27. static function prefixSearch( $ns, $search, $limit, &$results ) {
  28. $it = LuceneSearchSet::newFromQuery( 'prefix', $search, $ns, $limit, 0 );
  29. $results = array();
  30. if( $it ) { // $it can be null
  31. while( $res = $it->next() ) {
  32. $results[] = $res->getTitle()->getPrefixedText();
  33. }
  34. }
  35. return false;
  36. }
  37. /**
  38. * Check if we are searching all the namespaces on this wiki
  39. *
  40. * @return boolean
  41. */
  42. function searchingEverything(){
  43. return $this->namespaces == array_keys( SearchEngine::searchableNamespaces() );
  44. }
  45. /**
  46. * Prepare query for the lucene-search daemon:
  47. *
  48. * 1) rewrite namespaces into standardized form
  49. * e.g. image:clouds -> [6]:clouds
  50. *
  51. * 2) rewrite localizations of "search everything" keyword
  52. * e.g. alle:heidegger -> all:heidegger
  53. *
  54. * @param string query
  55. * @return string rewritten query
  56. * @access private
  57. */
  58. function replacePrefixes( $query ) {
  59. global $wgContLang, $wgLuceneUseRelated;
  60. wfProfileIn( __METHOD__ );
  61. // quick check, most of the time we don't need any rewriting
  62. if(strpos($query,':')===false){
  63. wfProfileOut( __METHOD__ );
  64. return $query;
  65. }
  66. // "search everything" keyword
  67. $allkeyword = wfMsgForContent('searchall');
  68. // check if this is query for related articles
  69. $relatedkey = wfMsgForContent('searchrelated').':';
  70. if($wgLuceneUseRelated && strncmp($query, $relatedkey, strlen($relatedkey)) == 0){
  71. $this->related = true;
  72. list($dummy,$ret) = explode(":",$query,2);
  73. wfProfileOut( __METHOD__ );
  74. return trim($ret);
  75. }
  76. global $wgCanonicalNamespaceNames, $wgNamespaceAliases;
  77. $nsNamesRaw = array_merge($wgContLang->getNamespaces(), $wgCanonicalNamespaceNames,
  78. array_keys( array_merge($wgNamespaceAliases, $wgContLang->getNamespaceAliases()) ) );
  79. # add all namespace names w/o spaces
  80. $nsNames = array();
  81. foreach($nsNamesRaw as $ns){
  82. if( $ns != ''){
  83. $nsNames[] = $ns;
  84. $nsNames[] = str_replace('_',' ',$ns);
  85. }
  86. }
  87. $regexp = implode('|',array_unique( $nsNames ));
  88. # rewrite the query by replacing valid namespace names
  89. $parts = preg_split('/(")/',$query,-1,PREG_SPLIT_DELIM_CAPTURE);
  90. $inquotes = false;
  91. $rewritten = '';
  92. foreach($parts as $part){
  93. if( $part == '"'){ # stuff in quote doesnt get rewritten
  94. $rewritten .= $part;
  95. $inquotes = !$inquotes;
  96. } elseif( $inquotes ){
  97. $rewritten .= $part;
  98. } else{
  99. # replace namespaces
  100. $r = preg_replace_callback('/(^|[| :])('.$regexp.'):/i',array($this,'replaceNamespace'),$part);
  101. # replace to backend all: notation
  102. $rewritten .= str_replace($allkeyword.':', 'all:', $r);
  103. }
  104. }
  105. wfProfileOut( __METHOD__ );
  106. return $rewritten;
  107. }
  108. /** callback to replace namespace names to internal notation, e.g. User: -> [2]: */
  109. function replaceNamespace($matches){
  110. global $wgContLang;
  111. $inx = $wgContLang->getNsIndex(str_replace(' ', '_', $matches[2]));
  112. if ($inx === false)
  113. return $matches[0];
  114. else
  115. return $matches[1]."[$inx]:";
  116. }
  117. function acceptListRedirects() {
  118. return false;
  119. }
  120. /** Merge the prefix into the query (if any) */
  121. function transformSearchTerm( $term ) {
  122. global $wgLuceneSearchVersion;
  123. if ( $wgLuceneSearchVersion >= 2.1 && $this->prefix != '' ){
  124. # convert to internal backend prefix notation
  125. $term = $term.' prefix:'.$this->prefix;
  126. }
  127. return $term;
  128. }
  129. }
  130. class LuceneResult extends SearchResult {
  131. /**
  132. * Construct a result object from single result line
  133. *
  134. * @param array $lines
  135. * @param string $method - method used to fetch these results
  136. * @return array (float, Title)
  137. * @access private
  138. */
  139. function __construct( $lines, $method ) {
  140. global $wgContLang;
  141. $interwiki = null;
  142. $line = $lines['result'];
  143. wfDebug( "Lucene line: '$line'\n" );
  144. # detect format
  145. $parts = explode(' ', $line);
  146. if(count($parts) == 3)
  147. list( $score, $namespace, $title ) = $parts;
  148. else
  149. list( $score, $interwiki, $namespace, $nsText, $title ) = $parts;
  150. $score = floatval( $score );
  151. $namespace = intval( $namespace );
  152. $title = urldecode( $title );
  153. if(!isset($nsText))
  154. $nsText = $wgContLang->getNsText($namespace);
  155. else
  156. $nsText = urldecode($nsText);
  157. $this->mInterwiki = '';
  158. // make title
  159. if( is_null($interwiki)){
  160. $this->mTitle = Title::makeTitle( $namespace, $title );
  161. } else{
  162. $interwiki = urldecode( $interwiki );
  163. // there might be a better way to make an interwiki link
  164. $t = $interwiki.':'.$nsText.':'.str_replace( '_', ' ', $title );
  165. $this->mTitle = Title::newFromText( $t );
  166. $this->mInterwiki = $interwiki;
  167. }
  168. $this->mScore = $score;
  169. $this->mWordCount = null;
  170. if(array_key_exists("#h.wordcount",$lines))
  171. $this->mWordCount = intval($lines["#h.wordcount"][0]);
  172. $this->mSize = null;
  173. if(array_key_exists("#h.size",$lines))
  174. $this->mSize = intval($lines["#h.size"][0]);
  175. $this->mDate = null;
  176. if(array_key_exists("#h.date",$lines))
  177. $this->mDate = $lines["#h.date"][0];
  178. // various snippets
  179. list( $this->mHighlightTitle, $dummy ) = $this->extractSnippet($lines,$nsText,"#h.title");
  180. if( is_null($this->mHighlightTitle) && $this->isInterwiki() ){
  181. // construct highlighted interwiki title without the interwiki part
  182. $this->mHighlightTitle = ($nsText==''? '' : $nsText.':') . str_replace( '_', ' ', $title );
  183. }
  184. list( $this->mHighlightText, $dummy ) = $this->extractSnippet($lines,'',"#h.text",true);
  185. list( $this->mHighlightRedirect, $redirect ) = $this->extractSnippet($lines,$nsText,"#h.redirect");
  186. $this->mRedirectTitle = null;
  187. if( !is_null($redirect)){
  188. # build redirect Title object
  189. if($interwiki != ''){
  190. $t = $interwiki.':'.$redirect;
  191. $this->mRedirectTitle = Title::newFromText( $t );
  192. } else{
  193. $parts = explode(":",$redirect,2);
  194. $redirectNs = intval($parts[0]);
  195. $redirectText = str_replace('_', ' ', $parts[1]);
  196. $this->mRedirectTitle = Title::makeTitle($redirectNs,$redirectText);
  197. }
  198. }
  199. list( $this->mHighlightSection, $section) = $this->extractSnippet($lines,'',"#h.section");
  200. $this->mSectionTitle = null;
  201. if( !is_null($section)){
  202. # build title + fragment Title object
  203. $t = $nsText.':'.str_replace( '_', ' ', $title ).'#'.$section;
  204. $this->mSectionTitle = Title::newFromText($t);
  205. }
  206. # fetch revision info if not an interwiki title, and not using prefix search
  207. if($this->mInterwiki == '' && $method != 'prefix')
  208. $this->mRevision = Revision::newFromTitle( $this->mTitle );
  209. if(!is_null($this->mTitle) && $this->mTitle->getNamespace() == NS_IMAGE)
  210. $this->mImage = wfFindFile( $this->mTitle );
  211. }
  212. /**
  213. * Get the pair [highlighted snippet, unmodified text] for highlighted text
  214. *
  215. * @param string $lines
  216. * @param string $nsText textual form of namespace
  217. * @param string $type
  218. * @param boolean $useFinalSeparator
  219. * @return array (highlighted, unmodified text)
  220. */
  221. function extractSnippet($lines, $nsText, $type, $useFinalSeparator=false){
  222. if(!array_key_exists($type,$lines))
  223. return array(null,null);
  224. $ret = "";
  225. $original = null;
  226. foreach($lines[$type] as $h){
  227. list($s,$o) = $this->extractSnippetLine($h,$useFinalSeparator);
  228. $ret .= $s;
  229. $original = $o;
  230. }
  231. if($nsText!='')
  232. $ret = $nsText.':'.$ret;
  233. return array($ret,$original);
  234. }
  235. /**
  236. * Parse one line of a snippet
  237. *
  238. * @param string $line
  239. * @param boolean $useFinalSeparator if "..." is to be appended to the end of snippet
  240. * @access protected
  241. * @return array(snippet,unmodified text)
  242. */
  243. function extractSnippetLine($line, $useFinalSeparator){
  244. $parts = explode(" ",$line);
  245. if(count($parts)!=4 && count($parts)!=5){
  246. wfDebug("Bad result line:".$line."\n");
  247. return "";
  248. }
  249. $splits = $this->stripBracketsSplit($parts[0]);
  250. $highlight = $this->stripBracketsSplit($parts[1]);
  251. $suffix = urldecode($this->stripBrackets($parts[2]));
  252. $text = urldecode($parts[3]);
  253. $original = null;
  254. if(count($parts) > 4)
  255. $original = urldecode($parts[4]);
  256. $splits[] = strlen($text);
  257. $start = 0;
  258. $snippet = "";
  259. $hi = 0;
  260. $ellipsis = wfMsgForContent( 'ellipsis' );
  261. foreach($splits as $sp){
  262. $sp = intval($sp);
  263. // highlight words!
  264. while($hi < count($highlight) && intval($highlight[$hi]) < $sp){
  265. $s = intval($highlight[$hi]);
  266. $e = intval($highlight[$hi+1]);
  267. $snippet .= substr($text,$start,$s-$start)."<span class='searchmatch'>".substr($text,$s,$e-$s)."</span>";
  268. $start = $e;
  269. $hi += 2;
  270. }
  271. // copy till split point
  272. $snippet .= substr($text,$start,$sp-$start);
  273. if($sp == strlen($text) && $suffix != '')
  274. $snippet .= $suffix;
  275. elseif($useFinalSeparator)
  276. $snippet .= " <b>" . $ellipsis . "</b> ";
  277. $start = $sp;
  278. }
  279. return array($snippet,$original);
  280. }
  281. /**
  282. * @access private
  283. */
  284. function stripBrackets($str){
  285. if($str == '[]')
  286. return '';
  287. return substr($str,1,strlen($str)-2);
  288. }
  289. /**
  290. * @access private
  291. * @return array
  292. */
  293. function stripBracketsSplit($str){
  294. $strip = $this->stripBrackets($str);
  295. if($strip == '')
  296. return array();
  297. else
  298. return explode(",",$strip);
  299. }
  300. function getTitle() {
  301. return $this->mTitle;
  302. }
  303. function getScore() {
  304. return null; // lucene scores are meaningless to the user...
  305. }
  306. function getTitleSnippet($terms){
  307. if( is_null($this->mHighlightTitle) )
  308. return '';
  309. return $this->mHighlightTitle;
  310. }
  311. function getTextSnippet($terms) {
  312. if( is_null($this->mHighlightText) )
  313. return parent::getTextSnippet($terms);
  314. return $this->mHighlightText;
  315. }
  316. function getRedirectSnippet($terms) {
  317. if( is_null($this->mHighlightRedirect) )
  318. return '';
  319. return $this->mHighlightRedirect;
  320. }
  321. function getRedirectTitle(){
  322. return $this->mRedirectTitle;
  323. }
  324. function getSectionSnippet(){
  325. if( is_null($this->mHighlightSection) )
  326. return '';
  327. return $this->mHighlightSection;
  328. }
  329. function getSectionTitle(){
  330. return $this->mSectionTitle;
  331. }
  332. function getInterwikiPrefix(){
  333. return $this->mInterwiki;
  334. }
  335. function isInterwiki(){
  336. return $this->mInterwiki != '';
  337. }
  338. function getTimestamp(){
  339. if( is_null($this->mDate) )
  340. return parent::getTimestamp();
  341. return $this->mDate;
  342. }
  343. function getWordCount(){
  344. if( is_null($this->mWordCount) )
  345. return parent::getWordCount();
  346. return $this->mWordCount;
  347. }
  348. function getByteSize(){
  349. if( is_null($this->mSize) )
  350. return parent::getByteSize();
  351. return $this->mSize;
  352. }
  353. function hasRelated(){
  354. global $wgLuceneSearchVersion, $wgLuceneUseRelated;
  355. return $wgLuceneSearchVersion >= 2.1 && $wgLuceneUseRelated;
  356. }
  357. }
  358. class LuceneSearchSet extends SearchResultSet {
  359. /**
  360. * Contact the MWDaemon search server and return a wrapper
  361. * object with the set of results. Results may be cached.
  362. *
  363. * @param string $method The protocol verb to use
  364. * @param string $query
  365. * @param int $limit
  366. * @param int $offset
  367. * @param bool $searchAll
  368. * @return array
  369. * @access public
  370. */
  371. static function newFromQuery( $method, $query, $namespaces = array(),
  372. $limit = 20, $offset = 0, $searchAll = False ) {
  373. $fname = 'LuceneSearchSet::newFromQuery';
  374. wfProfileIn( __METHOD__ );
  375. global $wgLuceneHost, $wgLucenePort, $wgDBname, $wgMemc;
  376. global $wgLuceneSearchVersion, $wgLuceneSearchCacheExpiry;
  377. global $wgLuceneSearchTimeout;
  378. $hosts = $wgLuceneHost;
  379. if( $method == 'prefix'){
  380. global $wgLucenePrefixHost;
  381. if( isset($wgLucenePrefixHost) )
  382. $hosts = $wgLucenePrefixHost;
  383. }
  384. if( is_array( $hosts ) ) {
  385. $pick = mt_rand( 0, count( $hosts ) - 1 );
  386. $host = $hosts[$pick];
  387. } else {
  388. $host = $hosts;
  389. }
  390. $enctext = rawurlencode( trim( $query ) );
  391. $searchUrl = "http://$host:$wgLucenePort/$method/$wgDBname/$enctext?" .
  392. wfArrayToCGI( array(
  393. 'namespaces' => implode( ',', $namespaces ),
  394. 'offset' => $offset,
  395. 'limit' => $limit,
  396. 'version' => $wgLuceneSearchVersion,
  397. 'iwlimit' => 10,
  398. 'searchall' => $searchAll? 1 : 0,
  399. ) );
  400. // try to fetch cached if caching is turned on
  401. if($wgLuceneSearchCacheExpiry > 0){
  402. $key = "$wgDBname:lucene:" . md5( $searchUrl );
  403. $resultSet = $wgMemc->get( $key );
  404. if( is_object( $resultSet ) ) {
  405. wfDebug( __METHOD__ . ": got cached lucene results for key $key\n" );
  406. wfProfileOut( __METHOD__ );
  407. return $resultSet;
  408. }
  409. }
  410. // Search server will be in local network but may not trigger checks on
  411. // Http::isLocal(), so suppress usage of $wgHTTPProxy if enabled.
  412. $httpOpts = array( 'proxy' => false );
  413. wfDebug( "Fetching search data from $searchUrl\n" );
  414. wfSuppressWarnings();
  415. $httpProfile = __METHOD__ . '-contact-' . $host;
  416. wfProfileIn( $httpProfile );
  417. $data = Http::get( $searchUrl, $wgLuceneSearchTimeout, $httpOpts);
  418. wfProfileOut( $httpProfile );
  419. wfRestoreWarnings();
  420. if( $data === false ) {
  421. // Network error or server error
  422. wfProfileOut( __METHOD__ );
  423. return null;
  424. } else {
  425. $inputLines = explode( "\n", trim( $data ) );
  426. $resultLines = array_map( 'trim', $inputLines );
  427. }
  428. $suggestion = null;
  429. $info = null;
  430. $interwiki = null;
  431. # All methods have same syntax...
  432. $totalHits = array_shift( $resultLines );
  433. if( $totalHits === false ) {
  434. # I/O error? this shouldn't happen
  435. wfDebug( "Couldn't read summary line...\n" );
  436. } else {
  437. $totalHits = intval( $totalHits );
  438. wfDebug( "total [$totalHits] hits\n" );
  439. if($wgLuceneSearchVersion >= 2.1){
  440. # second line is info
  441. list($dummy,$info) = explode(' ',array_shift($resultLines),2);
  442. # third line is suggestions
  443. $s = array_shift($resultLines);
  444. if(self::startsWith($s,'#suggest '))
  445. $suggestion = $s;
  446. # fifth line is interwiki info line
  447. $iwHeading = array_shift($resultLines);
  448. list($dummy,$iwCount,$iwTotal) = explode(' ',$iwHeading);
  449. if($iwCount>0){
  450. # pack interwiki lines into a separate result set
  451. $interwikiLen = 0;
  452. while(!self::startsWith($resultLines[$interwikiLen],"#results"))
  453. $interwikiLen++;
  454. $interwikiLines = array_splice($resultLines,0,$interwikiLen);
  455. $interwiki = new LuceneSearchSet( $method, $query, $interwikiLines, intval($iwCount), intval($iwTotal) );
  456. }
  457. # how many results we got
  458. list($dummy,$resultCount) = explode(" ",array_shift($resultLines));
  459. $resultCount = intval($resultCount);
  460. } else{
  461. $resultCount = count($resultLines);
  462. }
  463. }
  464. $resultSet = new LuceneSearchSet( $method, $query, $resultLines, $resultCount, $totalHits,
  465. $suggestion, $info, $interwiki );
  466. if($wgLuceneSearchCacheExpiry > 0){
  467. wfDebug( __METHOD__ . ": caching lucene results for key $key\n" );
  468. $wgMemc->add( $key, $resultSet, $wgLuceneSearchCacheExpiry );
  469. }
  470. wfProfileOut( __METHOD__ );
  471. return $resultSet;
  472. }
  473. static function startsWith($source, $prefix){
  474. return strncmp($source, $prefix, strlen($prefix)) == 0;
  475. }
  476. /**
  477. * Private constructor. Use LuceneSearchSet::newFromQuery().
  478. *
  479. * @param string $method
  480. * @param string $query
  481. * @param array $lines
  482. * @param int $resultCount
  483. * @param int $totalHits
  484. * @param string $suggestion
  485. * @param string $info
  486. * @access private
  487. */
  488. function __construct( $method, $query, $lines, $resultCount, $totalHits = null, $suggestion = null, $info = null, $interwiki = null ) {
  489. $this->mMethod = $method;
  490. $this->mQuery = $query;
  491. $this->mTotalHits = $totalHits;
  492. $this->mResults = $lines;
  493. $this->mResultCount = $resultCount;
  494. $this->mPos = 0;
  495. $this->mSuggestionQuery = null;
  496. $this->mSuggestionSnippet = '';
  497. $this->parseSuggestion($suggestion);
  498. $this->mInfo = $info;
  499. $this->mInterwiki = $interwiki;
  500. }
  501. /** Get suggestions from a suggestion result line */
  502. function parseSuggestion($suggestion){
  503. if( is_null($suggestion) )
  504. return;
  505. // parse split points and highlight changes
  506. list($dummy,$points,$sug) = explode(" ",$suggestion);
  507. $sug = urldecode($sug);
  508. $points = explode(",",substr($points,1,-1));
  509. array_unshift($points,0);
  510. $suggestText = "";
  511. for($i=1;$i<count($points);$i+=2){
  512. $suggestText .= htmlspecialchars(substr($sug,$points[$i-1],$points[$i]-$points[$i-1]));
  513. $suggestText .= '<em>'.htmlspecialchars(substr($sug,$points[$i],$points[$i+1]-$points[$i]))."</em>";
  514. }
  515. $suggestText .= htmlspecialchars(substr($sug,end($points)));
  516. $this->mSuggestionQuery = $this->replaceGenericPrefixes($sug);
  517. $this->mSuggestionSnippet = $this->replaceGenericPrefixes($suggestText);
  518. }
  519. /** replace prefixes like [2]: that are not in phrases */
  520. function replaceGenericPrefixes($text){
  521. $out = "";
  522. $phrases = explode('"',$text);
  523. for($i=0;$i<count($phrases);$i+=2){
  524. $out .= preg_replace_callback('/\[([0-9]+)\]:/', array($this,'genericPrefixCallback'), $phrases[$i]);
  525. if($i+1 < count($phrases))
  526. $out .= '"'.$phrases[$i+1].'"'; // phrase text
  527. }
  528. return $out;
  529. }
  530. function genericPrefixCallback($matches){
  531. global $wgContLang;
  532. return $wgContLang->getFormattedNsText($matches[1]).":";
  533. }
  534. function numRows() {
  535. return $this->mResultCount;
  536. }
  537. function termMatches() {
  538. $resq = preg_replace( "/\\[.*?\\]:/", " ", $this->mQuery ); # generic prefixes
  539. $resq = preg_replace( "/all:/", " ", $resq );
  540. // Fixme: this is ripped from SearchMySQL and probably kind of sucks,
  541. // but it handles quoted phrase searches more or less correctly.
  542. // Should encapsulate this stuff better.
  543. // FIXME: This doesn't handle parenthetical expressions.
  544. $regexes = array();
  545. $m = array();
  546. $lc = SearchEngine::legalSearchChars();
  547. if( preg_match_all( '/([-+<>~]?)(([' . $lc . ']+)(\*?)|"[^"]*")/',
  548. $resq, $m, PREG_SET_ORDER ) ) {
  549. foreach( $m as $terms ) {
  550. if( !empty( $terms[3] ) ) {
  551. // Match individual terms in result highlighting...
  552. $regexp = preg_quote( $terms[3], '/' );
  553. if( $terms[4] ) $regexp .= "[0-9A-Za-z_]+";
  554. } else {
  555. // Match the quoted term in result highlighting...
  556. $regexp = preg_quote( str_replace( '"', '', $terms[2] ), '/' );
  557. }
  558. $regexes[] = $regexp;
  559. }
  560. wfDebug( __METHOD__ . ': Match with /' . implode( '|', $regexes ) . "/\n" );
  561. } else {
  562. wfDebug( "Can't understand search query '{$resq}'\n" );
  563. }
  564. return $regexes;
  565. }
  566. /**
  567. * Stupid hack around PHP's limited lambda support
  568. * @access private
  569. */
  570. function regexQuote( $term ) {
  571. return preg_quote( $term, '/' );
  572. }
  573. function hasResults() {
  574. return count( $this->mResults ) > 0;
  575. }
  576. /**
  577. * Some search modes return a total hit count for the query
  578. * in the entire article database. This may include pages
  579. * in namespaces that would not be matched on the given
  580. * settings.
  581. *
  582. * @return int
  583. * @access public
  584. */
  585. function getTotalHits() {
  586. return $this->mTotalHits;
  587. }
  588. /**
  589. * Return information about how and from where the results were fetched,
  590. * should be useful for diagnostics and debugging
  591. *
  592. * @return string
  593. */
  594. function getInfo() {
  595. if( is_null($this->mInfo) )
  596. return null;
  597. return "Search results fetched via ".$this->mInfo;
  598. }
  599. /**
  600. * Return a result set of hits on other (multiple) wikis associated with this one
  601. *
  602. * @return SearchResultSet
  603. */
  604. function getInterwikiResults() {
  605. return $this->mInterwiki;
  606. }
  607. /**
  608. * Some search modes return a suggested alternate term if there are
  609. * no exact hits. Returns true if there is one on this set.
  610. *
  611. * @return bool
  612. * @access public
  613. */
  614. function hasSuggestion() {
  615. return is_string( $this->mSuggestionQuery ) && $this->mSuggestionQuery != '';
  616. }
  617. function getSuggestionQuery(){
  618. return $this->mSuggestionQuery;
  619. }
  620. function getSuggestionSnippet(){
  621. return $this->mSuggestionSnippet;
  622. }
  623. /**
  624. * Fetches next search result, or false.
  625. * @return LuceneResult
  626. * @access public
  627. * @abstract
  628. */
  629. function next() {
  630. # Group together lines belonging to one hit
  631. $group = array();
  632. for(;$this->mPos < count($this->mResults);$this->mPos++){
  633. $l = trim($this->mResults[$this->mPos]);
  634. if(count($group) == 0) // main line
  635. $group['result'] = $l;
  636. elseif($l[0] == '#'){ // additional meta
  637. list($meta,$value) = explode(" ",$l,2);
  638. $group[$meta][] = $value;
  639. } else
  640. break;
  641. }
  642. if($group == false)
  643. return false;
  644. else
  645. return new LuceneResult( $group, $this->mMethod );
  646. }
  647. }