Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
58.46% covered (warning)
58.46%
38 / 65
33.33% covered (danger)
33.33%
2 / 6
CRAP
0.00% covered (danger)
0.00%
0 / 1
Filters
58.46% covered (warning)
58.46%
38 / 65
33.33% covered (danger)
33.33%
2 / 6
74.45
0.00% covered (danger)
0.00%
0 / 1
 booleanOr
0.00% covered (danger)
0.00%
0 / 9
0.00% covered (danger)
0.00%
0 / 1
30
 unify
100.00% covered (success)
100.00%
26 / 26
100.00% covered (success)
100.00%
1 / 1
10
 unifyNonScript
100.00% covered (success)
100.00%
12 / 12
100.00% covered (success)
100.00%
1 / 1
6
 insource
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
2
 intitle
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
12
 insourceOrIntitle
0.00% covered (danger)
0.00%
0 / 10
0.00% covered (danger)
0.00%
0 / 1
2
1<?php
2
3namespace CirrusSearch\Search;
4
5use Elastica\Query\AbstractQuery;
6use Elastica\Query\BoolQuery;
7use Elastica\Query\MatchAll;
8
9/**
10 * Utilities for dealing with filters.
11 *
12 * This program is free software; you can redistribute it and/or modify
13 * it under the terms of the GNU General Public License as published by
14 * the Free Software Foundation; either version 2 of the License, or
15 * (at your option) any later version.
16 *
17 * This program is distributed in the hope that it will be useful,
18 * but WITHOUT ANY WARRANTY; without even the implied warranty of
19 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
20 * GNU General Public License for more details.
21 *
22 * You should have received a copy of the GNU General Public License along
23 * with this program; if not, write to the Free Software Foundation, Inc.,
24 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
25 * http://www.gnu.org/copyleft/gpl.html
26 */
27class Filters {
28    /**
29     * Turns a list of queries into a boolean OR, requiring only one
30     * of the provided queries to match.
31     *
32     * @param AbstractQuery[] $queries
33     * @param bool $matchAll When true (default) function never returns null,
34     *  when no queries are provided a MatchAll is returned.
35     * @return AbstractQuery|null The resulting OR query. Only returns null when
36     *  no queries are passed and $matchAll is false.
37     */
38    public static function booleanOr( array $queries, $matchAll = true ) {
39        if ( !$queries ) {
40            return $matchAll ? new MatchAll() : null;
41        } elseif ( count( $queries ) === 1 ) {
42            return reset( $queries );
43        } else {
44            $bool = new BoolQuery();
45            foreach ( $queries as $query ) {
46                $bool->addShould( $query );
47            }
48            $bool->setMinimumShouldMatch( 1 );
49            return $bool;
50        }
51    }
52
53    /**
54     * Merges lists of include/exclude filters into a single filter that
55     * Elasticsearch will execute efficiently.
56     *
57     * @param AbstractQuery[] $mustFilters filters that must match all returned documents
58     * @param AbstractQuery[] $mustNotFilters filters that must not match all returned documents
59     * @return null|AbstractQuery null if there are no filters or one that will execute
60     *     all of the provided filters
61     */
62    public static function unify( array $mustFilters, array $mustNotFilters ) {
63        // We want to make sure that we execute script filters last.  So we do these steps:
64        // 1.  Strip script filters from $must and $mustNot.
65        // 2.  Unify the non-script filters.
66        // 3.  Build a BoolAnd filter out of the script filters if there are any.
67        $scriptFilters = [];
68        $nonScriptMust = [];
69        $nonScriptMustNot = [];
70        foreach ( $mustFilters as $must ) {
71            if ( $must->hasParam( 'script' ) ) {
72                $scriptFilters[] = $must;
73            } else {
74                $nonScriptMust[] = $must;
75            }
76        }
77        $scriptMustNotFilter = new BoolQuery();
78        foreach ( $mustNotFilters as $mustNot ) {
79            if ( $mustNot->hasParam( 'script' ) ) {
80                $scriptMustNotFilter->addMustNot( $mustNot );
81            } else {
82                $nonScriptMustNot[] = $mustNot;
83            }
84        }
85        if ( $scriptMustNotFilter->hasParam( 'must_not' ) ) {
86            $scriptFilters[] = $scriptMustNotFilter;
87        }
88
89        $nonScript = self::unifyNonScript( $nonScriptMust, $nonScriptMustNot );
90        $scriptFiltersCount = count( $scriptFilters );
91        if ( $scriptFiltersCount === 0 ) {
92            return $nonScript;
93        }
94
95        $bool = new BoolQuery();
96        if ( $nonScript === null ) {
97            if ( $scriptFiltersCount === 1 ) {
98                return $scriptFilters[ 0 ];
99            }
100        } else {
101            $bool->addFilter( $nonScript );
102        }
103        foreach ( $scriptFilters as $scriptFilter ) {
104            $bool->addFilter( $scriptFilter );
105        }
106        return $bool;
107    }
108
109    /**
110     * Unify non-script filters into a single filter.
111     *
112     * @param AbstractQuery[] $mustFilters filters that must be found
113     * @param AbstractQuery[] $mustNotFilters filters that must not be found
114     * @return null|AbstractQuery null if there are no filters or one that will execute
115     *     all of the provided filters
116     */
117    private static function unifyNonScript( array $mustFilters, array $mustNotFilters ) {
118        $mustFilterCount = count( $mustFilters );
119        $mustNotFilterCount = count( $mustNotFilters );
120        if ( $mustFilterCount + $mustNotFilterCount === 0 ) {
121            return null;
122        }
123        if ( $mustFilterCount === 1 && $mustNotFilterCount == 0 ) {
124            return $mustFilters[ 0 ];
125        }
126        $bool = new BoolQuery();
127        foreach ( $mustFilters as $must ) {
128            $bool->addMust( $must );
129        }
130        foreach ( $mustNotFilters as $mustNot ) {
131            $bool->addMustNot( $mustNot );
132        }
133        return $bool;
134    }
135
136    /**
137     * Create a query for insource: queries. This function is pure, deferring
138     * state changes to the reference-updating return function.
139     *
140     * @param Escaper $escaper
141     * @param string $value
142     * @return AbstractQuery
143     */
144    public static function insource( Escaper $escaper, $value ) {
145        return self::insourceOrIntitle( $escaper, $value, static function () {
146            return [ 'source_text.plain' ];
147        } );
148    }
149
150    /**
151     * Create a query for intitle: queries.
152     *
153     * @param Escaper $escaper
154     * @param string $value
155     * @param bool $plain Only search plain fields
156     * @return AbstractQuery
157     */
158    public static function intitle( Escaper $escaper, $value, $plain = false ) {
159        return self::insourceOrIntitle( $escaper, $value, static function ( $queryString ) use ( $plain ) {
160            if ( $plain || preg_match( '/[?*]/u', $queryString ) ) {
161                return [ 'title.plain', 'redirect.title.plain' ];
162            } else {
163                return [ 'title', 'title.plain', 'redirect.title', 'redirect.title.plain' ];
164            }
165        } );
166    }
167
168    /**
169     * @param Escaper $escaper
170     * @param string $value
171     * @param callable $fieldF
172     * @return AbstractQuery
173     */
174    private static function insourceOrIntitle( Escaper $escaper, $value, $fieldF ) {
175        $queryString = $escaper->fixupWholeQueryString(
176            $escaper->fixupQueryStringPart( $value ) );
177        $field = $fieldF( $queryString );
178        $query = new \Elastica\Query\QueryString( $queryString );
179        $query->setFields( $field );
180        $query->setDefaultOperator( 'AND' );
181        $query->setAllowLeadingWildcard( $escaper->getAllowLeadingWildcard() );
182        $query->setFuzzyPrefixLength( 2 );
183        $query->setRewrite( 'top_terms_boost_1024' );
184
185        return $query;
186    }
187}