xref: /dokuwiki/inc/Search/Query/QueryParser.php (revision 0b1bbbbb7d4e3c531cd255dbf878ce27d5967a0c)
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