PluginProbe ʕ •ᴥ•ʔ
Jetpack – WP Security, Backup, Speed, & Growth / 16.1-beta.3
Jetpack – WP Security, Backup, Speed, & Growth v16.1-beta.3
16.2-a.3 16.1.2 16.2-a.1 16.1.1 16.1 16.1-beta 16.1-beta.2 16.1-beta.3 16.1-a.5 16.1-a.3 16.0.1 16.1-a.1 16.0 16.0-beta 16.0-a.7 16.0-a.5 15.9.1 16.0-a.3 16.0-a.1 15.9 15.9-beta 15.9-a.7 15.9-a.5 15.9-a.3 15.9-a.1 15.8 15.8-beta 15.8-a.7 15.8-a.5 5.2.5 5.3.4 5.4.4 5.5.5 5.6.5 5.7.5 5.8.4 5.9.4 6.0.4 6.1 6.1.1 6.1.2 6.1.3 6.1.4 6.1.5 6.2 6.2.1 6.2.2 6.2.3 6.2.4 6.2.5 6.3 6.3.1 6.3.2 6.3.3 6.3.4 6.3.5 6.3.6 6.3.7 6.4 6.4.1 6.4.2 6.4.3 6.4.4 6.4.5 6.4.6 6.5 6.5.1 6.5.2 6.5.3 6.5.4 6.6 6.6.1 6.6.2 6.6.3 6.6.4 6.6.5 6.7 6.7.1 6.7.2 6.7.3 6.7.4 6.8 6.8.1 6.8.2 6.8.3 6.8.4 6.8.5 6.9 6.9.1 6.9.2 6.9.3 6.9.4 7.0 7.0.1 7.0.2 7.0.3 7.0.4 7.0.5 7.1 7.1.1 7.1.2 7.1.3 7.1.4 7.1.5 7.2 7.2.1 7.2.1.1 7.2.2 7.2.3 7.2.4 7.2.5 7.3 7.3.0.1 7.3.1 7.3.1.1 7.3.2 7.3.3 7.3.4 7.3.5 7.4 7.4.1 7.4.2 7.4.3 7.4.4 7.4.5 7.5 7.5.0.1 7.5.1 7.5.2 7.5.3 7.5.4 7.5.5 7.5.6 7.5.7 7.6 7.6.1 7.6.2 7.6.3 7.6.4 7.7 7.7.1 7.7.2 7.7.3 7.7.4 7.7.5 7.7.6 7.8 7.8.1 7.8.2 7.8.3 7.8.4 7.9 7.9.1 7.9.2 7.9.3 7.9.4 8.0 8.0.1 8.0.2 8.0.3 8.1 8.1.1 8.1.2 8.1.3 8.1.4 8.2 8.2.0.1 8.2.1 8.2.2 8.2.3 8.2.4 8.2.5 8.2.6 8.3 8.3.1 8.3.2 8.3.3 8.4 8.4.1 8.4.2 8.4.3 8.4.4 8.4.5 8.5 8.5.1 8.5.2 8.5.3 8.6 8.6.1 8.6.2 8.6.3 8.6.4 8.7 8.7.0.1 8.7.1 8.7.2 8.7.3 8.7.4 8.8 8.8.1 8.8.2 8.8.3 8.8.4 8.8.5 8.9 8.9.1 8.9.2 8.9.3 8.9.4 9.0 9.0.1 9.0.2 9.0.3 9.0.4 9.0.5 9.1 9.1.1 9.1.2 9.1.3 9.2 9.2.1 9.2.2 9.2.3 9.2.4 9.3 9.3.1 9.3.2 9.3.3 9.3.4 9.3.5 9.4 9.4.1 9.4.2 9.4.3 9.4.4 9.5 9.5.1 9.5.2 9.5.3 9.5.4 9.5.5 9.6 9.6.1 9.6.2 9.6.3 9.6.4 9.7 9.7.1 9.7.2 15.7-beta.2 9.7.3 15.7.1 9.8 15.8-a.1 9.8.1 15.8-a.3 9.8.2 2.0.9 9.8.3 2.1.7 9.9 2.2.10 9.9.1 2.3.10 9.9.2 2.4.7 9.9.3 2.5.5 2.6.6 2.7.5 2.8.5 2.9.6 3.0.6 3.1.5 3.2.5 3.3.6 3.4.6 3.5.6 3.6.4 3.7.5 3.8.5 3.9.10 4.0.7 4.1.4 4.2.5 4.3.5 4.4.5 4.5.3 4.6.3 4.7.4 4.8.5 4.9.3 5.0.3 5.1.4 trunk 10.0 10.0.1 10.0.2 10.1 10.1.1 10.1.2 10.2 10.2.1 10.2.2 10.2.3 10.3 10.3.1 10.3.2 10.4 10.4.1 10.4.2 10.5 10.5.1 10.5.2 10.5.3 10.6 10.6.1 10.6.2 10.7 10.7.1 10.7.2 10.8 10.8.1 10.8.2 10.9 10.9.1 10.9.2 10.9.3 11.0 11.0.1 11.0.2 11.1 11.1.1 11.1.2 11.1.3 11.1.4 11.2 11.2.1 11.2.2 11.3 11.3.1 11.3.2 11.3.3 11.3.4 11.4 11.4.1 11.4.2 11.5 11.5.1 11.5.2 11.5.3 11.6 11.6.1 11.6.2 11.7 11.7.1 11.7.2 11.7.3 11.8 11.8.3 11.8.4 11.8.5 11.8.6 11.9 11.9.1 11.9.2 11.9.3 12.0 12.0.1 12.0.2 12.1 12.1.1 12.1.2 12.2 12.2.1 12.2.2 12.3 12.3.1 12.4 12.4.1 12.5 12.5.1 12.6 12.6.1 12.6.2 12.6.3 12.7 12.7.1 12.7.2 12.8 12.8.1 12.8.2 12.9 12.9.1 12.9.2 12.9.3 12.9.4 13.0 13.0.1 13.1 13.1.1 13.1.2 13.1.3 13.1.4 13.2 13.2.1 13.2.2 13.2.3 13.3 13.3.1 13.3.2 13.4 13.4.1 13.4.2 13.4.3 13.4.4 13.5 13.5.1 13.6 13.6.1 13.7 13.7.1 13.8 13.8.1 13.8.2 13.9 13.9.1 14.0 14.1 14.2 14.2.1 14.3 14.4 14.4.1 14.5 14.6 14.7 14.8 14.9 14.9.1 15.0 15.0.1 15.0.2 15.1 15.1.1 15.2 15.3 15.3.1 15.4 15.5 15.6 15.7 15.7-a.1 15.7-a.3 15.7-a.5 15.7-a.7 15.7-beta
jetpack / jetpack_vendor / automattic / jetpack-search / src / wpes / class-query-parser.php
jetpack / jetpack_vendor / automattic / jetpack-search / src / wpes Last commit date
class-query-builder.php 9 months ago class-query-parser.php 9 months ago
class-query-parser.php
720 lines
1 <?php
2 /**
3 * Parse a pure text query into WordPress Elasticsearch query. This builds on
4 * the Query_Builder() to provide search query parsing.
5 *
6 * The key part of this parser is taking a user's query string typed into a box
7 * and converting it into an ES search query.
8 *
9 * This varies by application, but roughly it means extracting some parts of the query
10 * (authors, tags, and phrases) that are treated as a filter. Then taking the
11 * remaining words and building the correct query (possibly with prefix searching
12 * if we are doing search as you type)
13 *
14 * This class only supports ES 2.x+
15 *
16 * Disables comment chehcks.
17 * phpcs:disable Squiz.Commenting
18 *
19 * This parser builds queries of the form:
20 * bool:
21 * must:
22 * AND match of a single field (ideally an edgengram field)
23 * filter:
24 * filter clauses from context (eg @gibrown, #news, etc)
25 * should:
26 * boosting of results by various fields
27 *
28 * Features supported:
29 * - search as you type
30 * - phrases
31 * - supports querying across multiple languages at once
32 *
33 * @package automattic/jetpack-search
34 */
35
36 namespace Automattic\Jetpack\Search\WPES;
37
38 if ( ! defined( 'ABSPATH' ) ) {
39 exit( 0 );
40 }
41
42 /**
43 * Query parser class.
44 */
45 class Query_Parser extends Query_Builder {
46 protected $orig_query = '';
47 protected $current_query = '';
48 protected $langs;
49 protected $avail_langs = array( 'ar', 'bg', 'ca', 'cs', 'da', 'de', 'el', 'en', 'es', 'eu', 'fa', 'fi', 'fr', 'he', 'hi', 'hu', 'hy', 'id', 'it', 'ja', 'ko', 'nl', 'no', 'pt', 'ro', 'ru', 'sv', 'tr', 'zh' );
50
51 /**
52 * All fields.
53 *
54 * @var array
55 */
56 public $all_fields;
57
58 /**
59 * Phrase fields.
60 *
61 * @var array
62 */
63 public $phrase_fields;
64
65 public function __construct( $user_query, $langs ) {
66 $this->orig_query = $user_query;
67 $this->current_query = $this->orig_query;
68 $this->langs = $this->norm_langs( $langs );
69 }
70
71 protected $extracted_phrases = array();
72
73 public function get_current_query() {
74 return $this->current_query;
75 }
76
77 public function set_current_query( $q ) {
78 $this->current_query = $q;
79 }
80
81 ///////////////////////////////////////////////////////
82 // Methods for Building arrays of multilingual fields
83
84 /*
85 * Normalize language codes
86 */
87 public function norm_langs( $langs ) {
88 $lst = array();
89 foreach ( $langs as $l ) {
90 $l = strtok( $l, '-_' );
91 if ( in_array( $l, $this->avail_langs, true ) ) {
92 $lst[ $l ] = true;
93 } else {
94 $lst['default'] = true;
95 }
96 }
97 return array_keys( $lst );
98 }
99
100 public function get_lang_field_suffix() {
101 if ( ! is_array( $this->langs ) || empty( $this->langs ) ) {
102 return;
103 }
104
105 // Returns the first language only
106 return $this->langs[0];
107 }
108
109 /*
110 * Take a list of field prefixes and expand them for multi-lingual
111 * with the provided boostings.
112 */
113 public function merge_ml_fields( $fields2boosts, $additional_fields ) {
114 $flds = array();
115 foreach ( $fields2boosts as $f => $b ) {
116 foreach ( $this->langs as $l ) {
117 $flds[] = $f . '.' . $l . '^' . $b;
118 }
119 }
120 foreach ( $additional_fields as $f ) {
121 $flds[] = $f;
122 }
123 return $flds;
124 }
125
126 ////////////////////////////////////
127 // Extract Fields for Filtering on
128
129 /*
130 * Extract any @mentions from the user query
131 * use them as a filter if we can find a wp.com id
132 * otherwise use them as a
133 *
134 * args:
135 * wpcom_id_field: wp.com id field
136 * must_query_fields: array of fields to search for matching results (optional)
137 * boost_query_fields: array of fields to search in for boosting results (optional)
138 * prefixes: array of prefixes that the user can use to indicate an author
139 *
140 * returns true/false of whether any were found
141 *
142 * See also: https://github.com/twitter/twitter-text/blob/master/java/src/com/twitter/Regex.java
143 */
144 public function author_field_filter( $args ) {
145 $defaults = array(
146 'wpcom_id_field' => 'author_id',
147 'must_query_fields' => null,
148 'boost_query_fields' => null,
149 'prefixes' => array( '@' ),
150 );
151 $args = wp_parse_args( $args, $defaults );
152
153 $names = array();
154 foreach ( $args['prefixes'] as $p ) {
155 $found = $this->get_fields( $p );
156 if ( $found ) {
157 foreach ( $found as $f ) {
158 $names[] = $f;
159 }
160 }
161 }
162
163 if ( empty( $names ) ) {
164 return false;
165 }
166
167 foreach ( $args['prefixes'] as $p ) {
168 $this->remove_fields( $p );
169 }
170
171 $user_ids = array();
172
173 //loop through the matches and separate into filters and queries
174 foreach ( $names as $n ) {
175 //check for exact match on login
176 $userdata = get_user_by( 'login', strtolower( $n ) );
177 $filtering = false;
178 if ( $userdata ) {
179 $user_ids[ $userdata->ID ] = true;
180 $filtering = true;
181 }
182
183 $is_phrase = false;
184 if ( preg_match( '/"/', $n ) ) {
185 $is_phrase = true;
186 $n = preg_replace( '/"/', '', $n );
187 }
188
189 if ( ! empty( $args['must_query_fields'] ) && ! $filtering ) {
190 if ( $is_phrase ) {
191 $this->add_query(
192 array(
193 'multi_match' => array(
194 'fields' => $args['must_query_fields'],
195 'query' => $n,
196 'type' => 'phrase',
197 ),
198 )
199 );
200 } else {
201 $this->add_query(
202 array(
203 'multi_match' => array(
204 'fields' => $args['must_query_fields'],
205 'query' => $n,
206 ),
207 )
208 );
209 }
210 }
211
212 if ( ! empty( $args['boost_query_fields'] ) ) {
213 if ( $is_phrase ) {
214 $this->add_query(
215 array(
216 'multi_match' => array(
217 'fields' => $args['boost_query_fields'],
218 'query' => $n,
219 'type' => 'phrase',
220 ),
221 ),
222 'should'
223 );
224 } else {
225 $this->add_query(
226 array(
227 'multi_match' => array(
228 'fields' => $args['boost_query_fields'],
229 'query' => $n,
230 ),
231 ),
232 'should'
233 );
234 }
235 }
236 }
237
238 if ( ! empty( $user_ids ) ) {
239 $user_ids = array_keys( $user_ids );
240 $this->add_filter( array( 'terms' => array( $args['wpcom_id_field'] => $user_ids ) ) );
241 }
242
243 return true;
244 }
245
246 /*
247 * Extract any prefix followed by text use them as a must clause,
248 * and optionally as a boost to the should query
249 * This can be used for hashtags. eg #News, or #"current events",
250 * but also works for any arbitrary field. eg from:Greg
251 *
252 * args:
253 * must_query_fields: array of fields that must match the tag (optional)
254 * boost_query_fields: array of fields to boost search on (optional)
255 * prefixes: array of prefixes that the user can use to indicate a tag
256 *
257 * returns true/false of whether any were found
258 *
259 */
260 public function text_field_filter( $args ) {
261 $defaults = array(
262 'must_query_fields' => array( 'tag.name' ),
263 'boost_query_fields' => array( 'tag.name' ),
264 'prefixes' => array( '#' ),
265 );
266 $args = wp_parse_args( $args, $defaults );
267
268 $tags = array();
269 foreach ( $args['prefixes'] as $p ) {
270 $found = $this->get_fields( $p );
271 if ( $found ) {
272 foreach ( $found as $f ) {
273 $tags[] = $f;
274 }
275 }
276 }
277
278 if ( empty( $tags ) ) {
279 return false;
280 }
281
282 foreach ( $args['prefixes'] as $p ) {
283 $this->remove_fields( $p );
284 }
285
286 foreach ( $tags as $t ) {
287 $is_phrase = false;
288 if ( preg_match( '/"/', $t ) ) {
289 $is_phrase = true;
290 $t = preg_replace( '/"/', '', $t );
291 }
292
293 if ( ! empty( $args['must_query_fields'] ) ) {
294 if ( $is_phrase ) {
295 $this->add_query(
296 array(
297 'multi_match' => array(
298 'fields' => $args['must_query_fields'],
299 'query' => $t,
300 'type' => 'phrase',
301 ),
302 )
303 );
304 } else {
305 $this->add_query(
306 array(
307 'multi_match' => array(
308 'fields' => $args['must_query_fields'],
309 'query' => $t,
310 ),
311 )
312 );
313 }
314 }
315
316 if ( ! empty( $args['boost_query_fields'] ) ) {
317 if ( $is_phrase ) {
318 $this->add_query(
319 array(
320 'multi_match' => array(
321 'fields' => $args['boost_query_fields'],
322 'query' => $t,
323 'type' => 'phrase',
324 ),
325 ),
326 'should'
327 );
328 } else {
329 $this->add_query(
330 array(
331 'multi_match' => array(
332 'fields' => $args['boost_query_fields'],
333 'query' => $t,
334 ),
335 ),
336 'should'
337 );
338 }
339 }
340 }
341
342 return true;
343 }
344
345 /*
346 * Extract anything surrounded by quotes or if there is an opening quote
347 * that is not complete, and add them to the query as a phrase query.
348 * Quotes can be either '' or ""
349 *
350 * args:
351 * must_query_fields: array of fields that must match the phrases
352 * boost_query_fields: array of fields to boost the phrases on (optional)
353 *
354 * returns true/false of whether any were found
355 *
356 */
357 public function phrase_filter( $args ) {
358 $defaults = array(
359 'must_query_fields' => array( 'all_content' ),
360 'boost_query_fields' => array( 'title' ),
361 );
362 $args = wp_parse_args( $args, $defaults );
363
364 $phrases = array();
365 if ( preg_match_all( '/"([^"]+)"/', $this->current_query, $matches ) ) {
366 foreach ( $matches[1] as $match ) {
367 $phrases[] = $match;
368 }
369 $this->current_query = preg_replace( '/"([^"]+)"/', '', $this->current_query );
370 }
371
372 if ( preg_match_all( "/'([^']+)'/", $this->current_query, $matches ) ) {
373 foreach ( $matches[1] as $match ) {
374 $phrases[] = $match;
375 }
376 $this->current_query = preg_replace( "/'([^']+)'/", '', $this->current_query );
377 }
378
379 //look for a final, uncompleted phrase
380 $phrase_prefix = false;
381 if ( preg_match_all( '/"([^"]+)$/', $this->current_query, $matches ) ) {
382 $phrase_prefix = $matches[1][0];
383 $this->current_query = preg_replace( '/"([^"]+)$/', '', $this->current_query );
384 }
385 if ( preg_match_all( "/(?:'\B|\B')([^']+)$/", $this->current_query, $matches ) ) {
386 $phrase_prefix = $matches[1][0];
387 $this->current_query = preg_replace( "/(?:'\B|\B')([^']+)$/", '', $this->current_query );
388 }
389
390 if ( $phrase_prefix ) {
391 $phrases[] = $phrase_prefix;
392 }
393 if ( empty( $phrases ) ) {
394 return false;
395 }
396
397 foreach ( $phrases as $p ) {
398 $this->add_query(
399 array(
400 'multi_match' => array(
401 'fields' => $args['must_query_fields'],
402 'query' => $p,
403 'type' => 'phrase',
404 ),
405 )
406 );
407
408 if ( ! empty( $args['boost_query_fields'] ) ) {
409 $this->add_query(
410 array(
411 'multi_match' => array(
412 'fields' => $args['boost_query_fields'],
413 'query' => $p,
414 'operator' => 'and',
415 ),
416 ),
417 'should'
418 );
419 }
420 }
421
422 return true;
423 }
424
425 /*
426 * Query fields based on the remaining parts of the query
427 * This could be the final AND part of the query terms to match, or it
428 * could be boosting certain elements of the query
429 *
430 * args:
431 * must_query_fields: array of fields that must match the remaining terms (optional)
432 * boost_query_fields: array of fields to boost the remaining terms on (optional)
433 *
434 */
435 public function remaining_query( $args ) {
436 $defaults = array(
437 'must_query_fields' => null,
438 'boost_query_fields' => null,
439 'boost_operator' => 'and',
440 'boost_query_type' => 'best_fields',
441 );
442 $args = wp_parse_args( $args, $defaults );
443
444 if ( empty( $this->current_query ) || ctype_space( $this->current_query ) ) {
445 return;
446 }
447
448 if ( ! empty( $args['must_query_fields'] ) ) {
449 $this->add_query(
450 array(
451 'multi_match' => array(
452 'fields' => $args['must_query_fields'],
453 'query' => $this->current_query,
454 'operator' => 'and',
455 ),
456 )
457 );
458 }
459
460 if ( ! empty( $args['boost_query_fields'] ) ) {
461 $this->add_query(
462 array(
463 'multi_match' => array(
464 'fields' => $args['boost_query_fields'],
465 'query' => $this->current_query,
466 'operator' => $args['boost_operator'],
467 'type' => $args['boost_query_type'],
468 ),
469 ),
470 'should'
471 );
472 }
473 }
474
475 /*
476 * Query fields using a prefix query (alphabetical expansions on the index).
477 * This is not recommended. Slower performance and worse relevancy.
478 *
479 * (UNTESTED! Copied from old prefix expansion code)
480 *
481 * args:
482 * must_query_fields: array of fields that must match the remaining terms (optional)
483 * boost_query_fields: array of fields to boost the remaining terms on (optional)
484 *
485 */
486 public function remaining_prefix_query( $args ) {
487 $defaults = array(
488 'must_query_fields' => array( 'all_content' ),
489 'boost_query_fields' => array( 'title' ),
490 'boost_operator' => 'and',
491 'boost_query_type' => 'best_fields',
492 );
493 $args = wp_parse_args( $args, $defaults );
494
495 if ( empty( $this->current_query ) || ctype_space( $this->current_query ) ) {
496 return;
497 }
498
499 //////////////////////////////////
500 // Example cases to think about:
501 // "elasticse"
502 // "elasticsearch"
503 // "elasticsearch "
504 // "elasticsearch lucen"
505 // "elasticsearch lucene"
506 // "the future" - note the stopword which will match nothing!
507 // "F1" - an exact match that also has tons of expansions
508 // "こんにちは" ja "hello"
509 // "こんにちは友人" ja "hello friend" - we just rely on the prefix phrase and ES to split words
510 // - this could still be better I bet. Maybe we need to analyze with ES first?
511 //
512
513 /////////////////////////////
514 //extract pieces of query
515 // eg: "PREFIXREMAINDER PREFIXWORD"
516 // "elasticsearch lucen"
517
518 $prefix_word = false;
519 $prefix_remainder = false;
520 if ( preg_match_all( '/([^ ]+)$/', $this->current_query, $matches ) ) {
521 $prefix_word = $matches[1][0];
522 }
523
524 $prefix_remainder = preg_replace( '/([^ ]+)$/', '', $this->current_query );
525 if ( ctype_space( $prefix_remainder ) ) {
526 $prefix_remainder = false;
527 }
528
529 if ( ! $prefix_word ) {
530 //Space at the end of the query, so skip using a prefix query
531 if ( ! empty( $args['must_query_fields'] ) ) {
532 $this->add_query(
533 array(
534 'multi_match' => array(
535 'fields' => $args['must_query_fields'],
536 'query' => $this->current_query,
537 'operator' => 'and',
538 ),
539 )
540 );
541 }
542
543 if ( ! empty( $args['boost_query_fields'] ) ) {
544 $this->add_query(
545 array(
546 'multi_match' => array(
547 'fields' => $args['boost_query_fields'],
548 'query' => $this->current_query,
549 'operator' => $args['boost_operator'],
550 'type' => $args['boost_query_type'],
551 ),
552 ),
553 'should'
554 );
555 }
556 } else {
557
558 //must match the prefix word and the prefix remainder
559 if ( ! empty( $args['must_query_fields'] ) ) {
560 //need to do an OR across a few fields to handle all cases
561 $must_q = array(
562 'bool' => array(
563 'should' => array(),
564 'minimum_should_match' => 1,
565 ),
566 );
567
568 //treat all words as an exact search (boosts complete word like "news"
569 //from prefixes of "newspaper")
570 $must_q['bool']['should'][] = array(
571 'multi_match' => array(
572 'fields' => $this->all_fields,
573 // NOTE: This line has been disabled since $full_text is not available.
574 // 'query' => $full_text,
575 'operator' => 'and',
576 'type' => 'cross_fields',
577 ),
578 );
579
580 //always optimistically try and match the full text as a phrase
581 //prefix "the futu" should try to match "the future"
582 //otherwise the first stopword kinda breaks
583 //This also works as the prefix match for a single word "elasticsea"
584 $must_q['bool']['should'][] = array(
585 'multi_match' => array(
586 'fields' => $this->phrase_fields,
587 // NOTE: This line has been disabled since $full_text is not available.
588 // 'query' => $full_text,
589 'operator' => 'and',
590 'type' => 'phrase_prefix',
591 'max_expansions' => 100,
592 ),
593 );
594
595 if ( $prefix_remainder ) {
596 //Multiple words found, so treat each word on its own and not just as
597 //a part of a phrase
598 //"elasticsearch lucen" => "elasticsearch" exact AND "lucen" prefix
599 $must_q['bool']['should'][] = array(
600 'bool' => array(
601 'must' => array(
602 array(
603 'multi_match' => array(
604 'fields' => $this->phrase_fields,
605 'query' => $prefix_word,
606 'operator' => 'and',
607 'type' => 'phrase_prefix',
608 'max_expansions' => 100,
609 ),
610 ),
611 array(
612 'multi_match' => array(
613 'fields' => $this->all_fields,
614 'query' => $prefix_remainder,
615 'operator' => 'and',
616 'type' => 'cross_fields',
617 ),
618 ),
619 ),
620 ),
621 );
622 }
623
624 $this->add_query( $must_q );
625 }
626
627 //Now add any boosting of the query
628 if ( ! empty( $args['boost_query_fields'] ) ) {
629 //treat all words as an exact search (boosts complete word like "news"
630 //from prefixes of "newspaper")
631 $this->add_query(
632 array(
633 'multi_match' => array(
634 'fields' => $args['boost_query_fields'],
635 'query' => $this->current_query,
636 'operator' => $args['boost_query_operator'],
637 'type' => $args['boost_query_type'],
638 ),
639 ),
640 'should'
641 );
642
643 //optimistically boost the full phrase prefix match
644 $this->add_query(
645 array(
646 'multi_match' => array(
647 'fields' => $args['boost_query_fields'],
648 'query' => $this->current_query,
649 'operator' => 'and',
650 'type' => 'phrase_prefix',
651 'max_expansions' => 100,
652 ),
653 )
654 );
655 }
656 }
657 }
658
659 /*
660 * Boost results based on the lang probability overlaps
661 *
662 * args:
663 * langs2prob: list of languages to search in with associated boosts
664 */
665 public function boost_lang_probs( $langs2prob ) {
666 foreach ( $langs2prob as $p ) {
667 $this->add_function(
668 'field_value_factor',
669 array(
670 'modifier' => 'none',
671 'factor' => $p,
672 'missing' => 0.01, //1% chance doc did not have right lang detected
673 )
674 );
675 }
676 }
677
678 ////////////////////////////////////
679 // Helper Methods
680
681 //Get the text after some prefix. eg @gibrown, or @"Greg Brown"
682 protected function get_fields( $field_prefix ) {
683 $regex = '/' . $field_prefix . '(("[^"]+")|([^\\p{Z}]+))/';
684 if ( preg_match_all( $regex, $this->current_query, $match ) ) {
685 return $match[1];
686 }
687 return false;
688 }
689
690 //Remove the prefix and text from the query
691 protected function remove_fields( $field_name ) {
692 $regex = '/' . $field_name . '(("[^"]+")|([^\\p{Z}]+))/';
693 $this->current_query = preg_replace( $regex, '', $this->current_query );
694 }
695
696 //Best effort string truncation that splits on word breaks
697 protected function truncate_string( $string, $limit, $break = ' ' ) {
698 if ( mb_strwidth( $string ) <= $limit ) {
699 return $string;
700 }
701
702 // walk backwards from $limit to find first break
703 $breakpoint = $limit;
704 $broken = false;
705 while ( $breakpoint > 0 ) {
706 if ( mb_strimwidth( $string, $breakpoint, 1 ) === $break ) {
707 $string = mb_strimwidth( $string, 0, $breakpoint );
708 $broken = true;
709 break;
710 }
711 --$breakpoint;
712 }
713 // if we weren't able to find a break, need to chop mid-word
714 if ( ! $broken ) {
715 $string = mb_strimwidth( $string, 0, $limit );
716 }
717 return $string;
718 }
719 }
720