Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
94.00% covered (success)
94.00%
47 / 50
78.57% covered (warning)
78.57%
11 / 14
CRAP
0.00% covered (danger)
0.00%
0 / 1
BasicQueryClassifier
94.00% covered (success)
94.00%
47 / 50
78.57% covered (warning)
78.57%
11 / 14
34.25
0.00% covered (danger)
0.00%
0 / 1
 classify
100.00% covered (success)
100.00%
20 / 20
100.00% covered (success)
100.00%
1 / 1
14
 visitWordsQueryNode
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 visitPhraseQueryNode
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
4
 visitPhrasePrefixNode
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 visitFuzzyNode
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 visitPrefixNode
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 visitWildcardNode
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 visitEmptyQueryNode
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 visitKeywordFeatureNode
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 visitParsedBooleanNode
75.00% covered (warning)
75.00%
3 / 4
0.00% covered (danger)
0.00%
0 / 1
3.14
 visitBooleanClause
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
3
 visitNegatedNode
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 classes
100.00% covered (success)
100.00%
8 / 8
100.00% covered (success)
100.00%
1 / 1
1
 visitNamespaceHeader
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
1<?php
2
3namespace CirrusSearch\Parser;
4
5use CirrusSearch\Parser\AST\BooleanClause;
6use CirrusSearch\Parser\AST\EmptyQueryNode;
7use CirrusSearch\Parser\AST\FuzzyNode;
8use CirrusSearch\Parser\AST\KeywordFeatureNode;
9use CirrusSearch\Parser\AST\NamespaceHeaderNode;
10use CirrusSearch\Parser\AST\NegatedNode;
11use CirrusSearch\Parser\AST\ParsedBooleanNode;
12use CirrusSearch\Parser\AST\ParsedQuery;
13use CirrusSearch\Parser\AST\PhrasePrefixNode;
14use CirrusSearch\Parser\AST\PhraseQueryNode;
15use CirrusSearch\Parser\AST\PrefixNode;
16use CirrusSearch\Parser\AST\Visitor\Visitor;
17use CirrusSearch\Parser\AST\WildcardNode;
18use CirrusSearch\Parser\AST\WordsQueryNode;
19
20/**
21 * Basic classifier to identify queries like:
22 * - simple words: foo bar
23 * - simple phrase: "foo bar"
24 * - simple words + simple phrase; foo "bar baz"
25 * - complex: any queries that use a keyword, or any non trivial features
26 * - bogus queries: queries where a bogus pattern have been identified at
27 *      parse time
28 */
29class BasicQueryClassifier implements ParsedQueryClassifier, Visitor {
30
31    /**
32     * The simplest query ever: only words
33     */
34    public const SIMPLE_BAG_OF_WORDS = 'simple_bag_of_words';
35
36    /**
37     * Only quoted words
38     */
39    public const SIMPLE_PHRASE = 'simple_phrase_query';
40
41    /**
42     * A simple bag of words query with some quoted words
43     */
44    public const BAG_OF_WORDS_WITH_PHRASE = 'bag_of_words_with_phrase_query';
45
46    /**
47     * Expert: a query that uses some special syntax such as:
48     * - wildcards/fuzzy/word prefix
49     * - explicit boolean expression
50     * - complex phrase (phrase prefix, non default slop)
51     */
52    public const COMPLEX_QUERY = 'complex_query';
53
54    /**
55     * Query that was fixed/corrected
56     */
57    public const BOGUS_QUERY = 'bogus_query';
58
59    /**
60     * Query that is only a morelike
61     */
62    public const MORE_LIKE_ONLY = 'more_like_only';
63
64    private bool $hasWords;
65
66    private bool $hasSimplePhrase;
67
68    private bool $hasComplex;
69
70    private int $depth;
71
72    /**
73     * @var int
74     */
75    private int $maxDepth;
76
77    /**
78     * @param ParsedQuery $query
79     * @return string[]
80     */
81    public function classify( ParsedQuery $query ) {
82        $this->hasWords = false;
83        $this->hasSimplePhrase = false;
84        $this->hasComplex = false;
85        $this->depth = 0;
86        $this->maxDepth = 0;
87
88        $classes = [];
89        if ( $query->getParseWarnings() !== [] ) {
90            $classes[] = self::BOGUS_QUERY;
91        }
92
93        $query->getRoot()->accept( $this );
94
95        if ( $this->maxDepth === 0 && in_array( 'more_like', $query->getFeaturesUsed() ) ) {
96            $classes[] = self::MORE_LIKE_ONLY;
97        }
98        if ( $this->hasComplex ) {
99            $classes[] = self::COMPLEX_QUERY;
100        } elseif ( $this->maxDepth === 0 && $this->hasWords && !$this->hasSimplePhrase ) {
101            $classes[] = self::SIMPLE_BAG_OF_WORDS;
102        } elseif ( $this->maxDepth === 0 && !$this->hasWords && $this->hasSimplePhrase ) {
103            $classes[] = self::SIMPLE_PHRASE;
104        } elseif ( $this->maxDepth === 1 && $this->hasWords && $this->hasSimplePhrase ) {
105            $classes[] = self::BAG_OF_WORDS_WITH_PHRASE;
106        }
107
108        return $classes;
109    }
110
111    public function visitWordsQueryNode( WordsQueryNode $node ) {
112        $this->hasWords = true;
113    }
114
115    public function visitPhraseQueryNode( PhraseQueryNode $node ) {
116        if ( $node->isStem() || $node->getSlop() !== -1 ) {
117            $this->hasComplex = true;
118        } elseif ( !$node->isUnbalanced() ) {
119            $this->hasSimplePhrase = true;
120        }
121    }
122
123    public function visitPhrasePrefixNode( PhrasePrefixNode $node ) {
124        $this->hasComplex = true;
125    }
126
127    public function visitFuzzyNode( FuzzyNode $node ) {
128        $this->hasComplex = true;
129    }
130
131    public function visitPrefixNode( PrefixNode $node ) {
132        $this->hasComplex = true;
133    }
134
135    public function visitWildcardNode( WildcardNode $node ) {
136        $this->hasComplex = true;
137    }
138
139    public function visitEmptyQueryNode( EmptyQueryNode $node ) {
140    }
141
142    public function visitKeywordFeatureNode( KeywordFeatureNode $node ) {
143        $this->hasComplex = true;
144    }
145
146    public function visitParsedBooleanNode( ParsedBooleanNode $node ) {
147        if ( $this->hasComplex ) {
148            // we can quickly skip, this query cannot belong to this class
149            return;
150        }
151        foreach ( $node->getClauses() as $clause ) {
152            $clause->accept( $this );
153        }
154    }
155
156    public function visitBooleanClause( BooleanClause $clause ) {
157        $this->depth++;
158        $this->maxDepth = max( $this->depth, $this->maxDepth );
159        $this->hasComplex = $this->hasComplex || $clause->isExplicit() || $clause->getOccur() === BooleanClause::MUST_NOT;
160        $clause->getNode()->accept( $this );
161        $this->depth--;
162    }
163
164    public function visitNegatedNode( NegatedNode $node ) {
165        $this->hasComplex = true;
166    }
167
168    /**
169     * @return string[]
170     */
171    public function classes() {
172        return [
173            self::SIMPLE_BAG_OF_WORDS,
174            self::SIMPLE_PHRASE,
175            self::BAG_OF_WORDS_WITH_PHRASE,
176            self::COMPLEX_QUERY,
177            self::BOGUS_QUERY,
178            self::MORE_LIKE_ONLY,
179        ];
180    }
181
182    public function visitNamespaceHeader( NamespaceHeaderNode $node ) {
183    }
184}