xref: /dokuwiki/inc/Search/Query/QueryParser.php (revision 9369b4a991666bc911474806b106d8958e79f4c1)
10b1bbbbbSAndreas Gohr<?php
20b1bbbbbSAndreas Gohr
30b1bbbbbSAndreas Gohrnamespace dokuwiki\Search\Query;
40b1bbbbbSAndreas Gohr
50b1bbbbbSAndreas Gohruse dokuwiki\Search\Tokenizer;
6*9369b4a9SAndreas Gohruse dokuwiki\Utf8\Asian;
7*9369b4a9SAndreas Gohruse dokuwiki\Utf8\PhpString;
80b1bbbbbSAndreas Gohr
90b1bbbbbSAndreas Gohr/**
100b1bbbbbSAndreas Gohr * DokuWuki QueryParser class
110b1bbbbbSAndreas Gohr */
120b1bbbbbSAndreas Gohrclass QueryParser
130b1bbbbbSAndreas Gohr{
140b1bbbbbSAndreas Gohr    /**
150b1bbbbbSAndreas Gohr     * Transforms given search term into intermediate representation
160b1bbbbbSAndreas Gohr     *
170b1bbbbbSAndreas Gohr     * This function is used in QueryParser::convert() and not for general purpose use.
180b1bbbbbSAndreas Gohr     *
190b1bbbbbSAndreas Gohr     * @param string $term
200b1bbbbbSAndreas Gohr     * @param bool $consider_asian
210b1bbbbbSAndreas Gohr     * @param bool $phrase_mode
220b1bbbbbSAndreas Gohr     * @return string
23*9369b4a9SAndreas Gohr     * @author Kazutaka Miyasaka <kazmiya@gmail.com>
24*9369b4a9SAndreas Gohr     *
250b1bbbbbSAndreas Gohr     */
26*9369b4a9SAndreas Gohr    public function termParser(string $term, bool $consider_asian = true, bool $phrase_mode = false): string
270b1bbbbbSAndreas Gohr    {
280b1bbbbbSAndreas Gohr        $parsed = '';
290b1bbbbbSAndreas Gohr        if ($consider_asian) {
300b1bbbbbSAndreas Gohr            // successive asian characters need to be searched as a phrase
31*9369b4a9SAndreas Gohr            $words = Asian::splitAsianWords($term);
320b1bbbbbSAndreas Gohr            foreach ($words as $word) {
33*9369b4a9SAndreas Gohr                $phrase_mode = $phrase_mode || Asian::isAsianWords($word);
340b1bbbbbSAndreas Gohr                $parsed .= $this->termParser($word, false, $phrase_mode);
350b1bbbbbSAndreas Gohr            }
360b1bbbbbSAndreas Gohr        } else {
370b1bbbbbSAndreas Gohr            $term_noparen = str_replace(['(', ')'], ' ', $term);
380b1bbbbbSAndreas Gohr            $words = Tokenizer::getWords($term_noparen, true);
390b1bbbbbSAndreas Gohr
400b1bbbbbSAndreas Gohr            // W_: no need to highlight
41*9369b4a9SAndreas Gohr            if ($words === []) {
420b1bbbbbSAndreas Gohr                $parsed = '()'; // important: do not remove
430b1bbbbbSAndreas Gohr            } elseif ($words[0] === $term) {
440b1bbbbbSAndreas Gohr                $parsed = '(W+:' . $words[0] . ')';
450b1bbbbbSAndreas Gohr            } elseif ($phrase_mode) {
460b1bbbbbSAndreas Gohr                $term_encoded = str_replace(['(', ')'], ['OP', 'CP'], $term);
470b1bbbbbSAndreas Gohr                $parsed = '((W_:' . implode(')(W_:', $words) . ')(P+:' . $term_encoded . '))';
480b1bbbbbSAndreas Gohr            } else {
490b1bbbbbSAndreas Gohr                $parsed = '((W+:' . implode(')(W+:', $words) . '))';
500b1bbbbbSAndreas Gohr            }
510b1bbbbbSAndreas Gohr        }
520b1bbbbbSAndreas Gohr        return $parsed;
530b1bbbbbSAndreas Gohr    }
540b1bbbbbSAndreas Gohr
550b1bbbbbSAndreas Gohr    /**
560b1bbbbbSAndreas Gohr     * Parses a search query and builds an array of search formulas
570b1bbbbbSAndreas Gohr     *
58*9369b4a9SAndreas Gohr     * @param string $query search query
59*9369b4a9SAndreas Gohr     * @return array of search formulas
600b1bbbbbSAndreas Gohr     * @author Andreas Gohr <andi@splitbrain.org>
610b1bbbbbSAndreas Gohr     * @author Kazutaka Miyasaka <kazmiya@gmail.com>
620b1bbbbbSAndreas Gohr     *
630b1bbbbbSAndreas Gohr     */
64*9369b4a9SAndreas Gohr    public function convert(string $query): array
650b1bbbbbSAndreas Gohr    {
660b1bbbbbSAndreas Gohr        /**
670b1bbbbbSAndreas Gohr         * parse a search query and transform it into intermediate representation
680b1bbbbbSAndreas Gohr         *
690b1bbbbbSAndreas Gohr         * in a search query, you can use the following expressions:
700b1bbbbbSAndreas Gohr         *
710b1bbbbbSAndreas Gohr         *   words:
720b1bbbbbSAndreas Gohr         *     include
730b1bbbbbSAndreas Gohr         *     -exclude
740b1bbbbbSAndreas Gohr         *   phrases:
750b1bbbbbSAndreas Gohr         *     "phrase to be included"
760b1bbbbbSAndreas Gohr         *     -"phrase you want to exclude"
770b1bbbbbSAndreas Gohr         *   namespaces:
780b1bbbbbSAndreas Gohr         * @include:namespace (or ns:include:namespace)
790b1bbbbbSAndreas Gohr         *     ^exclude:namespace (or -ns:exclude:namespace)
800b1bbbbbSAndreas Gohr         *   groups:
810b1bbbbbSAndreas Gohr         *     ()
820b1bbbbbSAndreas Gohr         *     -()
830b1bbbbbSAndreas Gohr         *   operators:
840b1bbbbbSAndreas Gohr         *     and ('and' is the default operator: you can always omit this)
850b1bbbbbSAndreas Gohr         *     or  (or pipe symbol '|', lower precedence than 'and')
860b1bbbbbSAndreas Gohr         *
870b1bbbbbSAndreas Gohr         * e.g. a query [ aa "bb cc" @dd:ee ] means "search pages which contain
880b1bbbbbSAndreas Gohr         *      a word 'aa', a phrase 'bb cc' and are within a namespace 'dd:ee'".
890b1bbbbbSAndreas Gohr         *      this query is equivalent to [ -(-aa or -"bb cc" or -ns:dd:ee) ]
900b1bbbbbSAndreas Gohr         *      as long as you don't mind hit counts.
910b1bbbbbSAndreas Gohr         *
920b1bbbbbSAndreas Gohr         * intermediate representation consists of the following parts:
930b1bbbbbSAndreas Gohr         *
940b1bbbbbSAndreas Gohr         *   ( )           - group
950b1bbbbbSAndreas Gohr         *   AND           - logical and
960b1bbbbbSAndreas Gohr         *   OR            - logical or
970b1bbbbbSAndreas Gohr         *   NOT           - logical not
980b1bbbbbSAndreas Gohr         *   W+:, W-:, W_: - word      (underscore: no need to highlight)
990b1bbbbbSAndreas Gohr         *   P+:, P-:      - phrase    (minus sign: logically in NOT group)
1000b1bbbbbSAndreas Gohr         *   N+:, N-:      - namespace
1010b1bbbbbSAndreas Gohr         */
1020b1bbbbbSAndreas Gohr        $parsed_query = '';
1030b1bbbbbSAndreas Gohr        $parens_level = 0;
104*9369b4a9SAndreas Gohr        $terms = preg_split(
105*9369b4a9SAndreas Gohr            '/(-?".*?")/u',
106*9369b4a9SAndreas Gohr            PhpString::strtolower($query),
107*9369b4a9SAndreas Gohr            -1,
108*9369b4a9SAndreas Gohr            PREG_SPLIT_DELIM_CAPTURE | PREG_SPLIT_NO_EMPTY
1090b1bbbbbSAndreas Gohr        );
1100b1bbbbbSAndreas Gohr
1110b1bbbbbSAndreas Gohr        foreach ($terms as $term) {
1120b1bbbbbSAndreas Gohr            $parsed = '';
1130b1bbbbbSAndreas Gohr            if (preg_match('/^(-?)"(.+)"$/u', $term, $matches)) {
1140b1bbbbbSAndreas Gohr                // phrase-include and phrase-exclude
1150b1bbbbbSAndreas Gohr                $not = $matches[1] ? 'NOT' : '';
1160b1bbbbbSAndreas Gohr                $parsed = $not . $this->termParser($matches[2], false, true);
1170b1bbbbbSAndreas Gohr            } else {
1180b1bbbbbSAndreas Gohr                // fix incomplete phrase
1190b1bbbbbSAndreas Gohr                $term = str_replace('"', ' ', $term);
1200b1bbbbbSAndreas Gohr
1210b1bbbbbSAndreas Gohr                // fix parentheses
1220b1bbbbbSAndreas Gohr                $term = str_replace(')', ' ) ', $term);
1230b1bbbbbSAndreas Gohr                $term = str_replace('(', ' ( ', $term);
1240b1bbbbbSAndreas Gohr                $term = str_replace('- (', ' -(', $term);
1250b1bbbbbSAndreas Gohr
1260b1bbbbbSAndreas Gohr                // treat pipe symbols as 'OR' operators
1270b1bbbbbSAndreas Gohr                $term = str_replace('|', ' or ', $term);
1280b1bbbbbSAndreas Gohr
1290b1bbbbbSAndreas Gohr                // treat ideographic spaces (U+3000) as search term separators
1300b1bbbbbSAndreas Gohr                // FIXME: some more separators?
1310b1bbbbbSAndreas Gohr                $term = preg_replace('/[ \x{3000}]+/u', ' ', $term);
1320b1bbbbbSAndreas Gohr                $term = trim($term);
1330b1bbbbbSAndreas Gohr                if ($term === '') continue;
1340b1bbbbbSAndreas Gohr
1350b1bbbbbSAndreas Gohr                $tokens = explode(' ', $term);
1360b1bbbbbSAndreas Gohr                foreach ($tokens as $token) {
1370b1bbbbbSAndreas Gohr                    if ($token === '(') {
1380b1bbbbbSAndreas Gohr                        // parenthesis-include-open
1390b1bbbbbSAndreas Gohr                        $parsed .= '(';
1400b1bbbbbSAndreas Gohr                        ++$parens_level;
1410b1bbbbbSAndreas Gohr                    } elseif ($token === '-(') {
1420b1bbbbbSAndreas Gohr                        // parenthesis-exclude-open
1430b1bbbbbSAndreas Gohr                        $parsed .= 'NOT(';
1440b1bbbbbSAndreas Gohr                        ++$parens_level;
1450b1bbbbbSAndreas Gohr                    } elseif ($token === ')') {
1460b1bbbbbSAndreas Gohr                        // parenthesis-any-close
1470b1bbbbbSAndreas Gohr                        if ($parens_level === 0) continue;
1480b1bbbbbSAndreas Gohr                        $parsed .= ')';
1490b1bbbbbSAndreas Gohr                        $parens_level--;
1500b1bbbbbSAndreas Gohr                    } elseif ($token === 'and') {
1510b1bbbbbSAndreas Gohr                        // logical-and (do nothing)
1520b1bbbbbSAndreas Gohr                    } elseif ($token === 'or') {
1530b1bbbbbSAndreas Gohr                        // logical-or
1540b1bbbbbSAndreas Gohr                        $parsed .= 'OR';
1550b1bbbbbSAndreas Gohr                    } elseif (preg_match('/^(?:\^|-ns:)(.+)$/u', $token, $matches)) {
1560b1bbbbbSAndreas Gohr                        // namespace-exclude
1570b1bbbbbSAndreas Gohr                        $parsed .= 'NOT(N+:' . $matches[1] . ')';
1580b1bbbbbSAndreas Gohr                    } elseif (preg_match('/^(?:@|ns:)(.+)$/u', $token, $matches)) {
1590b1bbbbbSAndreas Gohr                        // namespace-include
1600b1bbbbbSAndreas Gohr                        $parsed .= '(N+:' . $matches[1] . ')';
1610b1bbbbbSAndreas Gohr                    } elseif (preg_match('/^-(.+)$/', $token, $matches)) {
1620b1bbbbbSAndreas Gohr                        // word-exclude
1630b1bbbbbSAndreas Gohr                        $parsed .= 'NOT(' . $this->termParser($matches[1]) . ')';
1640b1bbbbbSAndreas Gohr                    } else {
1650b1bbbbbSAndreas Gohr                        // word-include
1660b1bbbbbSAndreas Gohr                        $parsed .= $this->termParser($token);
1670b1bbbbbSAndreas Gohr                    }
1680b1bbbbbSAndreas Gohr                }
1690b1bbbbbSAndreas Gohr            }
1700b1bbbbbSAndreas Gohr            $parsed_query .= $parsed;
1710b1bbbbbSAndreas Gohr        }
1720b1bbbbbSAndreas Gohr
1730b1bbbbbSAndreas Gohr        // cleanup (very sensitive)
1740b1bbbbbSAndreas Gohr        $parsed_query .= str_repeat(')', $parens_level);
1750b1bbbbbSAndreas Gohr        do {
1760b1bbbbbSAndreas Gohr            $parsed_query_old = $parsed_query;
1770b1bbbbbSAndreas Gohr            $parsed_query = preg_replace('/(NOT)?\(\)/u', '', $parsed_query);
1780b1bbbbbSAndreas Gohr        } while ($parsed_query !== $parsed_query_old);
1790b1bbbbbSAndreas Gohr        $parsed_query = preg_replace('/(NOT|OR)+\)/u', ')', $parsed_query);
1800b1bbbbbSAndreas Gohr        $parsed_query = preg_replace('/(OR)+/u', 'OR', $parsed_query);
1810b1bbbbbSAndreas Gohr        $parsed_query = preg_replace('/\(OR/u', '(', $parsed_query);
1820b1bbbbbSAndreas Gohr        $parsed_query = preg_replace('/^OR|OR$/u', '', $parsed_query);
1830b1bbbbbSAndreas Gohr        $parsed_query = preg_replace('/\)(NOT)?\(/u', ')AND$1(', $parsed_query);
1840b1bbbbbSAndreas Gohr
1850b1bbbbbSAndreas Gohr        // adjustment: make highlightings right
1860b1bbbbbSAndreas Gohr        $parens_level = 0;
187*9369b4a9SAndreas Gohr        $notgrp_levels = [];
1880b1bbbbbSAndreas Gohr        $parsed_query_new = '';
189*9369b4a9SAndreas Gohr        $tokens = preg_split(
190*9369b4a9SAndreas Gohr            '/(NOT\(|[()])/u',
191*9369b4a9SAndreas Gohr            $parsed_query,
192*9369b4a9SAndreas Gohr            -1,
193*9369b4a9SAndreas Gohr            PREG_SPLIT_DELIM_CAPTURE | PREG_SPLIT_NO_EMPTY
1940b1bbbbbSAndreas Gohr        );
1950b1bbbbbSAndreas Gohr        foreach ($tokens as $token) {
1960b1bbbbbSAndreas Gohr            if ($token === 'NOT(') {
1970b1bbbbbSAndreas Gohr                $notgrp_levels[] = ++$parens_level;
1980b1bbbbbSAndreas Gohr            } elseif ($token === '(') {
1990b1bbbbbSAndreas Gohr                ++$parens_level;
2000b1bbbbbSAndreas Gohr            } elseif ($token === ')') {
2010b1bbbbbSAndreas Gohr                if ($parens_level-- === end($notgrp_levels)) array_pop($notgrp_levels);
2020b1bbbbbSAndreas Gohr            } elseif (count($notgrp_levels) % 2 === 1) {
2030b1bbbbbSAndreas Gohr                // turn highlight-flag off if terms are logically in "NOT" group
2040b1bbbbbSAndreas Gohr                $token = preg_replace('/([WPN])\+\:/u', '$1-:', $token);
2050b1bbbbbSAndreas Gohr            }
2060b1bbbbbSAndreas Gohr            $parsed_query_new .= $token;
2070b1bbbbbSAndreas Gohr        }
2080b1bbbbbSAndreas Gohr        $parsed_query = $parsed_query_new;
2090b1bbbbbSAndreas Gohr
2100b1bbbbbSAndreas Gohr        /**
2110b1bbbbbSAndreas Gohr         * convert infix notation string into postfix (Reverse Polish notation) array
2120b1bbbbbSAndreas Gohr         * by Shunting-yard algorithm
2130b1bbbbbSAndreas Gohr         *
2140b1bbbbbSAndreas Gohr         * see: http://en.wikipedia.org/wiki/Reverse_Polish_notation
2150b1bbbbbSAndreas Gohr         * see: http://en.wikipedia.org/wiki/Shunting-yard_algorithm
2160b1bbbbbSAndreas Gohr         */
217*9369b4a9SAndreas Gohr        $parsed_ary = [];
218*9369b4a9SAndreas Gohr        $ope_stack = [];
219*9369b4a9SAndreas Gohr        $ope_precedence = [')' => 1, 'OR' => 2, 'AND' => 3, 'NOT' => 4, '(' => 5];
2200b1bbbbbSAndreas Gohr        $ope_regex = '/([()]|OR|AND|NOT)/u';
2210b1bbbbbSAndreas Gohr
222*9369b4a9SAndreas Gohr        $tokens = preg_split(
223*9369b4a9SAndreas Gohr            $ope_regex,
224*9369b4a9SAndreas Gohr            $parsed_query,
225*9369b4a9SAndreas Gohr            -1,
226*9369b4a9SAndreas Gohr            PREG_SPLIT_DELIM_CAPTURE | PREG_SPLIT_NO_EMPTY
2270b1bbbbbSAndreas Gohr        );
2280b1bbbbbSAndreas Gohr        foreach ($tokens as $token) {
2290b1bbbbbSAndreas Gohr            if (preg_match($ope_regex, $token)) {
2300b1bbbbbSAndreas Gohr                // operator
2310b1bbbbbSAndreas Gohr                $last_ope = end($ope_stack);
232*9369b4a9SAndreas Gohr                while (
233*9369b4a9SAndreas Gohr                    $last_ope !== false
2340b1bbbbbSAndreas Gohr                    && $ope_precedence[$token] <= $ope_precedence[$last_ope]
2350b1bbbbbSAndreas Gohr                    && $last_ope != '('
2360b1bbbbbSAndreas Gohr                ) {
2370b1bbbbbSAndreas Gohr                    $parsed_ary[] = array_pop($ope_stack);
2380b1bbbbbSAndreas Gohr                    $last_ope = end($ope_stack);
2390b1bbbbbSAndreas Gohr                }
2400b1bbbbbSAndreas Gohr                if ($token == ')') {
2410b1bbbbbSAndreas Gohr                    array_pop($ope_stack); // this array_pop always deletes '('
2420b1bbbbbSAndreas Gohr                } else {
2430b1bbbbbSAndreas Gohr                    $ope_stack[] = $token;
2440b1bbbbbSAndreas Gohr                }
2450b1bbbbbSAndreas Gohr            } else {
2460b1bbbbbSAndreas Gohr                // operand
2470b1bbbbbSAndreas Gohr                $token_decoded = str_replace(['OP', 'CP'], ['(', ')'], $token);
2480b1bbbbbSAndreas Gohr                $parsed_ary[] = $token_decoded;
2490b1bbbbbSAndreas Gohr            }
2500b1bbbbbSAndreas Gohr        }
2510b1bbbbbSAndreas Gohr        $parsed_ary = array_values(array_merge($parsed_ary, array_reverse($ope_stack)));
2520b1bbbbbSAndreas Gohr
2530b1bbbbbSAndreas Gohr        // cleanup: each double "NOT" in RPN array actually does nothing
2540b1bbbbbSAndreas Gohr        $parsed_ary_count = count($parsed_ary);
2550b1bbbbbSAndreas Gohr        for ($i = 1; $i < $parsed_ary_count; ++$i) {
2560b1bbbbbSAndreas Gohr            if ($parsed_ary[$i] === 'NOT' && $parsed_ary[$i - 1] === 'NOT') {
2570b1bbbbbSAndreas Gohr                unset($parsed_ary[$i], $parsed_ary[$i - 1]);
2580b1bbbbbSAndreas Gohr            }
2590b1bbbbbSAndreas Gohr        }
2600b1bbbbbSAndreas Gohr        $parsed_ary = array_values($parsed_ary);
2610b1bbbbbSAndreas Gohr
2620b1bbbbbSAndreas Gohr        // build return value
263*9369b4a9SAndreas Gohr        $q = [];
2640b1bbbbbSAndreas Gohr        $q['query'] = $query;
2650b1bbbbbSAndreas Gohr        $q['parsed_str'] = $parsed_query;
2660b1bbbbbSAndreas Gohr        $q['parsed_ary'] = $parsed_ary;
2670b1bbbbbSAndreas Gohr
2680b1bbbbbSAndreas Gohr        foreach ($q['parsed_ary'] as $token) {
2690b1bbbbbSAndreas Gohr            if ($token[2] !== ':') continue;
2700b1bbbbbSAndreas Gohr            $body = substr($token, 3);
2710b1bbbbbSAndreas Gohr
2720b1bbbbbSAndreas Gohr            switch (substr($token, 0, 3)) {
2730b1bbbbbSAndreas Gohr                case 'N+:':
2740b1bbbbbSAndreas Gohr                    $q['ns'][] = $body; // for backward compatibility
2750b1bbbbbSAndreas Gohr                    break;
2760b1bbbbbSAndreas Gohr                case 'N-:':
2770b1bbbbbSAndreas Gohr                    $q['notns'][] = $body; // for backward compatibility
2780b1bbbbbSAndreas Gohr                    break;
2790b1bbbbbSAndreas Gohr                case 'W_:':
2800b1bbbbbSAndreas Gohr                    $q['words'][] = $body;
2810b1bbbbbSAndreas Gohr                    break;
2820b1bbbbbSAndreas Gohr                case 'W-:':
2830b1bbbbbSAndreas Gohr                    $q['words'][] = $body;
2840b1bbbbbSAndreas Gohr                    $q['not'][] = $body; // for backward compatibility
2850b1bbbbbSAndreas Gohr                    break;
2860b1bbbbbSAndreas Gohr                case 'W+:':
2870b1bbbbbSAndreas Gohr                    $q['words'][] = $body;
2880b1bbbbbSAndreas Gohr                    $q['highlight'][] = $body;
2890b1bbbbbSAndreas Gohr                    $q['and'][] = $body; // for backward compatibility
2900b1bbbbbSAndreas Gohr                    break;
2910b1bbbbbSAndreas Gohr                case 'P-:':
2920b1bbbbbSAndreas Gohr                    $q['phrases'][] = $body;
2930b1bbbbbSAndreas Gohr                    break;
2940b1bbbbbSAndreas Gohr                case 'P+:':
2950b1bbbbbSAndreas Gohr                    $q['phrases'][] = $body;
2960b1bbbbbSAndreas Gohr                    $q['highlight'][] = $body;
2970b1bbbbbSAndreas Gohr                    break;
2980b1bbbbbSAndreas Gohr            }
2990b1bbbbbSAndreas Gohr        }
3000b1bbbbbSAndreas Gohr        foreach (['words', 'phrases', 'highlight', 'ns', 'notns', 'and', 'not'] as $key) {
301*9369b4a9SAndreas Gohr            $q[$key] = empty($q[$key]) ? [] : array_values(array_unique($q[$key]));
3020b1bbbbbSAndreas Gohr        }
3030b1bbbbbSAndreas Gohr
3040b1bbbbbSAndreas Gohr        return $q;
3050b1bbbbbSAndreas Gohr    }
3060b1bbbbbSAndreas Gohr
3070b1bbbbbSAndreas Gohr    /**
3080b1bbbbbSAndreas Gohr     * Recreate a search query string based on parsed parts,
3090b1bbbbbSAndreas Gohr     * doesn't support negated phrases and `OR` searches
3100b1bbbbbSAndreas Gohr     *
3110b1bbbbbSAndreas Gohr     * @param array $and
3120b1bbbbbSAndreas Gohr     * @param array $not
3130b1bbbbbSAndreas Gohr     * @param array $phrases
3140b1bbbbbSAndreas Gohr     * @param array $ns
3150b1bbbbbSAndreas Gohr     * @param array $notns
3160b1bbbbbSAndreas Gohr     *
3170b1bbbbbSAndreas Gohr     * @return string
3180b1bbbbbSAndreas Gohr     */
3190b1bbbbbSAndreas Gohr    public function revert(array $and, array $not, array $phrases, array $ns, array $notns)
3200b1bbbbbSAndreas Gohr    {
3210b1bbbbbSAndreas Gohr        $query = implode(' ', $and);
3220b1bbbbbSAndreas Gohr
323*9369b4a9SAndreas Gohr        if ($not !== []) {
3240b1bbbbbSAndreas Gohr            $query .= ' -' . implode(' -', $not);
3250b1bbbbbSAndreas Gohr        }
326*9369b4a9SAndreas Gohr        if ($phrases !== []) {
3270b1bbbbbSAndreas Gohr            $query .= ' "' . implode('" "', $phrases) . '"';
3280b1bbbbbSAndreas Gohr        }
329*9369b4a9SAndreas Gohr        if ($ns !== []) {
3300b1bbbbbSAndreas Gohr            $query .= ' @' . implode(' @', $ns);
3310b1bbbbbSAndreas Gohr        }
332*9369b4a9SAndreas Gohr        if ($notns !== []) {
3330b1bbbbbSAndreas Gohr            $query .= ' ^' . implode(' ^', $notns);
3340b1bbbbbSAndreas Gohr        }
3350b1bbbbbSAndreas Gohr        return $query;
3360b1bbbbbSAndreas Gohr    }
3370b1bbbbbSAndreas Gohr}
338