@recaptime-dev's working patches + fork for Phorge, a community fork of Phabricator. (Upstream dev and stable branches are at upstream/main and upstream/stable respectively.) hq.recaptime.dev/wiki/Phorge
phorge phabricator
1
fork

Configure Feed

Select the types of activity you want to include in your feed.

Show users how fulltext search queries are parsed and executed; don't query stopwords or short tokens

Summary:
Depends on D17670. Fixes T12137. Fixes T12003. Ref T2632.

This shows users a readout of which terms were actually searched for.

This also drops those terms from the query we submit to the backend, dodging the weird behaviors / search engine bugs in T12137.

This might need some design tweaking.

Test Plan: {F4899825}

Reviewers: chad

Reviewed By: chad

Maniphest Tasks: T12137, T12003, T2632

Differential Revision: https://secure.phabricator.com/D17672

+855 -22
+2 -2
resources/celerity/map.php
··· 103 103 'rsrc/css/application/releeph/releeph-request-differential-create-dialog.css' => '8d8b92cd', 104 104 'rsrc/css/application/releeph/releeph-request-typeahead.css' => '667a48ae', 105 105 'rsrc/css/application/search/application-search-view.css' => '66ee5d46', 106 - 'rsrc/css/application/search/search-results.css' => '64ad079a', 106 + 'rsrc/css/application/search/search-results.css' => 'f87d23ad', 107 107 'rsrc/css/application/slowvote/slowvote.css' => 'a94b7230', 108 108 'rsrc/css/application/tokens/tokens.css' => '3d0f239e', 109 109 'rsrc/css/application/uiexample/example.css' => '528b19de', ··· 794 794 'phabricator-phtize' => 'd254d646', 795 795 'phabricator-prefab' => '8d40ae75', 796 796 'phabricator-remarkup-css' => '17c0fb37', 797 - 'phabricator-search-results-css' => '64ad079a', 797 + 'phabricator-search-results-css' => 'f87d23ad', 798 798 'phabricator-shaped-request' => '7cbe244b', 799 799 'phabricator-slowvote-css' => 'a94b7230', 800 800 'phabricator-source-code-view-css' => '4383192f',
+543
resources/sql/stopwords_myisam.txt
··· 1 + a's 2 + able 3 + about 4 + above 5 + according 6 + accordingly 7 + across 8 + actually 9 + after 10 + afterwards 11 + again 12 + against 13 + ain't 14 + all 15 + allow 16 + allows 17 + almost 18 + alone 19 + along 20 + already 21 + also 22 + although 23 + always 24 + am 25 + among 26 + amongst 27 + an 28 + and 29 + another 30 + any 31 + anybody 32 + anyhow 33 + anyone 34 + anything 35 + anyway 36 + anyways 37 + anywhere 38 + apart 39 + appear 40 + appreciate 41 + appropriate 42 + are 43 + aren't 44 + around 45 + as 46 + aside 47 + ask 48 + asking 49 + associated 50 + at 51 + available 52 + away 53 + awfully 54 + be 55 + became 56 + because 57 + become 58 + becomes 59 + becoming 60 + been 61 + before 62 + beforehand 63 + behind 64 + being 65 + believe 66 + below 67 + beside 68 + besides 69 + best 70 + better 71 + between 72 + beyond 73 + both 74 + brief 75 + but 76 + by 77 + c'mon 78 + c's 79 + came 80 + can 81 + can't 82 + cannot 83 + cant 84 + cause 85 + causes 86 + certain 87 + certainly 88 + changes 89 + clearly 90 + co 91 + com 92 + come 93 + comes 94 + concerning 95 + consequently 96 + consider 97 + considering 98 + contain 99 + containing 100 + contains 101 + corresponding 102 + could 103 + couldn't 104 + course 105 + currently 106 + definitely 107 + described 108 + despite 109 + did 110 + didn't 111 + different 112 + do 113 + does 114 + doesn't 115 + doing 116 + don't 117 + done 118 + down 119 + downwards 120 + during 121 + each 122 + edu 123 + eg 124 + eight 125 + either 126 + else 127 + elsewhere 128 + enough 129 + entirely 130 + especially 131 + et 132 + etc 133 + even 134 + ever 135 + every 136 + everybody 137 + everyone 138 + everything 139 + everywhere 140 + ex 141 + exactly 142 + example 143 + except 144 + far 145 + few 146 + fifth 147 + first 148 + five 149 + followed 150 + following 151 + follows 152 + for 153 + former 154 + formerly 155 + forth 156 + four 157 + from 158 + further 159 + furthermore 160 + get 161 + gets 162 + getting 163 + given 164 + gives 165 + go 166 + goes 167 + going 168 + gone 169 + got 170 + gotten 171 + greetings 172 + had 173 + hadn't 174 + happens 175 + hardly 176 + has 177 + hasn't 178 + have 179 + haven't 180 + having 181 + he 182 + he's 183 + hello 184 + help 185 + hence 186 + her 187 + here 188 + here's 189 + hereafter 190 + hereby 191 + herein 192 + hereupon 193 + hers 194 + herself 195 + hi 196 + him 197 + himself 198 + his 199 + hither 200 + hopefully 201 + how 202 + howbeit 203 + however 204 + i'd 205 + i'll 206 + i'm 207 + i've 208 + ie 209 + if 210 + ignored 211 + immediate 212 + in 213 + inasmuch 214 + inc 215 + indeed 216 + indicate 217 + indicated 218 + indicates 219 + inner 220 + insofar 221 + instead 222 + into 223 + inward 224 + is 225 + isn't 226 + it 227 + it'd 228 + it'll 229 + it's 230 + its 231 + itself 232 + just 233 + keep 234 + keeps 235 + kept 236 + know 237 + known 238 + knows 239 + last 240 + lately 241 + later 242 + latter 243 + latterly 244 + least 245 + less 246 + lest 247 + let 248 + let's 249 + like 250 + liked 251 + likely 252 + little 253 + look 254 + looking 255 + looks 256 + ltd 257 + mainly 258 + many 259 + may 260 + maybe 261 + me 262 + mean 263 + meanwhile 264 + merely 265 + might 266 + more 267 + moreover 268 + most 269 + mostly 270 + much 271 + must 272 + my 273 + myself 274 + name 275 + namely 276 + nd 277 + near 278 + nearly 279 + necessary 280 + need 281 + needs 282 + neither 283 + never 284 + nevertheless 285 + new 286 + next 287 + nine 288 + no 289 + nobody 290 + non 291 + none 292 + noone 293 + nor 294 + normally 295 + not 296 + nothing 297 + novel 298 + now 299 + nowhere 300 + obviously 301 + of 302 + off 303 + often 304 + oh 305 + ok 306 + okay 307 + old 308 + on 309 + once 310 + one 311 + ones 312 + only 313 + onto 314 + or 315 + other 316 + others 317 + otherwise 318 + ought 319 + our 320 + ours 321 + ourselves 322 + out 323 + outside 324 + over 325 + overall 326 + own 327 + particular 328 + particularly 329 + per 330 + perhaps 331 + placed 332 + please 333 + plus 334 + possible 335 + presumably 336 + probably 337 + provides 338 + que 339 + quite 340 + qv 341 + rather 342 + rd 343 + re 344 + really 345 + reasonably 346 + regarding 347 + regardless 348 + regards 349 + relatively 350 + respectively 351 + right 352 + said 353 + same 354 + saw 355 + say 356 + saying 357 + says 358 + second 359 + secondly 360 + see 361 + seeing 362 + seem 363 + seemed 364 + seeming 365 + seems 366 + seen 367 + self 368 + selves 369 + sensible 370 + sent 371 + serious 372 + seriously 373 + seven 374 + several 375 + shall 376 + she 377 + should 378 + shouldn't 379 + since 380 + six 381 + so 382 + some 383 + somebody 384 + somehow 385 + someone 386 + something 387 + sometime 388 + sometimes 389 + somewhat 390 + somewhere 391 + soon 392 + sorry 393 + specified 394 + specify 395 + specifying 396 + still 397 + sub 398 + such 399 + sup 400 + sure 401 + t's 402 + take 403 + taken 404 + tell 405 + tends 406 + th 407 + than 408 + thank 409 + thanks 410 + thanx 411 + that 412 + that's 413 + thats 414 + the 415 + their 416 + theirs 417 + them 418 + themselves 419 + then 420 + thence 421 + there 422 + there's 423 + thereafter 424 + thereby 425 + therefore 426 + therein 427 + theres 428 + thereupon 429 + these 430 + they 431 + they'd 432 + they'll 433 + they're 434 + they've 435 + think 436 + third 437 + this 438 + thorough 439 + thoroughly 440 + those 441 + though 442 + three 443 + through 444 + throughout 445 + thru 446 + thus 447 + to 448 + together 449 + too 450 + took 451 + toward 452 + towards 453 + tried 454 + tries 455 + truly 456 + try 457 + trying 458 + twice 459 + two 460 + un 461 + under 462 + unfortunately 463 + unless 464 + unlikely 465 + until 466 + unto 467 + up 468 + upon 469 + us 470 + use 471 + used 472 + useful 473 + uses 474 + using 475 + usually 476 + value 477 + various 478 + very 479 + via 480 + viz 481 + vs 482 + want 483 + wants 484 + was 485 + wasn't 486 + way 487 + we 488 + we'd 489 + we'll 490 + we're 491 + we've 492 + welcome 493 + well 494 + went 495 + were 496 + weren't 497 + what 498 + what's 499 + whatever 500 + when 501 + whence 502 + whenever 503 + where 504 + where's 505 + whereafter 506 + whereas 507 + whereby 508 + wherein 509 + whereupon 510 + wherever 511 + whether 512 + which 513 + while 514 + whither 515 + who 516 + who's 517 + whoever 518 + whole 519 + whom 520 + whose 521 + why 522 + will 523 + willing 524 + wish 525 + with 526 + within 527 + without 528 + won't 529 + wonder 530 + would 531 + wouldn't 532 + yes 533 + yet 534 + you 535 + you'd 536 + you'll 537 + you're 538 + you've 539 + your 540 + yours 541 + yourself 542 + yourselves 543 + zero
+4
src/__phutil_library_map__.php
··· 2844 2844 'PhabricatorFulltextEngineExtensionModule' => 'applications/search/index/PhabricatorFulltextEngineExtensionModule.php', 2845 2845 'PhabricatorFulltextIndexEngineExtension' => 'applications/search/engineextension/PhabricatorFulltextIndexEngineExtension.php', 2846 2846 'PhabricatorFulltextInterface' => 'applications/search/interface/PhabricatorFulltextInterface.php', 2847 + 'PhabricatorFulltextResultSet' => 'applications/search/query/PhabricatorFulltextResultSet.php', 2847 2848 'PhabricatorFulltextStorageEngine' => 'applications/search/fulltextstorage/PhabricatorFulltextStorageEngine.php', 2849 + 'PhabricatorFulltextToken' => 'applications/search/query/PhabricatorFulltextToken.php', 2848 2850 'PhabricatorFundApplication' => 'applications/fund/application/PhabricatorFundApplication.php', 2849 2851 'PhabricatorGDSetupCheck' => 'applications/config/check/PhabricatorGDSetupCheck.php', 2850 2852 'PhabricatorGarbageCollector' => 'infrastructure/daemon/garbagecollector/PhabricatorGarbageCollector.php', ··· 8005 8007 'PhabricatorFulltextEngineExtension' => 'Phobject', 8006 8008 'PhabricatorFulltextEngineExtensionModule' => 'PhabricatorConfigModule', 8007 8009 'PhabricatorFulltextIndexEngineExtension' => 'PhabricatorIndexEngineExtension', 8010 + 'PhabricatorFulltextResultSet' => 'Phobject', 8008 8011 'PhabricatorFulltextStorageEngine' => 'Phobject', 8012 + 'PhabricatorFulltextToken' => 'Phobject', 8009 8013 'PhabricatorFundApplication' => 'PhabricatorApplication', 8010 8014 'PhabricatorGDSetupCheck' => 'PhabricatorSetupCheck', 8011 8015 'PhabricatorGarbageCollector' => 'Phobject',
+6
src/applications/search/engine/PhabricatorApplicationSearchEngine.php
··· 978 978 $objects = $query->executeWithCursorPager($pager); 979 979 } 980 980 981 + $this->didExecuteQuery($query); 982 + 981 983 return $objects; 984 + } 985 + 986 + protected function didExecuteQuery(PhabricatorPolicyAwareQuery $query) { 987 + return; 982 988 } 983 989 984 990
+4
src/applications/search/fulltextstorage/PhabricatorFulltextStorageEngine.php
··· 101 101 public function initIndex() {} 102 102 103 103 104 + public function getFulltextTokens() { 105 + return array(); 106 + } 107 + 104 108 }
+112 -16
src/applications/search/fulltextstorage/PhabricatorMySQLFulltextStorageEngine.php
··· 3 3 final class PhabricatorMySQLFulltextStorageEngine 4 4 extends PhabricatorFulltextStorageEngine { 5 5 6 + private $fulltextTokens = array(); 7 + private $engineLimits; 8 + 6 9 public function getEngineIdentifier() { 7 10 return 'mysql'; 8 11 } ··· 203 206 $title_field = PhabricatorSearchDocumentFieldType::FIELD_TITLE; 204 207 $title_boost = 1024; 205 208 209 + $stemmer = new PhutilSearchStemmer(); 210 + 206 211 $raw_query = $query->getParameter('query'); 207 - $compiled_query = $this->compileQuery($raw_query); 212 + $raw_query = trim($raw_query); 213 + if (strlen($raw_query)) { 214 + $compiler = PhabricatorSearchDocument::newQueryCompiler() 215 + ->setStemmer($stemmer); 216 + 217 + $tokens = $compiler->newTokens($raw_query); 218 + 219 + list($min_length, $stopword_list) = $this->getEngineLimits($conn); 220 + 221 + // Process all the parts of the user's query so we can show them which 222 + // parts we searched for and which ones we ignored. 223 + $fulltext_tokens = array(); 224 + foreach ($tokens as $key => $token) { 225 + $fulltext_token = id(new PhabricatorFulltextToken()) 226 + ->setToken($token); 227 + 228 + $fulltext_tokens[$key] = $fulltext_token; 229 + 230 + $value = $token->getValue(); 231 + if (phutil_utf8_strlen($value) < $min_length) { 232 + $fulltext_token->setIsShort(true); 233 + continue; 234 + } 235 + 236 + if (isset($stopword_list[phutil_utf8_strtolower($value)])) { 237 + $fulltext_token->setIsStopword(true); 238 + continue; 239 + } 240 + } 241 + $this->fulltextTokens = $fulltext_tokens; 242 + 243 + // Remove tokens which aren't queryable from the query. This is mostly 244 + // a workaround for the peculiar behaviors described in T12137. 245 + foreach ($this->fulltextTokens as $key => $fulltext_token) { 246 + if (!$fulltext_token->isQueryable()) { 247 + unset($tokens[$key]); 248 + } 249 + } 250 + 251 + if (!$tokens) { 252 + throw new PhutilSearchQueryCompilerSyntaxException( 253 + pht( 254 + 'All of your search terms are too short or too common to '. 255 + 'appear in the search index. Search for longer or more '. 256 + 'distinctive terms.')); 257 + } 258 + 259 + $queries = array(); 260 + $queries[] = $compiler->compileLiteralQuery($tokens); 261 + $queries[] = $compiler->compileStemmedQuery($tokens); 262 + $compiled_query = implode(' ', array_filter($queries)); 263 + } else { 264 + $compiled_query = null; 265 + } 266 + 208 267 if (strlen($compiled_query)) { 209 268 $select[] = qsprintf( 210 269 $conn, ··· 394 453 return $sql; 395 454 } 396 455 397 - private function compileQuery($raw_query) { 398 - $stemmer = new PhutilSearchStemmer(); 399 - 400 - $compiler = PhabricatorSearchDocument::newQueryCompiler() 401 - ->setStemmer($stemmer); 402 - 403 - $tokens = $compiler->newTokens($raw_query); 404 - 405 - $queries = array(); 406 - $queries[] = $compiler->compileLiteralQuery($tokens); 407 - $queries[] = $compiler->compileStemmedQuery($tokens); 408 - 409 - return implode(' ', array_filter($queries)); 410 - } 411 - 412 456 public function indexExists() { 413 457 return true; 414 458 } 415 459 416 460 public function getIndexStats() { 417 461 return false; 462 + } 463 + 464 + public function getFulltextTokens() { 465 + return $this->fulltextTokens; 466 + } 467 + 468 + private function getEngineLimits(AphrontDatabaseConnection $conn) { 469 + if ($this->engineLimits === null) { 470 + $this->engineLimits = $this->newEngineLimits($conn); 471 + } 472 + return $this->engineLimits; 473 + } 474 + 475 + private function newEngineLimits(AphrontDatabaseConnection $conn) { 476 + $result = queryfx_one( 477 + $conn, 478 + 'SELECT 479 + @@innodb_ft_min_token_size innodb_max, 480 + @@ft_min_word_len myisam_max, 481 + @@ft_stopword_file myisam_stopwords'); 482 + 483 + if ($result['innodb_max']) { 484 + $min_len = $result['innodb_max']; 485 + $stopwords = queryfx_all( 486 + $conn, 487 + 'SELECT * FROM INFORMATION_SCHEMA.INNODB_FT_DEFAULT_STOPWORD'); 488 + $stopwords = ipull($stopwords, 'value'); 489 + $stopwords = array_fuse($stopwords); 490 + } else { 491 + $min_len = $result['myisam_max']; 492 + 493 + $file = $result['myisam_stopwords']; 494 + if (preg_match('(/resources/sql/stopwords\.txt\z)', $file)) { 495 + // If this is set to something that looks like the Phabricator 496 + // stopword file, read that. 497 + $file = 'stopwords.txt'; 498 + } else { 499 + // Otherwise, just use the default stopwords. This might be wrong 500 + // but we can't read the actual value dynamically and reading 501 + // whatever file the variable is set to could be a big headache 502 + // to get right from a security perspective. 503 + $file = 'stopwords_myisam.txt'; 504 + } 505 + 506 + $root = dirname(phutil_get_library_root('phabricator')); 507 + $data = Filesystem::readFile($root.'/resources/sql/'.$file); 508 + $stopwords = explode("\n", $data); 509 + $stopwords = array_filter($stopwords); 510 + $stopwords = array_fuse($stopwords); 511 + } 512 + 513 + return array($min_len, $stopwords); 418 514 } 419 515 420 516 }
+26
src/applications/search/query/PhabricatorFulltextResultSet.php
··· 1 + <?php 2 + 3 + final class PhabricatorFulltextResultSet extends Phobject { 4 + 5 + private $phids; 6 + private $fulltextTokens; 7 + 8 + public function setPHIDs($phids) { 9 + $this->phids = $phids; 10 + return $this; 11 + } 12 + 13 + public function getPHIDs() { 14 + return $this->phids; 15 + } 16 + 17 + public function setFulltextTokens($fulltext_tokens) { 18 + $this->fulltextTokens = $fulltext_tokens; 19 + return $this; 20 + } 21 + 22 + public function getFulltextTokens() { 23 + return $this->fulltextTokens; 24 + } 25 + 26 + }
+88
src/applications/search/query/PhabricatorFulltextToken.php
··· 1 + <?php 2 + 3 + final class PhabricatorFulltextToken extends Phobject { 4 + 5 + private $token; 6 + private $isShort; 7 + private $isStopword; 8 + 9 + public function setToken(PhutilSearchQueryToken $token) { 10 + $this->token = $token; 11 + return $this; 12 + } 13 + 14 + public function getToken() { 15 + return $this->token; 16 + } 17 + 18 + public function isQueryable() { 19 + return !$this->getIsShort() && !$this->getIsStopword(); 20 + } 21 + 22 + public function setIsShort($is_short) { 23 + $this->isShort = $is_short; 24 + return $this; 25 + } 26 + 27 + public function getIsShort() { 28 + return $this->isShort; 29 + } 30 + 31 + public function setIsStopword($is_stopword) { 32 + $this->isStopword = $is_stopword; 33 + return $this; 34 + } 35 + 36 + public function getIsStopword() { 37 + return $this->isStopword; 38 + } 39 + 40 + public function newTag() { 41 + $token = $this->getToken(); 42 + 43 + $tip = null; 44 + $icon = null; 45 + 46 + if ($this->getIsShort()) { 47 + $shade = PHUITagView::COLOR_GREY; 48 + $tip = pht('Ignored Short Word'); 49 + } else if ($this->getIsStopword()) { 50 + $shade = PHUITagView::COLOR_GREY; 51 + $tip = pht('Ignored Common Word'); 52 + } else { 53 + $operator = $token->getOperator(); 54 + switch ($operator) { 55 + case PhutilSearchQueryCompiler::OPERATOR_NOT: 56 + $shade = PHUITagView::COLOR_RED; 57 + $icon = 'fa-minus'; 58 + break; 59 + default: 60 + $shade = PHUITagView::COLOR_BLUE; 61 + break; 62 + } 63 + } 64 + 65 + $tag = id(new PHUITagView()) 66 + ->setType(PHUITagView::TYPE_SHADE) 67 + ->setShade($shade) 68 + ->setName($token->getValue()); 69 + 70 + if ($tip !== null) { 71 + Javelin::initBehavior('phabricator-tooltips'); 72 + 73 + $tag 74 + ->addSigil('has-tooltip') 75 + ->setMetadata( 76 + array( 77 + 'tip' => $tip, 78 + )); 79 + } 80 + 81 + if ($icon !== null) { 82 + $tag->setIcon($icon); 83 + } 84 + 85 + return $tag; 86 + } 87 + 88 + }
+30
src/applications/search/query/PhabricatorSearchApplicationSearchEngine.php
··· 3 3 final class PhabricatorSearchApplicationSearchEngine 4 4 extends PhabricatorApplicationSearchEngine { 5 5 6 + private $resultSet; 7 + 6 8 public function getResultTypeDescription() { 7 9 return pht('Fulltext Search Results'); 8 10 } ··· 243 245 PhabricatorSavedQuery $query, 244 246 array $handles) { 245 247 248 + $result_set = $this->resultSet; 249 + $fulltext_tokens = $result_set->getFulltextTokens(); 250 + 246 251 $viewer = $this->requireViewer(); 247 252 $list = new PHUIObjectItemListView(); 248 253 $list->setNoDataString(pht('No results found.')); ··· 263 268 } 264 269 } 265 270 271 + $fulltext_view = null; 272 + if ($fulltext_tokens) { 273 + require_celerity_resource('phabricator-search-results-css'); 274 + 275 + $fulltext_view = array(); 276 + foreach ($fulltext_tokens as $token) { 277 + $fulltext_view[] = $token->newTag(); 278 + } 279 + $fulltext_view = phutil_tag( 280 + 'div', 281 + array( 282 + 'class' => 'phui-fulltext-tokens', 283 + ), 284 + array( 285 + pht('Searched For:'), 286 + ' ', 287 + $fulltext_view, 288 + )); 289 + } 290 + 266 291 $result = new PhabricatorApplicationSearchResultView(); 292 + $result->setContent($fulltext_view); 267 293 $result->setObjectList($list); 268 294 269 295 return $result; ··· 278 304 } 279 305 280 306 return $owner_phids; 307 + } 308 + 309 + protected function didExecuteQuery(PhabricatorPolicyAwareQuery $query) { 310 + $this->resultSet = $query->getFulltextResultSet(); 281 311 } 282 312 283 313 }
+13 -1
src/applications/search/query/PhabricatorSearchDocumentQuery.php
··· 6 6 private $savedQuery; 7 7 private $objectCapabilities; 8 8 private $unfilteredOffset; 9 + private $fulltextResultSet; 9 10 10 11 public function withSavedQuery(PhabricatorSavedQuery $query) { 11 12 $this->savedQuery = $query; ··· 25 26 return $this->getRequiredCapabilities(); 26 27 } 27 28 29 + public function getFulltextResultSet() { 30 + if (!$this->fulltextResultSet) { 31 + throw new PhutilInvalidStateException('execute'); 32 + } 33 + 34 + return $this->fulltextResultSet; 35 + } 36 + 28 37 protected function willExecute() { 29 38 $this->unfilteredOffset = 0; 39 + $this->fulltextResultSet = null; 30 40 } 31 41 32 42 protected function loadPage() { ··· 39 49 ->setParameter('offset', $this->unfilteredOffset) 40 50 ->setParameter('limit', $this->getRawResultLimit()); 41 51 42 - $phids = PhabricatorSearchService::executeSearch($query); 52 + $result_set = PhabricatorSearchService::newResultSet($query, $this); 53 + $phids = $result_set->getPHIDs(); 43 54 55 + $this->fulltextResultSet = $result_set; 44 56 $this->unfilteredOffset += count($phids); 45 57 46 58 $handles = id(new PhabricatorHandleQuery())
+14 -3
src/infrastructure/cluster/search/PhabricatorSearchService.php
··· 245 245 * @throws PhutilAggregateException 246 246 */ 247 247 public static function executeSearch(PhabricatorSavedQuery $query) { 248 + $result_set = self::newResultSet($query); 249 + return $result_set->getPHIDs(); 250 + } 251 + 252 + public static function newResultSet(PhabricatorSavedQuery $query) { 248 253 $exceptions = array(); 249 254 // try all services until one succeeds 250 255 foreach (self::getAllServices() as $service) { 256 + if (!$service->isReadable()) { 257 + continue; 258 + } 259 + 251 260 try { 252 261 $engine = $service->getEngine(); 253 - $res = $engine->executeSearch($query); 254 - // return immediately if we get results 255 - return $res; 262 + $phids = $engine->executeSearch($query); 263 + 264 + return id(new PhabricatorFulltextResultSet()) 265 + ->setPHIDs($phids) 266 + ->setFulltextTokens($engine->getFulltextTokens()); 256 267 } catch (PhutilSearchQueryCompilerSyntaxException $ex) { 257 268 // If there's a query compilation error, return it directly to the 258 269 // user: they issued a query with bad syntax.
+13
webroot/rsrc/css/application/search/search-results.css
··· 16 16 font-weight: normal; 17 17 color: #000; 18 18 } 19 + 20 + .phui-fulltext-tokens { 21 + margin: 16px 8px; 22 + font-weight: bold; 23 + } 24 + 25 + .phui-fulltext-tokens .phui-tag-view { 26 + margin: 0 2px; 27 + } 28 + 29 + .phui-fulltext-tokens .phui-tag-view.phui-tag-shade-grey { 30 + opacity: 0.5; 31 + }