1*0b1bbbbbSAndreas Gohr<?php 2*0b1bbbbbSAndreas Gohr 3*0b1bbbbbSAndreas Gohrnamespace dokuwiki\Search\Query; 4*0b1bbbbbSAndreas Gohr 5*0b1bbbbbSAndreas Gohruse dokuwiki\Search\Tokenizer; 6*0b1bbbbbSAndreas Gohruse dokuwiki\Utf8; 7*0b1bbbbbSAndreas Gohr 8*0b1bbbbbSAndreas Gohr/** 9*0b1bbbbbSAndreas Gohr * DokuWuki QueryParser class 10*0b1bbbbbSAndreas Gohr */ 11*0b1bbbbbSAndreas Gohrclass QueryParser 12*0b1bbbbbSAndreas Gohr{ 13*0b1bbbbbSAndreas Gohr /** 14*0b1bbbbbSAndreas Gohr * Transforms given search term into intermediate representation 15*0b1bbbbbSAndreas Gohr * 16*0b1bbbbbSAndreas Gohr * This function is used in QueryParser::convert() and not for general purpose use. 17*0b1bbbbbSAndreas Gohr * 18*0b1bbbbbSAndreas Gohr * @author Kazutaka Miyasaka <kazmiya@gmail.com> 19*0b1bbbbbSAndreas Gohr * 20*0b1bbbbbSAndreas Gohr * @param string $term 21*0b1bbbbbSAndreas Gohr * @param bool $consider_asian 22*0b1bbbbbSAndreas Gohr * @param bool $phrase_mode 23*0b1bbbbbSAndreas Gohr * @return string 24*0b1bbbbbSAndreas Gohr */ 25*0b1bbbbbSAndreas Gohr public function termParser($term, $consider_asian = true, $phrase_mode = false) 26*0b1bbbbbSAndreas Gohr { 27*0b1bbbbbSAndreas Gohr $parsed = ''; 28*0b1bbbbbSAndreas Gohr if ($consider_asian) { 29*0b1bbbbbSAndreas Gohr // successive asian characters need to be searched as a phrase 30*0b1bbbbbSAndreas Gohr $words = Utf8\Asian::splitAsianWords($term); 31*0b1bbbbbSAndreas Gohr foreach ($words as $word) { 32*0b1bbbbbSAndreas Gohr $phrase_mode = $phrase_mode ? true : Utf8\Asian::isAsianWords($word); 33*0b1bbbbbSAndreas Gohr $parsed .= $this->termParser($word, false, $phrase_mode); 34*0b1bbbbbSAndreas Gohr } 35*0b1bbbbbSAndreas Gohr } else { 36*0b1bbbbbSAndreas Gohr $term_noparen = str_replace(['(',')'], ' ', $term); 37*0b1bbbbbSAndreas Gohr $words = Tokenizer::getWords($term_noparen, true); 38*0b1bbbbbSAndreas Gohr 39*0b1bbbbbSAndreas Gohr // W_: no need to highlight 40*0b1bbbbbSAndreas Gohr if (empty($words)) { 41*0b1bbbbbSAndreas Gohr $parsed = '()'; // important: do not remove 42*0b1bbbbbSAndreas Gohr } elseif ($words[0] === $term) { 43*0b1bbbbbSAndreas Gohr $parsed = '(W+:'.$words[0].')'; 44*0b1bbbbbSAndreas Gohr } elseif ($phrase_mode) { 45*0b1bbbbbSAndreas Gohr $term_encoded = str_replace(['(',')'], ['OP','CP'], $term); 46*0b1bbbbbSAndreas Gohr $parsed = '((W_:'.implode(')(W_:', $words).')(P+:'.$term_encoded.'))'; 47*0b1bbbbbSAndreas Gohr } else { 48*0b1bbbbbSAndreas Gohr $parsed = '((W+:'.implode(')(W+:', $words).'))'; 49*0b1bbbbbSAndreas Gohr } 50*0b1bbbbbSAndreas Gohr } 51*0b1bbbbbSAndreas Gohr return $parsed; 52*0b1bbbbbSAndreas Gohr } 53*0b1bbbbbSAndreas Gohr 54*0b1bbbbbSAndreas Gohr /** 55*0b1bbbbbSAndreas Gohr * Parses a search query and builds an array of search formulas 56*0b1bbbbbSAndreas Gohr * 57*0b1bbbbbSAndreas Gohr * @author Andreas Gohr <andi@splitbrain.org> 58*0b1bbbbbSAndreas Gohr * @author Kazutaka Miyasaka <kazmiya@gmail.com> 59*0b1bbbbbSAndreas Gohr * 60*0b1bbbbbSAndreas Gohr * @param string $query search query 61*0b1bbbbbSAndreas Gohr * @return array of search formulas 62*0b1bbbbbSAndreas Gohr */ 63*0b1bbbbbSAndreas Gohr public function convert($query) 64*0b1bbbbbSAndreas Gohr { 65*0b1bbbbbSAndreas Gohr /** 66*0b1bbbbbSAndreas Gohr * parse a search query and transform it into intermediate representation 67*0b1bbbbbSAndreas Gohr * 68*0b1bbbbbSAndreas Gohr * in a search query, you can use the following expressions: 69*0b1bbbbbSAndreas Gohr * 70*0b1bbbbbSAndreas Gohr * words: 71*0b1bbbbbSAndreas Gohr * include 72*0b1bbbbbSAndreas Gohr * -exclude 73*0b1bbbbbSAndreas Gohr * phrases: 74*0b1bbbbbSAndreas Gohr * "phrase to be included" 75*0b1bbbbbSAndreas Gohr * -"phrase you want to exclude" 76*0b1bbbbbSAndreas Gohr * namespaces: 77*0b1bbbbbSAndreas Gohr * @include:namespace (or ns:include:namespace) 78*0b1bbbbbSAndreas Gohr * ^exclude:namespace (or -ns:exclude:namespace) 79*0b1bbbbbSAndreas Gohr * groups: 80*0b1bbbbbSAndreas Gohr * () 81*0b1bbbbbSAndreas Gohr * -() 82*0b1bbbbbSAndreas Gohr * operators: 83*0b1bbbbbSAndreas Gohr * and ('and' is the default operator: you can always omit this) 84*0b1bbbbbSAndreas Gohr * or (or pipe symbol '|', lower precedence than 'and') 85*0b1bbbbbSAndreas Gohr * 86*0b1bbbbbSAndreas Gohr * e.g. a query [ aa "bb cc" @dd:ee ] means "search pages which contain 87*0b1bbbbbSAndreas Gohr * a word 'aa', a phrase 'bb cc' and are within a namespace 'dd:ee'". 88*0b1bbbbbSAndreas Gohr * this query is equivalent to [ -(-aa or -"bb cc" or -ns:dd:ee) ] 89*0b1bbbbbSAndreas Gohr * as long as you don't mind hit counts. 90*0b1bbbbbSAndreas Gohr * 91*0b1bbbbbSAndreas Gohr * intermediate representation consists of the following parts: 92*0b1bbbbbSAndreas Gohr * 93*0b1bbbbbSAndreas Gohr * ( ) - group 94*0b1bbbbbSAndreas Gohr * AND - logical and 95*0b1bbbbbSAndreas Gohr * OR - logical or 96*0b1bbbbbSAndreas Gohr * NOT - logical not 97*0b1bbbbbSAndreas Gohr * W+:, W-:, W_: - word (underscore: no need to highlight) 98*0b1bbbbbSAndreas Gohr * P+:, P-: - phrase (minus sign: logically in NOT group) 99*0b1bbbbbSAndreas Gohr * N+:, N-: - namespace 100*0b1bbbbbSAndreas Gohr */ 101*0b1bbbbbSAndreas Gohr $parsed_query = ''; 102*0b1bbbbbSAndreas Gohr $parens_level = 0; 103*0b1bbbbbSAndreas Gohr $terms = preg_split('/(-?".*?")/u', Utf8\PhpString::strtolower($query), 104*0b1bbbbbSAndreas Gohr -1, PREG_SPLIT_DELIM_CAPTURE | PREG_SPLIT_NO_EMPTY 105*0b1bbbbbSAndreas Gohr ); 106*0b1bbbbbSAndreas Gohr 107*0b1bbbbbSAndreas Gohr foreach ($terms as $term) { 108*0b1bbbbbSAndreas Gohr $parsed = ''; 109*0b1bbbbbSAndreas Gohr if (preg_match('/^(-?)"(.+)"$/u', $term, $matches)) { 110*0b1bbbbbSAndreas Gohr // phrase-include and phrase-exclude 111*0b1bbbbbSAndreas Gohr $not = $matches[1] ? 'NOT' : ''; 112*0b1bbbbbSAndreas Gohr $parsed = $not . $this->termParser($matches[2], false, true); 113*0b1bbbbbSAndreas Gohr } else { 114*0b1bbbbbSAndreas Gohr // fix incomplete phrase 115*0b1bbbbbSAndreas Gohr $term = str_replace('"', ' ', $term); 116*0b1bbbbbSAndreas Gohr 117*0b1bbbbbSAndreas Gohr // fix parentheses 118*0b1bbbbbSAndreas Gohr $term = str_replace(')' , ' ) ', $term); 119*0b1bbbbbSAndreas Gohr $term = str_replace('(' , ' ( ', $term); 120*0b1bbbbbSAndreas Gohr $term = str_replace('- (', ' -(', $term); 121*0b1bbbbbSAndreas Gohr 122*0b1bbbbbSAndreas Gohr // treat pipe symbols as 'OR' operators 123*0b1bbbbbSAndreas Gohr $term = str_replace('|', ' or ', $term); 124*0b1bbbbbSAndreas Gohr 125*0b1bbbbbSAndreas Gohr // treat ideographic spaces (U+3000) as search term separators 126*0b1bbbbbSAndreas Gohr // FIXME: some more separators? 127*0b1bbbbbSAndreas Gohr $term = preg_replace('/[ \x{3000}]+/u', ' ', $term); 128*0b1bbbbbSAndreas Gohr $term = trim($term); 129*0b1bbbbbSAndreas Gohr if ($term === '') continue; 130*0b1bbbbbSAndreas Gohr 131*0b1bbbbbSAndreas Gohr $tokens = explode(' ', $term); 132*0b1bbbbbSAndreas Gohr foreach ($tokens as $token) { 133*0b1bbbbbSAndreas Gohr if ($token === '(') { 134*0b1bbbbbSAndreas Gohr // parenthesis-include-open 135*0b1bbbbbSAndreas Gohr $parsed .= '('; 136*0b1bbbbbSAndreas Gohr ++$parens_level; 137*0b1bbbbbSAndreas Gohr } elseif ($token === '-(') { 138*0b1bbbbbSAndreas Gohr // parenthesis-exclude-open 139*0b1bbbbbSAndreas Gohr $parsed .= 'NOT('; 140*0b1bbbbbSAndreas Gohr ++$parens_level; 141*0b1bbbbbSAndreas Gohr } elseif ($token === ')') { 142*0b1bbbbbSAndreas Gohr // parenthesis-any-close 143*0b1bbbbbSAndreas Gohr if ($parens_level === 0) continue; 144*0b1bbbbbSAndreas Gohr $parsed .= ')'; 145*0b1bbbbbSAndreas Gohr $parens_level--; 146*0b1bbbbbSAndreas Gohr } elseif ($token === 'and') { 147*0b1bbbbbSAndreas Gohr // logical-and (do nothing) 148*0b1bbbbbSAndreas Gohr } elseif ($token === 'or') { 149*0b1bbbbbSAndreas Gohr // logical-or 150*0b1bbbbbSAndreas Gohr $parsed .= 'OR'; 151*0b1bbbbbSAndreas Gohr } elseif (preg_match('/^(?:\^|-ns:)(.+)$/u', $token, $matches)) { 152*0b1bbbbbSAndreas Gohr // namespace-exclude 153*0b1bbbbbSAndreas Gohr $parsed .= 'NOT(N+:'.$matches[1].')'; 154*0b1bbbbbSAndreas Gohr } elseif (preg_match('/^(?:@|ns:)(.+)$/u', $token, $matches)) { 155*0b1bbbbbSAndreas Gohr // namespace-include 156*0b1bbbbbSAndreas Gohr $parsed .= '(N+:'.$matches[1].')'; 157*0b1bbbbbSAndreas Gohr } elseif (preg_match('/^-(.+)$/', $token, $matches)) { 158*0b1bbbbbSAndreas Gohr // word-exclude 159*0b1bbbbbSAndreas Gohr $parsed .= 'NOT('.$this->termParser($matches[1]).')'; 160*0b1bbbbbSAndreas Gohr } else { 161*0b1bbbbbSAndreas Gohr // word-include 162*0b1bbbbbSAndreas Gohr $parsed .= $this->termParser($token); 163*0b1bbbbbSAndreas Gohr } 164*0b1bbbbbSAndreas Gohr } 165*0b1bbbbbSAndreas Gohr } 166*0b1bbbbbSAndreas Gohr $parsed_query .= $parsed; 167*0b1bbbbbSAndreas Gohr } 168*0b1bbbbbSAndreas Gohr 169*0b1bbbbbSAndreas Gohr // cleanup (very sensitive) 170*0b1bbbbbSAndreas Gohr $parsed_query .= str_repeat(')', $parens_level); 171*0b1bbbbbSAndreas Gohr do { 172*0b1bbbbbSAndreas Gohr $parsed_query_old = $parsed_query; 173*0b1bbbbbSAndreas Gohr $parsed_query = preg_replace('/(NOT)?\(\)/u', '', $parsed_query); 174*0b1bbbbbSAndreas Gohr } while ($parsed_query !== $parsed_query_old); 175*0b1bbbbbSAndreas Gohr $parsed_query = preg_replace('/(NOT|OR)+\)/u', ')' , $parsed_query); 176*0b1bbbbbSAndreas Gohr $parsed_query = preg_replace('/(OR)+/u' , 'OR' , $parsed_query); 177*0b1bbbbbSAndreas Gohr $parsed_query = preg_replace('/\(OR/u' , '(' , $parsed_query); 178*0b1bbbbbSAndreas Gohr $parsed_query = preg_replace('/^OR|OR$/u' , '' , $parsed_query); 179*0b1bbbbbSAndreas Gohr $parsed_query = preg_replace('/\)(NOT)?\(/u' , ')AND$1(', $parsed_query); 180*0b1bbbbbSAndreas Gohr 181*0b1bbbbbSAndreas Gohr // adjustment: make highlightings right 182*0b1bbbbbSAndreas Gohr $parens_level = 0; 183*0b1bbbbbSAndreas Gohr $notgrp_levels = array(); 184*0b1bbbbbSAndreas Gohr $parsed_query_new = ''; 185*0b1bbbbbSAndreas Gohr $tokens = preg_split('/(NOT\(|[()])/u', $parsed_query, 186*0b1bbbbbSAndreas Gohr -1, PREG_SPLIT_DELIM_CAPTURE | PREG_SPLIT_NO_EMPTY 187*0b1bbbbbSAndreas Gohr ); 188*0b1bbbbbSAndreas Gohr foreach ($tokens as $token) { 189*0b1bbbbbSAndreas Gohr if ($token === 'NOT(') { 190*0b1bbbbbSAndreas Gohr $notgrp_levels[] = ++$parens_level; 191*0b1bbbbbSAndreas Gohr } elseif ($token === '(') { 192*0b1bbbbbSAndreas Gohr ++$parens_level; 193*0b1bbbbbSAndreas Gohr } elseif ($token === ')') { 194*0b1bbbbbSAndreas Gohr if ($parens_level-- === end($notgrp_levels)) array_pop($notgrp_levels); 195*0b1bbbbbSAndreas Gohr } elseif (count($notgrp_levels) % 2 === 1) { 196*0b1bbbbbSAndreas Gohr // turn highlight-flag off if terms are logically in "NOT" group 197*0b1bbbbbSAndreas Gohr $token = preg_replace('/([WPN])\+\:/u', '$1-:', $token); 198*0b1bbbbbSAndreas Gohr } 199*0b1bbbbbSAndreas Gohr $parsed_query_new .= $token; 200*0b1bbbbbSAndreas Gohr } 201*0b1bbbbbSAndreas Gohr $parsed_query = $parsed_query_new; 202*0b1bbbbbSAndreas Gohr 203*0b1bbbbbSAndreas Gohr /** 204*0b1bbbbbSAndreas Gohr * convert infix notation string into postfix (Reverse Polish notation) array 205*0b1bbbbbSAndreas Gohr * by Shunting-yard algorithm 206*0b1bbbbbSAndreas Gohr * 207*0b1bbbbbSAndreas Gohr * see: http://en.wikipedia.org/wiki/Reverse_Polish_notation 208*0b1bbbbbSAndreas Gohr * see: http://en.wikipedia.org/wiki/Shunting-yard_algorithm 209*0b1bbbbbSAndreas Gohr */ 210*0b1bbbbbSAndreas Gohr $parsed_ary = array(); 211*0b1bbbbbSAndreas Gohr $ope_stack = array(); 212*0b1bbbbbSAndreas Gohr $ope_precedence = array(')' => 1, 'OR' => 2, 'AND' => 3, 'NOT' => 4, '(' => 5); 213*0b1bbbbbSAndreas Gohr $ope_regex = '/([()]|OR|AND|NOT)/u'; 214*0b1bbbbbSAndreas Gohr 215*0b1bbbbbSAndreas Gohr $tokens = preg_split($ope_regex, $parsed_query, 216*0b1bbbbbSAndreas Gohr -1, PREG_SPLIT_DELIM_CAPTURE | PREG_SPLIT_NO_EMPTY 217*0b1bbbbbSAndreas Gohr ); 218*0b1bbbbbSAndreas Gohr foreach ($tokens as $token) { 219*0b1bbbbbSAndreas Gohr if (preg_match($ope_regex, $token)) { 220*0b1bbbbbSAndreas Gohr // operator 221*0b1bbbbbSAndreas Gohr $last_ope = end($ope_stack); 222*0b1bbbbbSAndreas Gohr while ($last_ope !== false 223*0b1bbbbbSAndreas Gohr && $ope_precedence[$token] <= $ope_precedence[$last_ope] 224*0b1bbbbbSAndreas Gohr && $last_ope != '(' 225*0b1bbbbbSAndreas Gohr ) { 226*0b1bbbbbSAndreas Gohr $parsed_ary[] = array_pop($ope_stack); 227*0b1bbbbbSAndreas Gohr $last_ope = end($ope_stack); 228*0b1bbbbbSAndreas Gohr } 229*0b1bbbbbSAndreas Gohr if ($token == ')') { 230*0b1bbbbbSAndreas Gohr array_pop($ope_stack); // this array_pop always deletes '(' 231*0b1bbbbbSAndreas Gohr } else { 232*0b1bbbbbSAndreas Gohr $ope_stack[] = $token; 233*0b1bbbbbSAndreas Gohr } 234*0b1bbbbbSAndreas Gohr } else { 235*0b1bbbbbSAndreas Gohr // operand 236*0b1bbbbbSAndreas Gohr $token_decoded = str_replace(['OP','CP'], ['(',')'], $token); 237*0b1bbbbbSAndreas Gohr $parsed_ary[] = $token_decoded; 238*0b1bbbbbSAndreas Gohr } 239*0b1bbbbbSAndreas Gohr } 240*0b1bbbbbSAndreas Gohr $parsed_ary = array_values(array_merge($parsed_ary, array_reverse($ope_stack))); 241*0b1bbbbbSAndreas Gohr 242*0b1bbbbbSAndreas Gohr // cleanup: each double "NOT" in RPN array actually does nothing 243*0b1bbbbbSAndreas Gohr $parsed_ary_count = count($parsed_ary); 244*0b1bbbbbSAndreas Gohr for ($i = 1; $i < $parsed_ary_count; ++$i) { 245*0b1bbbbbSAndreas Gohr if ($parsed_ary[$i] === 'NOT' && $parsed_ary[$i - 1] === 'NOT') { 246*0b1bbbbbSAndreas Gohr unset($parsed_ary[$i], $parsed_ary[$i - 1]); 247*0b1bbbbbSAndreas Gohr } 248*0b1bbbbbSAndreas Gohr } 249*0b1bbbbbSAndreas Gohr $parsed_ary = array_values($parsed_ary); 250*0b1bbbbbSAndreas Gohr 251*0b1bbbbbSAndreas Gohr // build return value 252*0b1bbbbbSAndreas Gohr $q = array(); 253*0b1bbbbbSAndreas Gohr $q['query'] = $query; 254*0b1bbbbbSAndreas Gohr $q['parsed_str'] = $parsed_query; 255*0b1bbbbbSAndreas Gohr $q['parsed_ary'] = $parsed_ary; 256*0b1bbbbbSAndreas Gohr 257*0b1bbbbbSAndreas Gohr foreach ($q['parsed_ary'] as $token) { 258*0b1bbbbbSAndreas Gohr if ($token[2] !== ':') continue; 259*0b1bbbbbSAndreas Gohr $body = substr($token, 3); 260*0b1bbbbbSAndreas Gohr 261*0b1bbbbbSAndreas Gohr switch (substr($token, 0, 3)) { 262*0b1bbbbbSAndreas Gohr case 'N+:': 263*0b1bbbbbSAndreas Gohr $q['ns'][] = $body; // for backward compatibility 264*0b1bbbbbSAndreas Gohr break; 265*0b1bbbbbSAndreas Gohr case 'N-:': 266*0b1bbbbbSAndreas Gohr $q['notns'][] = $body; // for backward compatibility 267*0b1bbbbbSAndreas Gohr break; 268*0b1bbbbbSAndreas Gohr case 'W_:': 269*0b1bbbbbSAndreas Gohr $q['words'][] = $body; 270*0b1bbbbbSAndreas Gohr break; 271*0b1bbbbbSAndreas Gohr case 'W-:': 272*0b1bbbbbSAndreas Gohr $q['words'][] = $body; 273*0b1bbbbbSAndreas Gohr $q['not'][] = $body; // for backward compatibility 274*0b1bbbbbSAndreas Gohr break; 275*0b1bbbbbSAndreas Gohr case 'W+:': 276*0b1bbbbbSAndreas Gohr $q['words'][] = $body; 277*0b1bbbbbSAndreas Gohr $q['highlight'][] = $body; 278*0b1bbbbbSAndreas Gohr $q['and'][] = $body; // for backward compatibility 279*0b1bbbbbSAndreas Gohr break; 280*0b1bbbbbSAndreas Gohr case 'P-:': 281*0b1bbbbbSAndreas Gohr $q['phrases'][] = $body; 282*0b1bbbbbSAndreas Gohr break; 283*0b1bbbbbSAndreas Gohr case 'P+:': 284*0b1bbbbbSAndreas Gohr $q['phrases'][] = $body; 285*0b1bbbbbSAndreas Gohr $q['highlight'][] = $body; 286*0b1bbbbbSAndreas Gohr break; 287*0b1bbbbbSAndreas Gohr } 288*0b1bbbbbSAndreas Gohr } 289*0b1bbbbbSAndreas Gohr foreach (['words', 'phrases', 'highlight', 'ns', 'notns', 'and', 'not'] as $key) { 290*0b1bbbbbSAndreas Gohr $q[$key] = empty($q[$key]) ? array() : array_values(array_unique($q[$key])); 291*0b1bbbbbSAndreas Gohr } 292*0b1bbbbbSAndreas Gohr 293*0b1bbbbbSAndreas Gohr return $q; 294*0b1bbbbbSAndreas Gohr } 295*0b1bbbbbSAndreas Gohr 296*0b1bbbbbSAndreas Gohr /** 297*0b1bbbbbSAndreas Gohr * Recreate a search query string based on parsed parts, 298*0b1bbbbbSAndreas Gohr * doesn't support negated phrases and `OR` searches 299*0b1bbbbbSAndreas Gohr * 300*0b1bbbbbSAndreas Gohr * @param array $and 301*0b1bbbbbSAndreas Gohr * @param array $not 302*0b1bbbbbSAndreas Gohr * @param array $phrases 303*0b1bbbbbSAndreas Gohr * @param array $ns 304*0b1bbbbbSAndreas Gohr * @param array $notns 305*0b1bbbbbSAndreas Gohr * 306*0b1bbbbbSAndreas Gohr * @return string 307*0b1bbbbbSAndreas Gohr */ 308*0b1bbbbbSAndreas Gohr public function revert(array $and, array $not, array $phrases, array $ns, array $notns) 309*0b1bbbbbSAndreas Gohr { 310*0b1bbbbbSAndreas Gohr $query = implode(' ', $and); 311*0b1bbbbbSAndreas Gohr 312*0b1bbbbbSAndreas Gohr if (!empty($not)) { 313*0b1bbbbbSAndreas Gohr $query .= ' -' . implode(' -', $not); 314*0b1bbbbbSAndreas Gohr } 315*0b1bbbbbSAndreas Gohr if (!empty($phrases)) { 316*0b1bbbbbSAndreas Gohr $query .= ' "' . implode('" "', $phrases) . '"'; 317*0b1bbbbbSAndreas Gohr } 318*0b1bbbbbSAndreas Gohr if (!empty($ns)) { 319*0b1bbbbbSAndreas Gohr $query .= ' @' . implode(' @', $ns); 320*0b1bbbbbSAndreas Gohr } 321*0b1bbbbbSAndreas Gohr if (!empty($notns)) { 322*0b1bbbbbSAndreas Gohr $query .= ' ^' . implode(' ^', $notns); 323*0b1bbbbbSAndreas Gohr } 324*0b1bbbbbSAndreas Gohr return $query; 325*0b1bbbbbSAndreas Gohr } 326*0b1bbbbbSAndreas Gohr} 327