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 * @license GPL-2.0-or-later
13 */
14class Filters {
15    /**
16     * Turns a list of queries into a boolean OR, requiring only one
17     * of the provided queries to match.
18     *
19     * @param AbstractQuery[] $queries
20     * @param bool $matchAll When true (default) function never returns null,
21     *  when no queries are provided a MatchAll is returned.
22     * @return AbstractQuery|null The resulting OR query. Only returns null when
23     *  no queries are passed and $matchAll is false.
24     */
25    public static function booleanOr( array $queries, $matchAll = true ) {
26        if ( !$queries ) {
27            return $matchAll ? new MatchAll() : null;
28        } elseif ( count( $queries ) === 1 ) {
29            return reset( $queries );
30        } else {
31            $bool = new BoolQuery();
32            foreach ( $queries as $query ) {
33                $bool->addShould( $query );
34            }
35            $bool->setMinimumShouldMatch( 1 );
36            return $bool;
37        }
38    }
39
40    /**
41     * Merges lists of include/exclude filters into a single filter that
42     * Elasticsearch will execute efficiently.
43     *
44     * @param AbstractQuery[] $mustFilters filters that must match all returned documents
45     * @param AbstractQuery[] $mustNotFilters filters that must not match all returned documents
46     * @return null|AbstractQuery null if there are no filters or one that will execute
47     *     all of the provided filters
48     */
49    public static function unify( array $mustFilters, array $mustNotFilters ) {
50        // We want to make sure that we execute script filters last.  So we do these steps:
51        // 1.  Strip script filters from $must and $mustNot.
52        // 2.  Unify the non-script filters.
53        // 3.  Build a BoolAnd filter out of the script filters if there are any.
54        $scriptFilters = [];
55        $nonScriptMust = [];
56        $nonScriptMustNot = [];
57        foreach ( $mustFilters as $must ) {
58            if ( $must->hasParam( 'script' ) ) {
59                $scriptFilters[] = $must;
60            } else {
61                $nonScriptMust[] = $must;
62            }
63        }
64        $scriptMustNotFilter = new BoolQuery();
65        foreach ( $mustNotFilters as $mustNot ) {
66            if ( $mustNot->hasParam( 'script' ) ) {
67                $scriptMustNotFilter->addMustNot( $mustNot );
68            } else {
69                $nonScriptMustNot[] = $mustNot;
70            }
71        }
72        if ( $scriptMustNotFilter->hasParam( 'must_not' ) ) {
73            $scriptFilters[] = $scriptMustNotFilter;
74        }
75
76        $nonScript = self::unifyNonScript( $nonScriptMust, $nonScriptMustNot );
77        $scriptFiltersCount = count( $scriptFilters );
78        if ( $scriptFiltersCount === 0 ) {
79            return $nonScript;
80        }
81
82        $bool = new BoolQuery();
83        if ( $nonScript === null ) {
84            if ( $scriptFiltersCount === 1 ) {
85                return $scriptFilters[ 0 ];
86            }
87        } else {
88            $bool->addFilter( $nonScript );
89        }
90        foreach ( $scriptFilters as $scriptFilter ) {
91            $bool->addFilter( $scriptFilter );
92        }
93        return $bool;
94    }
95
96    /**
97     * Unify non-script filters into a single filter.
98     *
99     * @param AbstractQuery[] $mustFilters filters that must be found
100     * @param AbstractQuery[] $mustNotFilters filters that must not be found
101     * @return null|AbstractQuery null if there are no filters or one that will execute
102     *     all of the provided filters
103     */
104    private static function unifyNonScript( array $mustFilters, array $mustNotFilters ) {
105        $mustFilterCount = count( $mustFilters );
106        $mustNotFilterCount = count( $mustNotFilters );
107        if ( $mustFilterCount + $mustNotFilterCount === 0 ) {
108            return null;
109        }
110        if ( $mustFilterCount === 1 && $mustNotFilterCount == 0 ) {
111            return $mustFilters[ 0 ];
112        }
113        $bool = new BoolQuery();
114        foreach ( $mustFilters as $must ) {
115            $bool->addMust( $must );
116        }
117        foreach ( $mustNotFilters as $mustNot ) {
118            $bool->addMustNot( $mustNot );
119        }
120        return $bool;
121    }
122
123    /**
124     * Create a query for insource: queries. This function is pure, deferring
125     * state changes to the reference-updating return function.
126     *
127     * @param Escaper $escaper
128     * @param string $value
129     * @return AbstractQuery
130     */
131    public static function insource( Escaper $escaper, $value ) {
132        return self::insourceOrIntitle( $escaper, $value, static function () {
133            return [ 'source_text.plain' ];
134        } );
135    }
136
137    /**
138     * Create a query for intitle: queries.
139     *
140     * @param Escaper $escaper
141     * @param string $value
142     * @param bool $plain Only search plain fields
143     * @return AbstractQuery
144     */
145    public static function intitle( Escaper $escaper, $value, $plain = false ) {
146        return self::insourceOrIntitle( $escaper, $value, static function ( $queryString ) use ( $plain ) {
147            if ( $plain || preg_match( '/[?*]/u', $queryString ) ) {
148                return [ 'title.plain', 'redirect.title.plain' ];
149            } else {
150                return [ 'title', 'title.plain', 'redirect.title', 'redirect.title.plain' ];
151            }
152        } );
153    }
154
155    /**
156     * @param Escaper $escaper
157     * @param string $value
158     * @param callable $fieldF
159     * @return AbstractQuery
160     */
161    private static function insourceOrIntitle( Escaper $escaper, $value, $fieldF ) {
162        $queryString = $escaper->fixupWholeQueryString(
163            $escaper->fixupQueryStringPart( $value ) );
164        $field = $fieldF( $queryString );
165        $query = new \Elastica\Query\QueryString( $queryString );
166        $query->setFields( $field );
167        $query->setDefaultOperator( 'AND' );
168        $query->setAllowLeadingWildcard( $escaper->getAllowLeadingWildcard() );
169        $query->setFuzzyPrefixLength( 2 );
170        $query->setRewrite( 'top_terms_boost_1024' );
171
172        return $query;
173    }
174}