Нема описа

class.jetpack-search.php 58KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518151915201521152215231524152515261527152815291530153115321533153415351536153715381539154015411542154315441545154615471548154915501551155215531554155515561557155815591560156115621563156415651566156715681569157015711572157315741575157615771578157915801581158215831584158515861587158815891590159115921593159415951596159715981599160016011602160316041605160616071608160916101611161216131614161516161617161816191620162116221623162416251626162716281629163016311632163316341635163616371638163916401641164216431644164516461647164816491650165116521653165416551656165716581659166016611662166316641665166616671668166916701671167216731674167516761677167816791680168116821683168416851686168716881689169016911692169316941695169616971698169917001701170217031704170517061707170817091710171117121713171417151716171717181719172017211722172317241725172617271728172917301731173217331734173517361737173817391740174117421743174417451746174717481749175017511752175317541755175617571758175917601761176217631764176517661767176817691770177117721773177417751776177717781779178017811782178317841785178617871788178917901791179217931794179517961797179817991800180118021803180418051806180718081809181018111812181318141815181618171818181918201821182218231824182518261827182818291830183118321833183418351836183718381839184018411842184318441845184618471848184918501851185218531854185518561857185818591860186118621863186418651866186718681869187018711872187318741875187618771878187918801881188218831884188518861887188818891890189118921893189418951896189718981899190019011902190319041905190619071908190919101911191219131914191519161917191819191920192119221923192419251926192719281929193019311932193319341935193619371938193919401941194219431944194519461947194819491950195119521953195419551956195719581959196019611962196319641965196619671968196919701971197219731974197519761977197819791980198119821983198419851986198719881989199019911992199319941995199619971998199920002001200220032004200520062007200820092010201120122013201420152016201720182019202020212022202320242025
  1. <?php // phpcs:ignore WordPress.Files.FileName.InvalidClassFileName
  2. /**
  3. * Jetpack Search: Main Jetpack_Search class
  4. *
  5. * @package Jetpack
  6. * @subpackage Jetpack Search
  7. * @since 5.0.0
  8. */
  9. use Automattic\Jetpack\Connection\Client;
  10. require_once __DIR__ . '/class-jetpack-search-options.php';
  11. /**
  12. * The main class for the Jetpack Search module.
  13. *
  14. * @since 5.0.0
  15. */
  16. class Jetpack_Search {
  17. /**
  18. * The number of found posts.
  19. *
  20. * @since 5.0.0
  21. *
  22. * @var int
  23. */
  24. protected $found_posts = 0;
  25. /**
  26. * The search result, as returned by the WordPress.com REST API.
  27. *
  28. * @since 5.0.0
  29. *
  30. * @var array
  31. */
  32. protected $search_result;
  33. /**
  34. * This site's blog ID on WordPress.com.
  35. *
  36. * @since 5.0.0
  37. *
  38. * @var int
  39. */
  40. protected $jetpack_blog_id;
  41. /**
  42. * The Elasticsearch aggregations (filters).
  43. *
  44. * @since 5.0.0
  45. *
  46. * @var array
  47. */
  48. protected $aggregations = array();
  49. /**
  50. * The maximum number of aggregations allowed.
  51. *
  52. * @since 5.0.0
  53. *
  54. * @var int
  55. */
  56. protected $max_aggregations_count = 100;
  57. /**
  58. * Statistics about the last Elasticsearch query.
  59. *
  60. * @since 5.6.0
  61. *
  62. * @var array
  63. */
  64. protected $last_query_info = array();
  65. /**
  66. * Statistics about the last Elasticsearch query failure.
  67. *
  68. * @since 5.6.0
  69. *
  70. * @var array
  71. */
  72. protected $last_query_failure_info = array();
  73. /**
  74. * The singleton instance of this class.
  75. *
  76. * @since 5.0.0
  77. *
  78. * @var Jetpack_Search
  79. */
  80. protected static $instance;
  81. /**
  82. * Languages with custom analyzers. Other languages are supported, but are analyzed with the default analyzer.
  83. *
  84. * @since 5.0.0
  85. *
  86. * @var array
  87. */
  88. public static $analyzed_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' );
  89. /**
  90. * Jetpack_Search constructor.
  91. *
  92. * @since 5.0.0
  93. *
  94. * Doesn't do anything. This class needs to be initialized via the instance() method instead.
  95. */
  96. protected function __construct() {
  97. }
  98. /**
  99. * Prevent __clone()'ing of this class.
  100. *
  101. * @since 5.0.0
  102. */
  103. public function __clone() {
  104. wp_die( "Please don't __clone Jetpack_Search" );
  105. }
  106. /**
  107. * Prevent __wakeup()'ing of this class.
  108. *
  109. * @since 5.0.0
  110. */
  111. public function __wakeup() {
  112. wp_die( "Please don't __wakeup Jetpack_Search" );
  113. }
  114. /**
  115. * Get singleton instance of Jetpack_Search.
  116. *
  117. * Instantiates and sets up a new instance if needed, or returns the singleton.
  118. *
  119. * @since 5.0.0
  120. *
  121. * @return Jetpack_Search The Jetpack_Search singleton.
  122. */
  123. public static function instance() {
  124. if ( ! isset( self::$instance ) ) {
  125. if ( Jetpack_Search_Options::is_instant_enabled() ) {
  126. require_once __DIR__ . '/class-jetpack-instant-search.php';
  127. self::$instance = new Jetpack_Instant_Search();
  128. } else {
  129. self::$instance = new Jetpack_Search();
  130. }
  131. self::$instance->setup();
  132. }
  133. return self::$instance;
  134. }
  135. /**
  136. * Perform various setup tasks for the class.
  137. *
  138. * Checks various pre-requisites and adds hooks.
  139. *
  140. * @since 5.0.0
  141. */
  142. public function setup() {
  143. if ( ! Jetpack::is_connection_ready() || ! $this->is_search_supported() ) {
  144. /**
  145. * Fires when the Jetpack Search fails and would fallback to MySQL.
  146. *
  147. * @module search
  148. * @since 7.9.0
  149. *
  150. * @param string $reason Reason for Search fallback.
  151. * @param mixed $data Data associated with the request, such as attempted search parameters.
  152. */
  153. do_action( 'jetpack_search_abort', 'inactive', null );
  154. return;
  155. }
  156. $this->jetpack_blog_id = Jetpack::get_option( 'id' );
  157. if ( ! $this->jetpack_blog_id ) {
  158. /** This action is documented in modules/search/class.jetpack-search.php */
  159. do_action( 'jetpack_search_abort', 'no_blog_id', null );
  160. return;
  161. }
  162. $this->load_php();
  163. $this->init_hooks();
  164. }
  165. /**
  166. * Loads the php for this version of search
  167. *
  168. * @since 8.3.0
  169. */
  170. public function load_php() {
  171. $this->base_load_php();
  172. }
  173. /**
  174. * Loads the PHP common to all search. Should be called from extending classes.
  175. */
  176. protected function base_load_php() {
  177. require_once __DIR__ . '/class.jetpack-search-helpers.php';
  178. require_once __DIR__ . '/class.jetpack-search-template-tags.php';
  179. require_once JETPACK__PLUGIN_DIR . 'modules/widgets/search.php';
  180. }
  181. /**
  182. * Setup the various hooks needed for the plugin to take over search duties.
  183. *
  184. * @since 5.0.0
  185. */
  186. public function init_hooks() {
  187. if ( ! is_admin() ) {
  188. add_filter( 'posts_pre_query', array( $this, 'filter__posts_pre_query' ), 10, 2 );
  189. add_filter( 'jetpack_search_es_wp_query_args', array( $this, 'filter__add_date_filter_to_query' ), 10, 2 );
  190. add_action( 'did_jetpack_search_query', array( $this, 'store_last_query_info' ) );
  191. add_action( 'failed_jetpack_search_query', array( $this, 'store_query_failure' ) );
  192. add_action( 'init', array( $this, 'set_filters_from_widgets' ) );
  193. add_action( 'pre_get_posts', array( $this, 'maybe_add_post_type_as_var' ) );
  194. } else {
  195. add_action( 'update_option', array( $this, 'track_widget_updates' ), 10, 3 );
  196. }
  197. add_action( 'jetpack_deactivate_module_search', array( $this, 'move_search_widgets_to_inactive' ) );
  198. }
  199. /**
  200. * Is search supported on the current plan
  201. *
  202. * @since 6.0
  203. * Loads scripts for Tracks analytics library
  204. */
  205. public function is_search_supported() {
  206. if ( method_exists( 'Jetpack_Plan', 'supports' ) ) {
  207. return Jetpack_Plan::supports( 'search' );
  208. }
  209. return false;
  210. }
  211. /**
  212. * Does this site have a VIP index
  213. * Get the version number to use when loading the file. Allows us to bypass cache when developing.
  214. *
  215. * @since 6.0
  216. * @return string $script_version Version number.
  217. */
  218. public function has_vip_index() {
  219. return defined( 'JETPACK_SEARCH_VIP_INDEX' ) && JETPACK_SEARCH_VIP_INDEX;
  220. }
  221. /**
  222. * When an Elasticsearch query fails, this stores it and enqueues some debug information in the footer.
  223. *
  224. * @since 5.6.0
  225. *
  226. * @param array $meta Information about the failure.
  227. */
  228. public function store_query_failure( $meta ) {
  229. $this->last_query_failure_info = $meta;
  230. add_action( 'wp_footer', array( $this, 'print_query_failure' ) );
  231. }
  232. /**
  233. * Outputs information about the last Elasticsearch failure.
  234. *
  235. * @since 5.6.0
  236. */
  237. public function print_query_failure() {
  238. if ( $this->last_query_failure_info ) {
  239. printf(
  240. '<!-- Jetpack Search failed with code %s: %s - %s -->',
  241. esc_html( $this->last_query_failure_info['response_code'] ),
  242. esc_html( $this->last_query_failure_info['json']['error'] ),
  243. esc_html( $this->last_query_failure_info['json']['message'] )
  244. );
  245. }
  246. }
  247. /**
  248. * Stores information about the last Elasticsearch query and enqueues some debug information in the footer.
  249. *
  250. * @since 5.6.0
  251. *
  252. * @param array $meta Information about the query.
  253. */
  254. public function store_last_query_info( $meta ) {
  255. $this->last_query_info = $meta;
  256. add_action( 'wp_footer', array( $this, 'print_query_success' ) );
  257. }
  258. /**
  259. * Outputs information about the last Elasticsearch search.
  260. *
  261. * @since 5.6.0
  262. */
  263. public function print_query_success() {
  264. if ( $this->last_query_info ) {
  265. printf(
  266. '<!-- Jetpack Search took %s ms, ES time %s ms -->',
  267. (int) $this->last_query_info['elapsed_time'],
  268. esc_html( $this->last_query_info['es_time'] )
  269. );
  270. if ( isset( $_GET['searchdebug'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended
  271. printf(
  272. '<!-- Query response data: %s -->',
  273. esc_html( print_r( $this->last_query_info, 1 ) ) // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_print_r
  274. );
  275. }
  276. }
  277. }
  278. /**
  279. * Returns the last query information, or false if no information was stored.
  280. *
  281. * @since 5.8.0
  282. *
  283. * @return bool|array
  284. */
  285. public function get_last_query_info() {
  286. return empty( $this->last_query_info ) ? false : $this->last_query_info;
  287. }
  288. /**
  289. * Returns the last query failure information, or false if no failure information was stored.
  290. *
  291. * @since 5.8.0
  292. *
  293. * @return bool|array
  294. */
  295. public function get_last_query_failure_info() {
  296. return empty( $this->last_query_failure_info ) ? false : $this->last_query_failure_info;
  297. }
  298. /**
  299. * Wraps a WordPress filter called "jetpack_search_disable_widget_filters" that allows
  300. * developers to disable filters supplied by the search widget. Useful if filters are
  301. * being defined at the code level.
  302. *
  303. * @since 5.7.0
  304. * @deprecated 5.8.0 Use Jetpack_Search_Helpers::are_filters_by_widget_disabled() directly.
  305. *
  306. * @return bool
  307. */
  308. public function are_filters_by_widget_disabled() {
  309. return Jetpack_Search_Helpers::are_filters_by_widget_disabled();
  310. }
  311. /**
  312. * Retrieves a list of known Jetpack search filters widget IDs, gets the filters for each widget,
  313. * and applies those filters to this Jetpack_Search object.
  314. *
  315. * @since 5.7.0
  316. */
  317. public function set_filters_from_widgets() {
  318. if ( Jetpack_Search_Helpers::are_filters_by_widget_disabled() ) {
  319. return;
  320. }
  321. $filters = Jetpack_Search_Helpers::get_filters_from_widgets();
  322. if ( ! empty( $filters ) ) {
  323. $this->set_filters( $filters );
  324. }
  325. }
  326. /**
  327. * Restricts search results to certain post types via a GET argument.
  328. *
  329. * @since 5.8.0
  330. *
  331. * @param WP_Query $query A WP_Query instance.
  332. */
  333. public function maybe_add_post_type_as_var( WP_Query $query ) {
  334. $post_type = ( ! empty( $_GET['post_type'] ) ) ? $_GET['post_type'] : false; // phpcs:ignore WordPress.Security.NonceVerification.Recommended
  335. if ( $this->should_handle_query( $query ) && $post_type ) {
  336. $post_types = ( is_string( $post_type ) && false !== strpos( $post_type, ',' ) )
  337. ? explode( ',', $post_type )
  338. : (array) $post_type;
  339. $post_types = array_map( 'sanitize_key', $post_types );
  340. $query->set( 'post_type', $post_types );
  341. }
  342. }
  343. /**
  344. * Run a search on the WordPress.com public API.
  345. *
  346. * @since 5.0.0
  347. *
  348. * @param array $es_args Args conforming to the WP.com /sites/<blog_id>/search endpoint.
  349. *
  350. * @return object|WP_Error The response from the public API, or a WP_Error.
  351. */
  352. public function search( array $es_args ) {
  353. $endpoint = sprintf( '/sites/%s/search', $this->jetpack_blog_id );
  354. $service_url = 'https://public-api.wordpress.com/rest/v1' . $endpoint;
  355. $do_authenticated_request = false;
  356. if ( class_exists( 'Automattic\\Jetpack\\Connection\\Client' ) &&
  357. isset( $es_args['authenticated_request'] ) &&
  358. true === $es_args['authenticated_request'] ) {
  359. $do_authenticated_request = true;
  360. }
  361. unset( $es_args['authenticated_request'] );
  362. $request_args = array(
  363. 'headers' => array(
  364. 'Content-Type' => 'application/json',
  365. ),
  366. 'timeout' => 10,
  367. 'user-agent' => 'jetpack_search',
  368. );
  369. $request_body = wp_json_encode( $es_args );
  370. $start_time = microtime( true );
  371. if ( $do_authenticated_request ) {
  372. $request_args['method'] = 'POST';
  373. $request = Client::wpcom_json_api_request_as_blog( $endpoint, Client::WPCOM_JSON_API_VERSION, $request_args, $request_body );
  374. } else {
  375. $request_args = array_merge(
  376. $request_args,
  377. array(
  378. 'body' => $request_body,
  379. )
  380. );
  381. $request = wp_remote_post( $service_url, $request_args );
  382. }
  383. $end_time = microtime( true );
  384. if ( is_wp_error( $request ) ) {
  385. return $request;
  386. }
  387. $response_code = wp_remote_retrieve_response_code( $request );
  388. if ( ! $response_code || $response_code < 200 || $response_code >= 300 ) {
  389. return new WP_Error( 'invalid_search_api_response', 'Invalid response from API - ' . $response_code );
  390. }
  391. $response = json_decode( wp_remote_retrieve_body( $request ), true );
  392. $took = is_array( $response ) && ! empty( $response['took'] )
  393. ? $response['took']
  394. : null;
  395. $query = array(
  396. 'args' => $es_args,
  397. 'response' => $response,
  398. 'response_code' => $response_code,
  399. 'elapsed_time' => ( $end_time - $start_time ) * 1000, // Convert from float seconds to ms.
  400. 'es_time' => $took,
  401. 'url' => $service_url,
  402. );
  403. /**
  404. * Fires after a search request has been performed.
  405. *
  406. * Includes the following info in the $query parameter:
  407. *
  408. * array args Array of Elasticsearch arguments for the search
  409. * array response Raw API response, JSON decoded
  410. * int response_code HTTP response code of the request
  411. * float elapsed_time Roundtrip time of the search request, in milliseconds
  412. * float es_time Amount of time Elasticsearch spent running the request, in milliseconds
  413. * string url API url that was queried
  414. *
  415. * @module search
  416. *
  417. * @since 5.0.0
  418. * @since 5.8.0 This action now fires on all queries instead of just successful queries.
  419. *
  420. * @param array $query Array of information about the query performed
  421. */
  422. do_action( 'did_jetpack_search_query', $query );
  423. if ( ! $response_code || $response_code < 200 || $response_code >= 300 ) {
  424. /**
  425. * Fires after a search query request has failed
  426. *
  427. * @module search
  428. *
  429. * @since 5.6.0
  430. *
  431. * @param array Array containing the response code and response from the failed search query
  432. */
  433. do_action(
  434. 'failed_jetpack_search_query',
  435. array(
  436. 'response_code' => $response_code,
  437. 'json' => $response,
  438. )
  439. );
  440. return new WP_Error( 'invalid_search_api_response', 'Invalid response from API - ' . $response_code );
  441. }
  442. return $response;
  443. }
  444. /**
  445. * Bypass the normal Search query and offload it to Jetpack servers.
  446. *
  447. * This is the main hook of the plugin and is responsible for returning the posts that match the search query.
  448. *
  449. * @since 5.0.0
  450. *
  451. * @param array $posts Current array of posts (still pre-query).
  452. * @param WP_Query $query The WP_Query being filtered.
  453. *
  454. * @return array Array of matching posts.
  455. */
  456. public function filter__posts_pre_query( $posts, $query ) {
  457. if ( ! $this->should_handle_query( $query ) ) {
  458. // Intentionally not adding the 'jetpack_search_abort' action since this should fire for every request except for search.
  459. return $posts;
  460. }
  461. $this->do_search( $query );
  462. if ( ! is_array( $this->search_result ) ) {
  463. /** This action is documented in modules/search/class.jetpack-search.php */
  464. do_action( 'jetpack_search_abort', 'no_search_results_array', $this->search_result );
  465. return $posts;
  466. }
  467. // If no results, nothing to do.
  468. if ( ! count( $this->search_result['results']['hits'] ) ) {
  469. return array();
  470. }
  471. $post_ids = array();
  472. foreach ( $this->search_result['results']['hits'] as $result ) {
  473. $post_ids[] = (int) $result['fields']['post_id'];
  474. }
  475. // Query all posts now.
  476. $args = array(
  477. 'post__in' => $post_ids,
  478. 'orderby' => 'post__in',
  479. 'perm' => 'readable',
  480. 'post_type' => 'any',
  481. 'ignore_sticky_posts' => true,
  482. 'suppress_filters' => true,
  483. 'posts_per_page' => $query->get( 'posts_per_page' ),
  484. );
  485. $posts_query = new WP_Query( $args );
  486. // WP Core doesn't call the set_found_posts and its filters when filtering posts_pre_query like we do, so need to do these manually.
  487. $query->found_posts = $this->found_posts;
  488. $query->max_num_pages = ceil( $this->found_posts / $query->get( 'posts_per_page' ) );
  489. return $posts_query->posts;
  490. }
  491. /**
  492. * Build up the search, then run it against the Jetpack servers.
  493. *
  494. * @since 5.0.0
  495. *
  496. * @param WP_Query $query The original WP_Query to use for the parameters of our search.
  497. */
  498. public function do_search( WP_Query $query ) {
  499. if ( ! $this->should_handle_query( $query ) ) {
  500. // If we make it here, either 'filter__posts_pre_query' somehow allowed it or a different entry to do_search.
  501. /** This action is documented in modules/search/class.jetpack-search.php */
  502. do_action( 'jetpack_search_abort', 'search_attempted_non_search_query', $query );
  503. return;
  504. }
  505. $page = ( $query->get( 'paged' ) ) ? absint( $query->get( 'paged' ) ) : 1;
  506. // Get maximum allowed offset and posts per page values for the API.
  507. $max_offset = Jetpack_Search_Helpers::get_max_offset();
  508. $max_posts_per_page = Jetpack_Search_Helpers::get_max_posts_per_page();
  509. $posts_per_page = $query->get( 'posts_per_page' );
  510. if ( $posts_per_page > $max_posts_per_page ) {
  511. $posts_per_page = $max_posts_per_page;
  512. }
  513. // Start building the WP-style search query args.
  514. // They'll be translated to ES format args later.
  515. $es_wp_query_args = array(
  516. 'query' => $query->get( 's' ),
  517. 'posts_per_page' => $posts_per_page,
  518. 'paged' => $page,
  519. 'orderby' => $query->get( 'orderby' ),
  520. 'order' => $query->get( 'order' ),
  521. );
  522. if ( ! empty( $this->aggregations ) ) {
  523. $es_wp_query_args['aggregations'] = $this->aggregations;
  524. }
  525. // Did we query for authors?
  526. if ( $query->get( 'author_name' ) ) {
  527. $es_wp_query_args['author_name'] = $query->get( 'author_name' );
  528. }
  529. $es_wp_query_args['post_type'] = $this->get_es_wp_query_post_type_for_query( $query );
  530. $es_wp_query_args['terms'] = $this->get_es_wp_query_terms_for_query( $query );
  531. /**
  532. * Modify the search query parameters, such as controlling the post_type.
  533. *
  534. * These arguments are in the format of WP_Query arguments
  535. *
  536. * @module search
  537. *
  538. * @since 5.0.0
  539. *
  540. * @param array $es_wp_query_args The current query args, in WP_Query format.
  541. * @param WP_Query $query The original WP_Query object.
  542. */
  543. $es_wp_query_args = apply_filters( 'jetpack_search_es_wp_query_args', $es_wp_query_args, $query );
  544. // If page * posts_per_page is greater than our max offset, send a 404. This is necessary because the offset is
  545. // capped at Jetpack_Search_Helpers::get_max_offset(), so a high page would always return the last page of results otherwise.
  546. if ( ( $es_wp_query_args['paged'] * $es_wp_query_args['posts_per_page'] ) > $max_offset ) {
  547. $query->set_404();
  548. return;
  549. }
  550. // If there were no post types returned, then 404 to avoid querying against non-public post types, which could
  551. // happen if we don't add the post type restriction to the ES query.
  552. if ( empty( $es_wp_query_args['post_type'] ) ) {
  553. $query->set_404();
  554. return;
  555. }
  556. // Convert the WP-style args into ES args.
  557. $es_query_args = $this->convert_wp_es_to_es_args( $es_wp_query_args );
  558. // Only trust ES to give us IDs, not the content since it is a mirror.
  559. $es_query_args['fields'] = array(
  560. 'post_id',
  561. );
  562. /**
  563. * Modify the underlying ES query that is passed to the search endpoint. The returned args must represent a valid ES query
  564. *
  565. * This filter is harder to use if you're unfamiliar with ES, but allows complete control over the query
  566. *
  567. * @module search
  568. *
  569. * @since 5.0.0
  570. *
  571. * @param array $es_query_args The raw Elasticsearch query args.
  572. * @param WP_Query $query The original WP_Query object.
  573. */
  574. $es_query_args = apply_filters( 'jetpack_search_es_query_args', $es_query_args, $query );
  575. // Do the actual search query!
  576. $this->search_result = $this->search( $es_query_args );
  577. if ( is_wp_error( $this->search_result ) || ! is_array( $this->search_result ) || empty( $this->search_result['results'] ) || empty( $this->search_result['results']['hits'] ) ) {
  578. $this->found_posts = 0;
  579. return;
  580. }
  581. // If we have aggregations, fix the ordering to match the input order (ES doesn't guarantee the return order).
  582. if ( isset( $this->search_result['results']['aggregations'] ) && ! empty( $this->search_result['results']['aggregations'] ) ) {
  583. $this->search_result['results']['aggregations'] = $this->fix_aggregation_ordering( $this->search_result['results']['aggregations'], $this->aggregations );
  584. }
  585. // Total number of results for paging purposes. Capped at $max_offset + $posts_per_page, as deep paging gets quite expensive.
  586. $this->found_posts = min( $this->search_result['results']['total'], $max_offset + $posts_per_page );
  587. }
  588. /**
  589. * If the query has already been run before filters have been updated, then we need to re-run the query
  590. * to get the latest aggregations.
  591. *
  592. * This is especially useful for supporting widget management in the customizer.
  593. *
  594. * @since 5.8.0
  595. *
  596. * @return bool Whether the query was successful or not.
  597. */
  598. public function update_search_results_aggregations() {
  599. if ( empty( $this->last_query_info ) || empty( $this->last_query_info['args'] ) ) {
  600. return false;
  601. }
  602. $es_args = $this->last_query_info['args'];
  603. $builder = new Jetpack_WPES_Query_Builder();
  604. $this->add_aggregations_to_es_query_builder( $this->aggregations, $builder );
  605. $es_args['aggregations'] = $builder->build_aggregation();
  606. $this->search_result = $this->search( $es_args );
  607. return ! is_wp_error( $this->search_result );
  608. }
  609. /**
  610. * Given a WP_Query, convert its WP_Tax_Query (if present) into the WP-style Elasticsearch term arguments for the search.
  611. *
  612. * @since 5.0.0
  613. *
  614. * @param WP_Query $query The original WP_Query object for which to parse the taxonomy query.
  615. *
  616. * @return array The new WP-style Elasticsearch arguments (that will be converted into 'real' Elasticsearch arguments).
  617. */
  618. public function get_es_wp_query_terms_for_query( WP_Query $query ) {
  619. $args = array();
  620. $the_tax_query = $query->tax_query;
  621. if ( ! $the_tax_query ) {
  622. return $args;
  623. }
  624. if ( ! $the_tax_query instanceof WP_Tax_Query || empty( $the_tax_query->queried_terms ) || ! is_array( $the_tax_query->queried_terms ) ) {
  625. return $args;
  626. }
  627. $args = array();
  628. foreach ( $the_tax_query->queries as $tax_query ) {
  629. // Right now we only support slugs...see note above.
  630. if ( ! is_array( $tax_query ) || 'slug' !== $tax_query['field'] ) {
  631. continue;
  632. }
  633. $taxonomy = $tax_query['taxonomy'];
  634. if ( ! isset( $args[ $taxonomy ] ) || ! is_array( $args[ $taxonomy ] ) ) {
  635. $args[ $taxonomy ] = array();
  636. }
  637. $args[ $taxonomy ] = array_merge( $args[ $taxonomy ], $tax_query['terms'] );
  638. }
  639. return $args;
  640. }
  641. /**
  642. * Parse out the post type from a WP_Query.
  643. *
  644. * Only allows post types that are not marked as 'exclude_from_search'.
  645. *
  646. * @since 5.0.0
  647. *
  648. * @param WP_Query $query Original WP_Query object.
  649. *
  650. * @return array Array of searchable post types corresponding to the original query.
  651. */
  652. public function get_es_wp_query_post_type_for_query( WP_Query $query ) {
  653. $post_types = $query->get( 'post_type' );
  654. // If we're searching 'any', we want to only pass searchable post types to Elasticsearch.
  655. if ( 'any' === $post_types ) {
  656. $post_types = array_values(
  657. get_post_types(
  658. array(
  659. 'exclude_from_search' => false,
  660. )
  661. )
  662. );
  663. }
  664. if ( ! is_array( $post_types ) ) {
  665. $post_types = array( $post_types );
  666. }
  667. $post_types = array_unique( $post_types );
  668. $sanitized_post_types = array();
  669. // Make sure the post types are queryable.
  670. foreach ( $post_types as $post_type ) {
  671. if ( ! $post_type ) {
  672. continue;
  673. }
  674. $post_type_object = get_post_type_object( $post_type );
  675. if ( ! $post_type_object || $post_type_object->exclude_from_search ) {
  676. continue;
  677. }
  678. $sanitized_post_types[] = $post_type;
  679. }
  680. return $sanitized_post_types;
  681. }
  682. /**
  683. * Initialize widgets for the Search module (on wp.com only).
  684. *
  685. * @module search
  686. */
  687. public function action__widgets_init() {
  688. require_once __DIR__ . '/class.jetpack-search-widget-filters.php';
  689. register_widget( 'Jetpack_Search_Widget_Filters' );
  690. }
  691. /**
  692. * Get the Elasticsearch result.
  693. *
  694. * @since 5.0.0
  695. *
  696. * @param bool $raw If true, does not check for WP_Error or return the 'results' array - the JSON decoded HTTP response.
  697. *
  698. * @return array|bool The search results, or false if there was a failure.
  699. */
  700. public function get_search_result( $raw = false ) {
  701. if ( $raw ) {
  702. return $this->search_result;
  703. }
  704. return ( ! empty( $this->search_result ) && ! is_wp_error( $this->search_result ) && is_array( $this->search_result ) && ! empty( $this->search_result['results'] ) ) ? $this->search_result['results'] : false;
  705. }
  706. /**
  707. * Add the date portion of a WP_Query onto the query args.
  708. *
  709. * @since 5.0.0
  710. *
  711. * @param array $es_wp_query_args The Elasticsearch query arguments in WordPress form.
  712. * @param WP_Query $query The original WP_Query.
  713. *
  714. * @return array The es wp query args, with date filters added (as needed).
  715. */
  716. public function filter__add_date_filter_to_query( array $es_wp_query_args, WP_Query $query ) {
  717. if ( $query->get( 'year' ) ) {
  718. if ( $query->get( 'monthnum' ) ) {
  719. // Padding.
  720. $date_monthnum = sprintf( '%02d', $query->get( 'monthnum' ) );
  721. if ( $query->get( 'day' ) ) {
  722. // Padding.
  723. $date_day = sprintf( '%02d', $query->get( 'day' ) );
  724. $date_start = $query->get( 'year' ) . '-' . $date_monthnum . '-' . $date_day . ' 00:00:00';
  725. $date_end = $query->get( 'year' ) . '-' . $date_monthnum . '-' . $date_day . ' 23:59:59';
  726. } else {
  727. $days_in_month = gmdate( 't', mktime( 0, 0, 0, $query->get( 'monthnum' ), 14, $query->get( 'year' ) ) ); // 14 = middle of the month so no chance of DST issues
  728. $date_start = $query->get( 'year' ) . '-' . $date_monthnum . '-01 00:00:00';
  729. $date_end = $query->get( 'year' ) . '-' . $date_monthnum . '-' . $days_in_month . ' 23:59:59';
  730. }
  731. } else {
  732. $date_start = $query->get( 'year' ) . '-01-01 00:00:00';
  733. $date_end = $query->get( 'year' ) . '-12-31 23:59:59';
  734. }
  735. $es_wp_query_args['date_range'] = array(
  736. 'field' => 'date',
  737. 'gte' => $date_start,
  738. 'lte' => $date_end,
  739. );
  740. }
  741. return $es_wp_query_args;
  742. }
  743. /**
  744. * Converts WP_Query style args to Elasticsearch args.
  745. *
  746. * @since 5.0.0
  747. *
  748. * @param array $args Array of WP_Query style arguments.
  749. *
  750. * @return array Array of ES style query arguments.
  751. */
  752. public function convert_wp_es_to_es_args( array $args ) {
  753. jetpack_require_lib( 'jetpack-wpes-query-builder/jetpack-wpes-query-parser' );
  754. $defaults = array(
  755. 'blog_id' => get_current_blog_id(),
  756. 'query' => null, // Search phrase.
  757. 'query_fields' => array(), // list of fields to search.
  758. 'excess_boost' => array(), // map of field to excess boost values (multiply).
  759. 'post_type' => null, // string or an array.
  760. 'terms' => array(), // ex: array( 'taxonomy-1' => array( 'slug' ), 'taxonomy-2' => array( 'slug-a', 'slug-b' ) ). phpcs:ignore Squiz.PHP.CommentedOutCode.Found.
  761. 'author' => null, // id or an array of ids.
  762. 'author_name' => array(), // string or an array.
  763. 'date_range' => null, // array( 'field' => 'date', 'gt' => 'YYYY-MM-dd', 'lte' => 'YYYY-MM-dd' ); date formats: 'YYYY-MM-dd' or 'YYYY-MM-dd HH:MM:SS'. phpcs:ignore Squiz.PHP.CommentedOutCode.Found.
  764. 'orderby' => null, // Defaults to 'relevance' if query is set, otherwise 'date'. Pass an array for multiple orders.
  765. 'order' => 'DESC',
  766. 'posts_per_page' => 10,
  767. 'offset' => null,
  768. 'paged' => null,
  769. /**
  770. * Aggregations. Examples:
  771. * array(
  772. * 'Tag' => array( 'type' => 'taxonomy', 'taxonomy' => 'post_tag', 'count' => 10 ) ),
  773. * 'Post Type' => array( 'type' => 'post_type', 'count' => 10 ) ),
  774. * );
  775. */
  776. 'aggregations' => null,
  777. );
  778. $args = wp_parse_args( $args, $defaults );
  779. $parser = new Jetpack_WPES_Search_Query_Parser(
  780. $args['query'],
  781. /**
  782. * Filter the languages used by Jetpack Search's Query Parser.
  783. *
  784. * @module search
  785. *
  786. * @since 7.9.0
  787. *
  788. * @param array $languages The array of languages. Default is value of get_locale().
  789. */
  790. apply_filters( 'jetpack_search_query_languages', array( get_locale() ) )
  791. );
  792. if ( empty( $args['query_fields'] ) ) {
  793. if ( $this->has_vip_index() ) {
  794. // VIP indices do not have per language fields.
  795. $match_fields = $this->_get_caret_boosted_fields(
  796. array(
  797. 'title' => 0.1,
  798. 'content' => 0.1,
  799. 'excerpt' => 0.1,
  800. 'tag.name' => 0.1,
  801. 'category.name' => 0.1,
  802. 'author_login' => 0.1,
  803. 'author' => 0.1,
  804. )
  805. );
  806. $boost_fields = $this->_get_caret_boosted_fields(
  807. $this->_apply_boosts_multiplier(
  808. array(
  809. 'title' => 2,
  810. 'tag.name' => 1,
  811. 'category.name' => 1,
  812. 'author_login' => 1,
  813. 'author' => 1,
  814. ),
  815. $args['excess_boost']
  816. )
  817. );
  818. $boost_phrase_fields = $this->_get_caret_boosted_fields(
  819. array(
  820. 'title' => 1,
  821. 'content' => 1,
  822. 'excerpt' => 1,
  823. 'tag.name' => 1,
  824. 'category.name' => 1,
  825. 'author' => 1,
  826. )
  827. );
  828. } else {
  829. $match_fields = $parser->merge_ml_fields(
  830. array(
  831. 'title' => 0.1,
  832. 'content' => 0.1,
  833. 'excerpt' => 0.1,
  834. 'tag.name' => 0.1,
  835. 'category.name' => 0.1,
  836. ),
  837. $this->_get_caret_boosted_fields(
  838. array(
  839. 'author_login' => 0.1,
  840. 'author' => 0.1,
  841. )
  842. )
  843. );
  844. $boost_fields = $parser->merge_ml_fields(
  845. $this->_apply_boosts_multiplier(
  846. array(
  847. 'title' => 2,
  848. 'tag.name' => 1,
  849. 'category.name' => 1,
  850. ),
  851. $args['excess_boost']
  852. ),
  853. $this->_get_caret_boosted_fields(
  854. $this->_apply_boosts_multiplier(
  855. array(
  856. 'author_login' => 1,
  857. 'author' => 1,
  858. ),
  859. $args['excess_boost']
  860. )
  861. )
  862. );
  863. $boost_phrase_fields = $parser->merge_ml_fields(
  864. array(
  865. 'title' => 1,
  866. 'content' => 1,
  867. 'excerpt' => 1,
  868. 'tag.name' => 1,
  869. 'category.name' => 1,
  870. ),
  871. $this->_get_caret_boosted_fields(
  872. array(
  873. 'author' => 1,
  874. )
  875. )
  876. );
  877. }
  878. } else {
  879. // If code is overriding the fields, then use that. Important for backwards compatibility.
  880. $match_fields = $args['query_fields'];
  881. $boost_phrase_fields = $match_fields;
  882. $boost_fields = null;
  883. }
  884. $parser->phrase_filter(
  885. array(
  886. 'must_query_fields' => $match_fields,
  887. 'boost_query_fields' => null,
  888. )
  889. );
  890. $parser->remaining_query(
  891. array(
  892. 'must_query_fields' => $match_fields,
  893. 'boost_query_fields' => $boost_fields,
  894. )
  895. );
  896. // Boost on phrase matches.
  897. $parser->remaining_query(
  898. array(
  899. 'boost_query_fields' => $boost_phrase_fields,
  900. 'boost_query_type' => 'phrase',
  901. )
  902. );
  903. /**
  904. * Modify the recency decay parameters for the search query.
  905. *
  906. * The recency decay lowers the search scores based on the age of a post relative to an origin date. Basic adjustments:
  907. * - origin: A date. Posts with this date will have the highest score and no decay applied. Default is today.
  908. * - offset: Number of days/months/years (eg 30d). All posts within this time range of the origin (before and after) will have no decay applied. Default is no offset.
  909. * - scale: The number of days/months/years from the origin+offset at which the decay will equal the decay param. Default 360d
  910. * - decay: The amount of decay applied at offset+scale. Default 0.9.
  911. *
  912. * The curve applied is a Gaussian. More details available at {@see https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-function-score-query.html#function-decay}
  913. *
  914. * @module search
  915. *
  916. * @since 5.8.0
  917. *
  918. * @param array $decay_params The decay parameters.
  919. * @param array $args The WP query parameters.
  920. */
  921. $decay_params = apply_filters(
  922. 'jetpack_search_recency_score_decay',
  923. array(
  924. 'origin' => gmdate( 'Y-m-d' ),
  925. 'scale' => '360d',
  926. 'decay' => 0.9,
  927. ),
  928. $args
  929. );
  930. if ( ! empty( $decay_params ) ) {
  931. // Newer content gets weighted slightly higher.
  932. $parser->add_decay(
  933. 'gauss',
  934. array(
  935. 'date_gmt' => $decay_params,
  936. )
  937. );
  938. }
  939. $es_query_args = array(
  940. 'blog_id' => absint( $args['blog_id'] ),
  941. 'size' => absint( $args['posts_per_page'] ),
  942. );
  943. // ES "from" arg (offset).
  944. if ( $args['offset'] ) {
  945. $es_query_args['from'] = absint( $args['offset'] );
  946. } elseif ( $args['paged'] ) {
  947. $es_query_args['from'] = max( 0, ( absint( $args['paged'] ) - 1 ) * $es_query_args['size'] );
  948. }
  949. $es_query_args['from'] = min( $es_query_args['from'], Jetpack_Search_Helpers::get_max_offset() );
  950. if ( ! is_array( $args['author_name'] ) ) {
  951. $args['author_name'] = array( $args['author_name'] );
  952. }
  953. // ES stores usernames, not IDs, so transform.
  954. if ( ! empty( $args['author'] ) ) {
  955. if ( ! is_array( $args['author'] ) ) {
  956. $args['author'] = array( $args['author'] );
  957. }
  958. foreach ( $args['author'] as $author ) {
  959. $user = get_user_by( 'id', $author );
  960. if ( $user && ! empty( $user->user_login ) ) {
  961. $args['author_name'][] = $user->user_login;
  962. }
  963. }
  964. }
  965. /*
  966. * Build the filters from the query elements.
  967. * Filters rock because they are cached from one query to the next
  968. * but they are cached as individual filters, rather than all combined together.
  969. * May get performance boost by also caching the top level boolean filter too.
  970. */
  971. if ( $args['post_type'] ) {
  972. if ( ! is_array( $args['post_type'] ) ) {
  973. $args['post_type'] = array( $args['post_type'] );
  974. }
  975. $parser->add_filter(
  976. array(
  977. 'terms' => array(
  978. 'post_type' => $args['post_type'],
  979. ),
  980. )
  981. );
  982. }
  983. if ( $args['author_name'] ) {
  984. $parser->add_filter(
  985. array(
  986. 'terms' => array(
  987. 'author_login' => $args['author_name'],
  988. ),
  989. )
  990. );
  991. }
  992. if ( ! empty( $args['date_range'] ) && isset( $args['date_range']['field'] ) ) {
  993. $field = $args['date_range']['field'];
  994. unset( $args['date_range']['field'] );
  995. $parser->add_filter(
  996. array(
  997. 'range' => array(
  998. $field => $args['date_range'],
  999. ),
  1000. )
  1001. );
  1002. }
  1003. if ( is_array( $args['terms'] ) ) {
  1004. foreach ( $args['terms'] as $tax => $terms ) {
  1005. $terms = (array) $terms;
  1006. if ( count( $terms ) && mb_strlen( $tax ) ) {
  1007. switch ( $tax ) {
  1008. case 'post_tag':
  1009. $tax_fld = 'tag.slug';
  1010. break;
  1011. case 'category':
  1012. $tax_fld = 'category.slug';
  1013. break;
  1014. default:
  1015. $tax_fld = 'taxonomy.' . $tax . '.slug';
  1016. break;
  1017. }
  1018. foreach ( $terms as $term ) {
  1019. $parser->add_filter(
  1020. array(
  1021. 'term' => array(
  1022. $tax_fld => $term,
  1023. ),
  1024. )
  1025. );
  1026. }
  1027. }
  1028. }
  1029. }
  1030. if ( ! $args['orderby'] ) {
  1031. if ( $args['query'] ) {
  1032. $args['orderby'] = array( 'relevance' );
  1033. } else {
  1034. $args['orderby'] = array( 'date' );
  1035. }
  1036. }
  1037. // Validate the "order" field.
  1038. switch ( strtolower( $args['order'] ) ) {
  1039. case 'asc':
  1040. $args['order'] = 'asc';
  1041. break;
  1042. case 'desc':
  1043. default:
  1044. $args['order'] = 'desc';
  1045. break;
  1046. }
  1047. $es_query_args['sort'] = array();
  1048. foreach ( (array) $args['orderby'] as $orderby ) {
  1049. // Translate orderby from WP field to ES field.
  1050. switch ( $orderby ) {
  1051. case 'relevance':
  1052. // never order by score ascending.
  1053. $es_query_args['sort'][] = array(
  1054. '_score' => array(
  1055. 'order' => 'desc',
  1056. ),
  1057. );
  1058. break;
  1059. case 'date':
  1060. $es_query_args['sort'][] = array(
  1061. 'date' => array(
  1062. 'order' => $args['order'],
  1063. ),
  1064. );
  1065. break;
  1066. case 'ID':
  1067. $es_query_args['sort'][] = array(
  1068. 'id' => array(
  1069. 'order' => $args['order'],
  1070. ),
  1071. );
  1072. break;
  1073. case 'author':
  1074. $es_query_args['sort'][] = array(
  1075. 'author.raw' => array(
  1076. 'order' => $args['order'],
  1077. ),
  1078. );
  1079. break;
  1080. } // End switch.
  1081. } // End foreach.
  1082. if ( empty( $es_query_args['sort'] ) ) {
  1083. unset( $es_query_args['sort'] );
  1084. }
  1085. // Aggregations.
  1086. if ( ! empty( $args['aggregations'] ) ) {
  1087. $this->add_aggregations_to_es_query_builder( $args['aggregations'], $parser );
  1088. }
  1089. $es_query_args['filter'] = $parser->build_filter();
  1090. $es_query_args['query'] = $parser->build_query();
  1091. $es_query_args['aggregations'] = $parser->build_aggregation();
  1092. return $es_query_args;
  1093. }
  1094. /**
  1095. * Given an array of aggregations, parse and add them onto the Jetpack_WPES_Query_Builder object for use in Elasticsearch.
  1096. *
  1097. * @since 5.0.0
  1098. *
  1099. * @param array $aggregations Array of aggregations (filters) to add to the Jetpack_WPES_Query_Builder.
  1100. * @param Jetpack_WPES_Query_Builder $builder The builder instance that is creating the Elasticsearch query.
  1101. */
  1102. public function add_aggregations_to_es_query_builder( array $aggregations, Jetpack_WPES_Query_Builder $builder ) {
  1103. foreach ( $aggregations as $label => $aggregation ) {
  1104. if ( ! isset( $aggregation['type'] ) ) {
  1105. continue;
  1106. }
  1107. switch ( $aggregation['type'] ) {
  1108. case 'taxonomy':
  1109. $this->add_taxonomy_aggregation_to_es_query_builder( $aggregation, $label, $builder );
  1110. break;
  1111. case 'post_type':
  1112. $this->add_post_type_aggregation_to_es_query_builder( $aggregation, $label, $builder );
  1113. break;
  1114. case 'date_histogram':
  1115. $this->add_date_histogram_aggregation_to_es_query_builder( $aggregation, $label, $builder );
  1116. break;
  1117. }
  1118. }
  1119. }
  1120. /**
  1121. * Given an individual taxonomy aggregation, add it to the Jetpack_WPES_Query_Builder object for use in Elasticsearch.
  1122. *
  1123. * @since 5.0.0
  1124. *
  1125. * @param array $aggregation The aggregation to add to the query builder.
  1126. * @param string $label The 'label' (unique id) for this aggregation.
  1127. * @param Jetpack_WPES_Query_Builder $builder The builder instance that is creating the Elasticsearch query.
  1128. */
  1129. public function add_taxonomy_aggregation_to_es_query_builder( array $aggregation, $label, Jetpack_WPES_Query_Builder $builder ) {
  1130. $field = null;
  1131. switch ( $aggregation['taxonomy'] ) {
  1132. case 'post_tag':
  1133. $field = 'tag';
  1134. break;
  1135. case 'category':
  1136. $field = 'category';
  1137. break;
  1138. default:
  1139. $field = 'taxonomy.' . $aggregation['taxonomy'];
  1140. break;
  1141. }
  1142. $builder->add_aggs(
  1143. $label,
  1144. array(
  1145. 'terms' => array(
  1146. 'field' => $field . '.slug',
  1147. 'size' => min( (int) $aggregation['count'], $this->max_aggregations_count ),
  1148. ),
  1149. )
  1150. );
  1151. }
  1152. /**
  1153. * Given an individual post_type aggregation, add it to the Jetpack_WPES_Query_Builder object for use in Elasticsearch.
  1154. *
  1155. * @since 5.0.0
  1156. *
  1157. * @param array $aggregation The aggregation to add to the query builder.
  1158. * @param string $label The 'label' (unique id) for this aggregation.
  1159. * @param Jetpack_WPES_Query_Builder $builder The builder instance that is creating the Elasticsearch query.
  1160. */
  1161. public function add_post_type_aggregation_to_es_query_builder( array $aggregation, $label, Jetpack_WPES_Query_Builder $builder ) {
  1162. $builder->add_aggs(
  1163. $label,
  1164. array(
  1165. 'terms' => array(
  1166. 'field' => 'post_type',
  1167. 'size' => min( (int) $aggregation['count'], $this->max_aggregations_count ),
  1168. ),
  1169. )
  1170. );
  1171. }
  1172. /**
  1173. * Given an individual date_histogram aggregation, add it to the Jetpack_WPES_Query_Builder object for use in Elasticsearch.
  1174. *
  1175. * @since 5.0.0
  1176. *
  1177. * @param array $aggregation The aggregation to add to the query builder.
  1178. * @param string $label The 'label' (unique id) for this aggregation.
  1179. * @param Jetpack_WPES_Query_Builder $builder The builder instance that is creating the Elasticsearch query.
  1180. */
  1181. public function add_date_histogram_aggregation_to_es_query_builder( array $aggregation, $label, Jetpack_WPES_Query_Builder $builder ) {
  1182. $args = array(
  1183. 'interval' => $aggregation['interval'],
  1184. 'field' => ( ! empty( $aggregation['field'] ) && 'post_date_gmt' === $aggregation['field'] ) ? 'date_gmt' : 'date',
  1185. );
  1186. if ( isset( $aggregation['min_doc_count'] ) ) {
  1187. $args['min_doc_count'] = (int) $aggregation['min_doc_count'];
  1188. } else {
  1189. $args['min_doc_count'] = 1;
  1190. }
  1191. $builder->add_aggs(
  1192. $label,
  1193. array(
  1194. 'date_histogram' => $args,
  1195. )
  1196. );
  1197. }
  1198. /**
  1199. * And an existing filter object with a list of additional filters.
  1200. *
  1201. * Attempts to optimize the filters somewhat.
  1202. *
  1203. * @since 5.0.0
  1204. *
  1205. * @param array $curr_filter The existing filters to build upon.
  1206. * @param array $filters The new filters to add.
  1207. *
  1208. * @return array The resulting merged filters.
  1209. */
  1210. public static function and_es_filters( array $curr_filter, array $filters ) {
  1211. if ( ! is_array( $curr_filter ) || isset( $curr_filter['match_all'] ) ) {
  1212. if ( 1 === count( $filters ) ) {
  1213. return $filters[0];
  1214. }
  1215. return array(
  1216. 'and' => $filters,
  1217. );
  1218. }
  1219. return array(
  1220. 'and' => array_merge( array( $curr_filter ), $filters ),
  1221. );
  1222. }
  1223. /**
  1224. * Set the available filters for the search.
  1225. *
  1226. * These get rendered via the Jetpack_Search_Widget() widget.
  1227. *
  1228. * Behind the scenes, these are implemented using Elasticsearch Aggregations.
  1229. *
  1230. * If you do not require counts of how many documents match each filter, please consider using regular WP Query
  1231. * arguments instead, such as via the jetpack_search_es_wp_query_args filter
  1232. *
  1233. * @see https://www.elastic.co/guide/en/elasticsearch/reference/current/search-aggregations.html
  1234. *
  1235. * @since 5.0.0
  1236. *
  1237. * @param array $aggregations Array of filters (aggregations) to apply to the search.
  1238. */
  1239. public function set_filters( array $aggregations ) {
  1240. foreach ( (array) $aggregations as $key => $agg ) {
  1241. if ( empty( $agg['name'] ) ) {
  1242. $aggregations[ $key ]['name'] = $key;
  1243. }
  1244. }
  1245. $this->aggregations = $aggregations;
  1246. }
  1247. /**
  1248. * Set the search's facets (deprecated).
  1249. *
  1250. * @deprecated 5.0 Please use Jetpack_Search::set_filters() instead.
  1251. *
  1252. * @see Jetpack_Search::set_filters()
  1253. *
  1254. * @param array $facets Array of facets to apply to the search.
  1255. */
  1256. public function set_facets( array $facets ) {
  1257. _deprecated_function( __METHOD__, 'jetpack-5.0', 'Jetpack_Search::set_filters()' );
  1258. $this->set_filters( $facets );
  1259. }
  1260. /**
  1261. * Get the raw Aggregation results from the Elasticsearch response.
  1262. *
  1263. * @since 5.0.0
  1264. *
  1265. * @see https://www.elastic.co/guide/en/elasticsearch/reference/current/search-aggregations.html
  1266. *
  1267. * @return array Array of Aggregations performed on the search.
  1268. */
  1269. public function get_search_aggregations_results() {
  1270. $aggregations = array();
  1271. $search_result = $this->get_search_result();
  1272. if ( ! empty( $search_result ) && ! empty( $search_result['aggregations'] ) ) {
  1273. $aggregations = $search_result['aggregations'];
  1274. }
  1275. return $aggregations;
  1276. }
  1277. /**
  1278. * Get the raw Facet results from the Elasticsearch response.
  1279. *
  1280. * @deprecated 5.0 Please use Jetpack_Search::get_search_aggregations_results() instead.
  1281. *
  1282. * @see Jetpack_Search::get_search_aggregations_results()
  1283. *
  1284. * @return array Array of Facets performed on the search.
  1285. */
  1286. public function get_search_facets() {
  1287. _deprecated_function( __METHOD__, 'jetpack-5.0', 'Jetpack_Search::get_search_aggregations_results()' );
  1288. return $this->get_search_aggregations_results();
  1289. }
  1290. /**
  1291. * Get the results of the Filters performed, including the number of matching documents.
  1292. *
  1293. * Returns an array of Filters (keyed by $label, as passed to Jetpack_Search::set_filters()), containing the Filter and all resulting
  1294. * matching buckets, the url for applying/removing each bucket, etc.
  1295. *
  1296. * NOTE - if this is called before the search is performed, an empty array will be returned. Use the $aggregations class
  1297. * member if you need to access the raw filters set in Jetpack_Search::set_filters().
  1298. *
  1299. * @since 5.0.0
  1300. *
  1301. * @param WP_Query $query The optional original WP_Query to use for determining which filters are active. Defaults to the main query.
  1302. *
  1303. * @return array Array of filters applied and info about them.
  1304. */
  1305. public function get_filters( WP_Query $query = null ) {
  1306. if ( ! $query instanceof WP_Query ) {
  1307. global $wp_query;
  1308. $query = $wp_query;
  1309. }
  1310. $aggregation_data = $this->aggregations;
  1311. if ( empty( $aggregation_data ) ) {
  1312. return $aggregation_data;
  1313. }
  1314. $aggregation_results = $this->get_search_aggregations_results();
  1315. if ( ! $aggregation_results ) {
  1316. return $aggregation_data;
  1317. }
  1318. // NOTE - Looping over the _results_, not the original configured aggregations, so we get the 'real' data from ES.
  1319. foreach ( $aggregation_results as $label => $aggregation ) {
  1320. if ( empty( $aggregation ) ) {
  1321. continue;
  1322. }
  1323. $type = $this->aggregations[ $label ]['type'];
  1324. $aggregation_data[ $label ]['buckets'] = array();
  1325. $existing_term_slugs = array();
  1326. $tax_query_var = null;
  1327. // Figure out which terms are active in the query, for this taxonomy.
  1328. if ( 'taxonomy' === $this->aggregations[ $label ]['type'] ) {
  1329. $tax_query_var = $this->get_taxonomy_query_var( $this->aggregations[ $label ]['taxonomy'] );
  1330. if ( ! empty( $query->tax_query ) && ! empty( $query->tax_query->queries ) && is_array( $query->tax_query->queries ) ) {
  1331. foreach ( $query->tax_query->queries as $tax_query ) {
  1332. if ( is_array( $tax_query ) && $this->aggregations[ $label ]['taxonomy'] === $tax_query['taxonomy'] &&
  1333. 'slug' === $tax_query['field'] &&
  1334. is_array( $tax_query['terms'] ) ) {
  1335. $existing_term_slugs = array_merge( $existing_term_slugs, $tax_query['terms'] );
  1336. }
  1337. }
  1338. }
  1339. }
  1340. // Now take the resulting found aggregation items and generate the additional info about them, such as activation/deactivation url, name, count, etc.
  1341. $buckets = array();
  1342. if ( ! empty( $aggregation['buckets'] ) ) {
  1343. $buckets = (array) $aggregation['buckets'];
  1344. }
  1345. if ( 'date_histogram' === $type ) {
  1346. // re-order newest to oldest.
  1347. $buckets = array_reverse( $buckets );
  1348. }
  1349. // Some aggregation types like date_histogram don't support the max results parameter.
  1350. if ( is_int( $this->aggregations[ $label ]['count'] ) && count( $buckets ) > $this->aggregations[ $label ]['count'] ) {
  1351. $buckets = array_slice( $buckets, 0, $this->aggregations[ $label ]['count'] );
  1352. }
  1353. foreach ( $buckets as $item ) {
  1354. $query_vars = array();
  1355. $active = false;
  1356. $remove_url = null;
  1357. $name = '';
  1358. // What type was the original aggregation?
  1359. switch ( $type ) {
  1360. case 'taxonomy':
  1361. $taxonomy = $this->aggregations[ $label ]['taxonomy'];
  1362. $term = get_term_by( 'slug', $item['key'], $taxonomy );
  1363. if ( ! $term || ! $tax_query_var ) {
  1364. continue 2; // switch() is considered a looping structure.
  1365. }
  1366. $query_vars = array(
  1367. $tax_query_var => implode( '+', array_merge( $existing_term_slugs, array( $term->slug ) ) ),
  1368. );
  1369. $name = $term->name;
  1370. // Let's determine if this term is active or not.
  1371. if ( in_array( $item['key'], $existing_term_slugs, true ) ) {
  1372. $active = true;
  1373. $slug_count = count( $existing_term_slugs );
  1374. if ( $slug_count > 1 ) {
  1375. $remove_url = Jetpack_Search_Helpers::add_query_arg(
  1376. $tax_query_var,
  1377. rawurlencode( implode( '+', array_diff( $existing_term_slugs, array( $item['key'] ) ) ) )
  1378. );
  1379. } else {
  1380. $remove_url = Jetpack_Search_Helpers::remove_query_arg( $tax_query_var );
  1381. }
  1382. }
  1383. break;
  1384. case 'post_type':
  1385. $post_type = get_post_type_object( $item['key'] );
  1386. if ( ! $post_type || $post_type->exclude_from_search ) {
  1387. continue 2; // switch() is considered a looping structure.
  1388. }
  1389. $query_vars = array(
  1390. 'post_type' => $item['key'],
  1391. );
  1392. $name = $post_type->labels->singular_name;
  1393. // Is this post type active on this search?
  1394. $post_types = $query->get( 'post_type' );
  1395. if ( ! is_array( $post_types ) ) {
  1396. $post_types = array( $post_types );
  1397. }
  1398. if ( in_array( $item['key'], $post_types, true ) ) {
  1399. $active = true;
  1400. $post_type_count = count( $post_types );
  1401. // For the right 'remove filter' url, we need to remove the post type from the array, or remove the param entirely if it's the only one.
  1402. if ( $post_type_count > 1 ) {
  1403. $remove_url = Jetpack_Search_Helpers::add_query_arg(
  1404. 'post_type',
  1405. rawurlencode( implode( ',', array_diff( $post_types, array( $item['key'] ) ) ) )
  1406. );
  1407. } else {
  1408. $remove_url = Jetpack_Search_Helpers::remove_query_arg( 'post_type' );
  1409. }
  1410. }
  1411. break;
  1412. case 'date_histogram':
  1413. $timestamp = $item['key'] / 1000;
  1414. $current_year = $query->get( 'year' );
  1415. $current_month = $query->get( 'monthnum' );
  1416. $current_day = $query->get( 'day' );
  1417. switch ( $this->aggregations[ $label ]['interval'] ) {
  1418. case 'year':
  1419. $year = (int) gmdate( 'Y', $timestamp );
  1420. $query_vars = array(
  1421. 'year' => $year,
  1422. 'monthnum' => false,
  1423. 'day' => false,
  1424. );
  1425. $name = $year;
  1426. // Is this year currently selected?
  1427. if ( ! empty( $current_year ) && (int) $current_year === $year ) {
  1428. $active = true;
  1429. $remove_url = Jetpack_Search_Helpers::remove_query_arg( array( 'year', 'monthnum', 'day' ) );
  1430. }
  1431. break;
  1432. case 'month':
  1433. $year = (int) gmdate( 'Y', $timestamp );
  1434. $month = (int) gmdate( 'n', $timestamp );
  1435. $query_vars = array(
  1436. 'year' => $year,
  1437. 'monthnum' => $month,
  1438. 'day' => false,
  1439. );
  1440. $name = gmdate( 'F Y', $timestamp );
  1441. // Is this month currently selected?
  1442. if ( ! empty( $current_year ) && (int) $current_year === $year &&
  1443. ! empty( $current_month ) && (int) $current_month === $month ) {
  1444. $active = true;
  1445. $remove_url = Jetpack_Search_Helpers::remove_query_arg( array( 'year', 'monthnum' ) );
  1446. }
  1447. break;
  1448. case 'day':
  1449. $year = (int) gmdate( 'Y', $timestamp );
  1450. $month = (int) gmdate( 'n', $timestamp );
  1451. $day = (int) gmdate( 'j', $timestamp );
  1452. $query_vars = array(
  1453. 'year' => $year,
  1454. 'monthnum' => $month,
  1455. 'day' => $day,
  1456. );
  1457. $name = gmdate( 'F jS, Y', $timestamp );
  1458. // Is this day currently selected?
  1459. if ( ! empty( $current_year ) && (int) $current_year === $year &&
  1460. ! empty( $current_month ) && (int) $current_month === $month &&
  1461. ! empty( $current_day ) && (int) $current_day === $day ) {
  1462. $active = true;
  1463. $remove_url = Jetpack_Search_Helpers::remove_query_arg( array( 'day' ) );
  1464. }
  1465. break;
  1466. default:
  1467. continue 3; // switch() is considered a looping structure.
  1468. } // End switch.
  1469. break;
  1470. default:
  1471. // continue 2; // switch() is considered a looping structure.
  1472. } // End switch.
  1473. // Need to urlencode param values since add_query_arg doesn't.
  1474. $url_params = urlencode_deep( $query_vars );
  1475. $aggregation_data[ $label ]['buckets'][] = array(
  1476. 'url' => Jetpack_Search_Helpers::add_query_arg( $url_params ),
  1477. 'query_vars' => $query_vars,
  1478. 'name' => $name,
  1479. 'count' => $item['doc_count'],
  1480. 'active' => $active,
  1481. 'remove_url' => $remove_url,
  1482. 'type' => $type,
  1483. 'type_label' => $aggregation_data[ $label ]['name'],
  1484. 'widget_id' => ! empty( $aggregation_data[ $label ]['widget_id'] ) ? $aggregation_data[ $label ]['widget_id'] : 0,
  1485. );
  1486. } // End foreach.
  1487. } // End foreach.
  1488. /**
  1489. * Modify the aggregation filters returned by get_filters().
  1490. *
  1491. * Useful if you are setting custom filters outside of the supported filters (taxonomy, post_type etc.) and
  1492. * want to hook them up so they're returned when you call `get_filters()`.
  1493. *
  1494. * @module search
  1495. *
  1496. * @since 6.9.0
  1497. *
  1498. * @param array $aggregation_data The array of filters keyed on label.
  1499. * @param WP_Query $query The WP_Query object.
  1500. */
  1501. return apply_filters( 'jetpack_search_get_filters', $aggregation_data, $query );
  1502. }
  1503. /**
  1504. * Get the results of the facets performed.
  1505. *
  1506. * @deprecated 5.0 Please use Jetpack_Search::get_filters() instead.
  1507. *
  1508. * @see Jetpack_Search::get_filters()
  1509. *
  1510. * @return array $facets Array of facets applied and info about them.
  1511. */
  1512. public function get_search_facet_data() {
  1513. _deprecated_function( __METHOD__, 'jetpack-5.0', 'Jetpack_Search::get_filters()' );
  1514. return $this->get_filters();
  1515. }
  1516. /**
  1517. * Get the filters that are currently applied to this search.
  1518. *
  1519. * @since 5.0.0
  1520. *
  1521. * @return array Array of filters that were applied.
  1522. */
  1523. public function get_active_filter_buckets() {
  1524. $active_buckets = array();
  1525. $filters = $this->get_filters();
  1526. if ( ! is_array( $filters ) ) {
  1527. return $active_buckets;
  1528. }
  1529. foreach ( $filters as $filter ) {
  1530. if ( isset( $filter['buckets'] ) && is_array( $filter['buckets'] ) ) {
  1531. foreach ( $filter['buckets'] as $item ) {
  1532. if ( isset( $item['active'] ) && $item['active'] ) {
  1533. $active_buckets[] = $item;
  1534. }
  1535. }
  1536. }
  1537. }
  1538. return $active_buckets;
  1539. }
  1540. /**
  1541. * Get the filters that are currently applied to this search.
  1542. *
  1543. * @deprecated 5.0 Please use Jetpack_Search::get_active_filter_buckets() instead.
  1544. *
  1545. * @see Jetpack_Search::get_active_filter_buckets()
  1546. *
  1547. * @return array Array of filters that were applied.
  1548. */
  1549. public function get_current_filters() {
  1550. _deprecated_function( __METHOD__, 'jetpack-5.0', 'Jetpack_Search::get_active_filter_buckets()' );
  1551. return $this->get_active_filter_buckets();
  1552. }
  1553. /**
  1554. * Calculate the right query var to use for a given taxonomy.
  1555. *
  1556. * Allows custom code to modify the GET var that is used to represent a given taxonomy, via the jetpack_search_taxonomy_query_var filter.
  1557. *
  1558. * @since 5.0.0
  1559. *
  1560. * @param string $taxonomy_name The name of the taxonomy for which to get the query var.
  1561. *
  1562. * @return bool|string The query var to use for this taxonomy, or false if none found.
  1563. */
  1564. public function get_taxonomy_query_var( $taxonomy_name ) {
  1565. $taxonomy = get_taxonomy( $taxonomy_name );
  1566. if ( ! $taxonomy || is_wp_error( $taxonomy ) ) {
  1567. return false;
  1568. }
  1569. /**
  1570. * Modify the query var to use for a given taxonomy
  1571. *
  1572. * @module search
  1573. *
  1574. * @since 5.0.0
  1575. *
  1576. * @param string $query_var The current query_var for the taxonomy
  1577. * @param string $taxonomy_name The taxonomy name
  1578. */
  1579. return apply_filters( 'jetpack_search_taxonomy_query_var', $taxonomy->query_var, $taxonomy_name );
  1580. }
  1581. /**
  1582. * Takes an array of aggregation results, and ensures the array key ordering matches the key order in $desired
  1583. * which is the input order.
  1584. *
  1585. * Necessary because ES does not always return aggregations in the same order that you pass them in,
  1586. * and it should be possible to control the display order easily.
  1587. *
  1588. * @since 5.0.0
  1589. *
  1590. * @param array $aggregations Aggregation results to be reordered.
  1591. * @param array $desired Array with keys representing the desired ordering.
  1592. *
  1593. * @return array A new array with reordered keys, matching those in $desired.
  1594. */
  1595. public function fix_aggregation_ordering( array $aggregations, array $desired ) {
  1596. if ( empty( $aggregations ) || empty( $desired ) ) {
  1597. return $aggregations;
  1598. }
  1599. $reordered = array();
  1600. foreach ( array_keys( $desired ) as $agg_name ) {
  1601. if ( isset( $aggregations[ $agg_name ] ) ) {
  1602. $reordered[ $agg_name ] = $aggregations[ $agg_name ];
  1603. }
  1604. }
  1605. return $reordered;
  1606. }
  1607. /**
  1608. * Sends events to Tracks when a search filters widget is updated.
  1609. *
  1610. * @since 5.8.0
  1611. *
  1612. * @param string $option The option name. Only "widget_jetpack-search-filters" is cared about.
  1613. * @param array $old_value The old option value.
  1614. * @param array $new_value The new option value.
  1615. */
  1616. public function track_widget_updates( $option, $old_value, $new_value ) {
  1617. if ( 'widget_jetpack-search-filters' !== $option ) {
  1618. return;
  1619. }
  1620. $event = Jetpack_Search_Helpers::get_widget_tracks_value( $old_value, $new_value );
  1621. if ( ! $event ) {
  1622. return;
  1623. }
  1624. $tracking = new Automattic\Jetpack\Tracking();
  1625. $tracking->tracks_record_event(
  1626. wp_get_current_user(),
  1627. sprintf( 'jetpack_search_widget_%s', $event['action'] ),
  1628. $event['widget']
  1629. );
  1630. }
  1631. /**
  1632. * Moves any active search widgets to the inactive category.
  1633. *
  1634. * @since 5.9.0
  1635. */
  1636. public function move_search_widgets_to_inactive() {
  1637. if ( ! is_active_widget( false, false, Jetpack_Search_Helpers::FILTER_WIDGET_BASE, true ) ) {
  1638. return;
  1639. }
  1640. $sidebars_widgets = wp_get_sidebars_widgets();
  1641. if ( ! is_array( $sidebars_widgets ) ) {
  1642. return;
  1643. }
  1644. $changed = false;
  1645. foreach ( $sidebars_widgets as $sidebar => $widgets ) {
  1646. if ( 'wp_inactive_widgets' === $sidebar || 'orphaned_widgets' === substr( $sidebar, 0, 16 ) ) {
  1647. continue;
  1648. }
  1649. if ( is_array( $widgets ) ) {
  1650. foreach ( $widgets as $key => $widget ) {
  1651. if ( _get_widget_id_base( $widget ) === Jetpack_Search_Helpers::FILTER_WIDGET_BASE ) {
  1652. $changed = true;
  1653. array_unshift( $sidebars_widgets['wp_inactive_widgets'], $widget );
  1654. unset( $sidebars_widgets[ $sidebar ][ $key ] );
  1655. }
  1656. }
  1657. }
  1658. }
  1659. if ( $changed ) {
  1660. wp_set_sidebars_widgets( $sidebars_widgets );
  1661. }
  1662. }
  1663. /**
  1664. * Determine whether a given WP_Query should be handled by ElasticSearch.
  1665. *
  1666. * @param WP_Query $query The WP_Query object.
  1667. *
  1668. * @return bool
  1669. */
  1670. public function should_handle_query( $query ) {
  1671. /**
  1672. * Determine whether a given WP_Query should be handled by ElasticSearch.
  1673. *
  1674. * @module search
  1675. *
  1676. * @since 5.6.0
  1677. *
  1678. * @param bool $should_handle Should be handled by Jetpack Search.
  1679. * @param WP_Query $query The WP_Query object.
  1680. */
  1681. return apply_filters( 'jetpack_search_should_handle_query', $query->is_main_query() && $query->is_search(), $query );
  1682. }
  1683. /**
  1684. * Transforms an array with fields name as keys and boosts as value into
  1685. * shorthand "caret" format.
  1686. *
  1687. * @param array $fields_boost [ "title" => "2", "content" => "1" ].
  1688. *
  1689. * @return array [ "title^2", "content^1" ]
  1690. */
  1691. private function _get_caret_boosted_fields( array $fields_boost ) { // phpcs:ignore PSR2.Methods.MethodDeclaration.Underscore
  1692. $caret_boosted_fields = array();
  1693. foreach ( $fields_boost as $field => $boost ) {
  1694. $caret_boosted_fields[] = "$field^$boost";
  1695. }
  1696. return $caret_boosted_fields;
  1697. }
  1698. /**
  1699. * Apply a multiplier to boost values.
  1700. *
  1701. * @param array $fields_boost [ "title" => 2, "content" => 1 ].
  1702. * @param array $fields_boost_multiplier [ "title" => 0.1234 ].
  1703. *
  1704. * @return array [ "title" => "0.247", "content" => "1.000" ]
  1705. */
  1706. private function _apply_boosts_multiplier( array $fields_boost, array $fields_boost_multiplier ) { // phpcs:ignore PSR2.Methods.MethodDeclaration.Underscore
  1707. foreach ( $fields_boost as $field_name => $field_boost ) {
  1708. if ( isset( $fields_boost_multiplier[ $field_name ] ) ) {
  1709. $fields_boost[ $field_name ] *= $fields_boost_multiplier[ $field_name ];
  1710. }
  1711. // Set a floor and format the number as string.
  1712. $fields_boost[ $field_name ] = number_format(
  1713. max( 0.001, $fields_boost[ $field_name ] ),
  1714. 3,
  1715. '.',
  1716. ''
  1717. );
  1718. }
  1719. return $fields_boost;
  1720. }
  1721. }