Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
0.00% covered (danger)
0.00%
0 / 36
0.00% covered (danger)
0.00%
0 / 8
CRAP
0.00% covered (danger)
0.00%
0 / 1
NaiveSubphrasesSuggestionsBuilder
0.00% covered (danger)
0.00%
0 / 36
0.00% covered (danger)
0.00%
0 / 8
272
0.00% covered (danger)
0.00%
0 / 1
 __construct
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
2
 create
0.00% covered (danger)
0.00%
0 / 6
0.00% covered (danger)
0.00%
0 / 1
6
 getCharRange
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getExtraFields
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getRequiredFields
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 build
0.00% covered (danger)
0.00%
0 / 11
0.00% covered (danger)
0.00%
0 / 1
30
 splitTranslatedPage
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
12
 tokenize
0.00% covered (danger)
0.00%
0 / 9
0.00% covered (danger)
0.00%
0 / 1
6
1<?php
2
3namespace CirrusSearch\BuildDocument\Completion;
4
5/**
6 * Simple class for SuggestionsBuilder that needs to munge the title
7 * into a list of "subphrases" suggestions.
8 * Subphrases are only generated for title, redirects are not yet supported.
9 * A set of new fields is used to insert these suggestions 'suggest-extra'
10 * is used by default but can be overridden with string[] getExtraFields().
11 */
12class NaiveSubphrasesSuggestionsBuilder implements ExtraSuggestionsBuilder {
13    /** @const string */
14    private const LANG_FIELD = 'language';
15
16    /** @const int */
17    private const MAX_SUBPHRASES = 10;
18
19    /** @const string subpage type */
20    public const SUBPAGE_TYPE = 'subpage';
21
22    /** @const string subpage type */
23    public const STARTS_WITH_ANY_WORDS_TYPE = 'anywords';
24
25    /**
26     * @var string[] list of regex char ranges indexed by type
27     */
28    private static $RANGES_BY_TYPE = [
29        self::SUBPAGE_TYPE => '\/',
30        self::STARTS_WITH_ANY_WORDS_TYPE => '\/\s',
31    ];
32
33    /** @var int */
34    private $maxSubPhrases;
35
36    /**
37     * @var string regex character range, this value must be a valid char
38     * range and will be used to build a regular expression like
39     * '[' . $charRange . ']'
40     */
41    private $charRange;
42
43    /**
44     * @param string $charRange character range used to split subphrases
45     * @param int $maxSubPhrases defaults to MAX_SUBPHRASES
46     */
47    public function __construct( $charRange, $maxSubPhrases = self::MAX_SUBPHRASES ) {
48        $this->charRange = $charRange;
49        $this->maxSubPhrases = $maxSubPhrases;
50    }
51
52    public static function create( array $config ) {
53        $limit = $config['limit'] ?? self::MAX_SUBPHRASES;
54        if ( !isset( self::$RANGES_BY_TYPE[$config['type']] ) ) {
55            throw new \Exception( "Unsupported NaiveSubphrasesSuggestionsBuilder type " .
56                $config['type'] );
57        }
58        $cr = self::$RANGES_BY_TYPE[$config['type']];
59        return new self( $cr, $limit );
60    }
61
62    /**
63     * Get the char range used by this builder
64     * to split and generate subphrase suggestions
65     * @return string a valid regex char range that will be inserted inside
66     * square brackets.
67     */
68    protected function getCharRange() {
69        return $this->charRange;
70    }
71
72    /**
73     * List of FST fields where the subphrase suggestions
74     * will be added.
75     * @return string[]
76     */
77    protected function getExtraFields() {
78        return [ 'suggest-subphrases' ];
79    }
80
81    /**
82     * @inheritDoc
83     */
84    public function getRequiredFields() {
85        // This builder needs the language field
86        // to exclude subpages generated by the translate
87        // extension
88        return [ self::LANG_FIELD ];
89    }
90
91    /**
92     * @param mixed[] $inputDoc
93     * @param string $suggestType (title or redirect)
94     * @param int $score
95     * @param \Elastica\Document $suggestDoc suggestion type (title or redirect)
96     * @param int $targetNamespace
97     */
98    public function build( array $inputDoc, $suggestType, $score, \Elastica\Document $suggestDoc, $targetNamespace ) {
99        if ( $suggestType === SuggestBuilder::REDIRECT_SUGGESTION ) {
100            // It's unclear howto support redirects here.
101            // It seems hard to retrieve the best redirect if
102            // we destroy it with this builder. We would have to
103            // add a special code at search time and apply the
104            // same splitting strategy on retrieved redirects.
105            return;
106        }
107
108        $language = "";
109        if ( isset( $inputDoc[self::LANG_FIELD] ) ) {
110            $language = $inputDoc[self::LANG_FIELD];
111        }
112
113        $subPages = $this->tokenize( $inputDoc['title'], $language );
114        if ( $subPages ) {
115            $suggest = $suggestDoc->get( 'suggest' );
116            $suggest['input'] = $subPages;
117            foreach ( $this->getExtraFields() as $field ) {
118                $suggestDoc->set( $field, $suggest );
119            }
120        }
121    }
122
123    /**
124     * Split a translated page title into an array
125     * with the title at offset 0 and the language
126     * subpage at offset 1.
127     *
128     * e.g. splitTranslatedPage("Hello/en", "en")
129     *  - will output [ "Hello", "/en" ]
130     * e.g. splitTranslatedPage("Hello/test", "en")
131     *  - will output [ "Hello/test", "" ]
132     *
133     * @param string $title
134     * @param string $language
135     * @return string[]
136     */
137    public function splitTranslatedPage( $title, $language ) {
138        $langSubPage = '/' . $language;
139        if ( strlen( $langSubPage ) < strlen( $title ) &&
140            substr_compare( $title, $langSubPage, -strlen( $langSubPage ) ) == 0
141        ) {
142            return [ substr( $title, 0, -strlen( $langSubPage ) ), $langSubPage ];
143        } else {
144            return [ $title, "" ];
145        }
146    }
147
148    /**
149     * Tokenize the input $title by generating phrases suited
150     * for completion search.
151     * e.g. :
152     * $title = "Hello Beautifull Word/en";
153     * $builder->tokenize( $title, "en", "\\s" );
154     * will generate the following array:
155     *   [ "Beautifull Word/en", "Word/en" ]
156     *
157     * @param string $title
158     * @param string $language
159     * @return string[] tokenized phrasal suggestions
160     */
161    public function tokenize( $title, $language ) {
162        list( $title, $langSubPage ) = $this->splitTranslatedPage( $title, $language );
163
164        $cr = $this->getCharRange();
165        $matches = preg_split( "/[$cr]+/", $title, $this->maxSubPhrases + 1,
166            PREG_SPLIT_OFFSET_CAPTURE | PREG_SPLIT_NO_EMPTY );
167        // Remove the first one because it's the whole title
168        array_shift( $matches );
169        $subphrases = [];
170        foreach ( $matches as $m ) {
171            $subphrases[] = substr( $title, (int)$m[1] ) . $langSubPage;
172        }
173        return $subphrases;
174    }
175}