PluginProbe
Yoast SEO – Advanced SEO with real-time guidance and built-in AI / trunk
Yoast SEO – Advanced SEO with real-time guidance and built-in AI vtrunk
28.5 28.4 28.3 28.2 28.1 28.0 27.9 27.8 27.7 27.6 27.5 trunk 18.0 18.1 18.2 18.3 18.4 18.4.1 18.5 18.5.1 18.6 18.7 18.8 18.9 19.0 All 129 releases
← All changes | src/repositories/indexable-repository.php +87 -2 27.7 → trunk View file →
@@ -18,8 +18,23 @@
18 18 */
19 19 class Indexable_Repository {
20 20
21 21 /**
22 + * The maximum number of comma-separated phrases honoured by a title-keyword search.
23 + * Any phrases beyond this are ignored so an oversized list cannot blow up the query.
24 + *
25 + * @var int
26 + */
27 + public const MAX_TITLE_KEYWORD_PHRASES = 10;
28 +
29 + /**
30 + * The maximum page size honoured by a title-keyword search.
31 + *
32 + * @var int
33 + */
34 + public const MAX_TITLE_KEYWORD_PAGE_SIZE = 100;
35 +
36 + /**
22 37 * The indexable builder.
23 38 *
24 39 * @var Indexable_Builder
25 40 */
@@ -397,8 +412,18 @@
397 412 }
398 413
399 414 $indexables_to_create = \array_diff( $object_ids, $indexables_available );
400 415
416 + if ( ! empty( $indexables_to_create ) ) {
417 + // Warm the object caches for the whole batch, so each build below does not trigger its own uncached queries.
418 + if ( $object_type === 'post' ) {
419 + \_prime_post_caches( $indexables_to_create );
420 + }
421 + elseif ( $object_type === 'term' ) {
422 + \_prime_term_caches( $indexables_to_create );
423 + }
424 + }
425 +
401 426 foreach ( $indexables_to_create as $indexable_to_create ) {
402 427 $indexables[] = $this->builder->build_for_id_and_type( $indexable_to_create, $object_type );
403 428 }
404 429 }
@@ -408,9 +433,9 @@
408 433
409 434 /**
410 435 * Finds the indexables by id's.
411 436 *
412 - * @param array $indexable_ids The indexable id's.
437 + * @param int[] $indexable_ids The indexable id's.
413 438 *
414 439 * @return Indexable[] The found indexables.
415 440 */
416 441 public function find_by_ids( array $indexable_ids ) {
@@ -465,9 +490,9 @@
465 490 /**
466 491 * Returns all subpages with a given post_parent.
467 492 *
468 493 * @param int $post_parent The post parent.
469 - * @param array $exclude_ids The id's to exclude.
494 + * @param int[] $exclude_ids The id's to exclude.
470 495 *
471 496 * @return Indexable[] array of indexables.
472 497 */
473 498 public function get_subpages_by_post_parent( $post_parent, $exclude_ids = [] ) {
@@ -511,8 +536,68 @@
511 536 $query->order_by_desc( 'object_last_modified' )
512 537 ->limit( $limit );
513 538
514 539 return $query->find_many();
540 + }
541 +
542 + /**
543 + * Finds posts whose breadcrumb title contains any of the given comma-separated phrases.
544 + *
545 + * The search string is a comma-separated list. Each value is matched as a whole
546 + * contiguous substring of the breadcrumb title, and a post is returned when it
547 + * contains any one of the values (a logical OR between values). For example,
548 + * "hiking boots, trail" returns posts whose title contains "hiking boots" or "trail".
549 + *
550 + * Results are paginated and ordered most recently modified first (with the indexable
551 + * id as a stable tiebreaker), so requesting a later page returns older matches.
552 + *
553 + * At most self::MAX_TITLE_KEYWORD_PHRASES phrases are honoured; any beyond that are ignored.
554 + * The page size is clamped to the range 1..self::MAX_TITLE_KEYWORD_PAGE_SIZE.
555 + *
556 + * @param string $keywords The comma-separated phrases to match against the breadcrumb title.
557 + * @param int $page The page of results to return, 1-based.
558 + * @param int $page_size The number of posts per page.
559 + * @param string $post_type The post type to restrict the search to.
560 + *
561 + * @return Indexable[] The matching indexables for the requested page, ordered by most recently modified.
562 + */
563 + public function find_posts_by_title_keywords( string $keywords, int $page = 1, int $page_size = 10, string $post_type = 'post' ) {
564 + $phrases = \array_map( 'trim', \explode( ',', $keywords ) );
565 + $phrases = \array_filter(
566 + $phrases,
567 + static function ( $phrase ) {
568 + return $phrase !== '';
569 + },
570 + );
571 + $phrases = \array_slice( $phrases, 0, self::MAX_TITLE_KEYWORD_PHRASES );
572 +
573 + // An empty search must not degrade into matching every post.
574 + if ( empty( $phrases ) ) {
575 + return [];
576 + }
577 +
578 + $likes = \array_fill( 0, \count( $phrases ), 'breadcrumb_title LIKE %s' );
579 + $params = \array_map(
580 + function ( $phrase ) {
581 + return '%' . $this->wpdb->esc_like( $phrase ) . '%';
582 + },
583 + $phrases,
584 + );
585 +
586 + $page_size = \min( \max( 1, $page_size ), self::MAX_TITLE_KEYWORD_PAGE_SIZE );
587 + $offset = ( ( \max( 1, $page ) - 1 ) * $page_size );
588 +
589 + $indexables = $this->query()
590 + ->where( 'object_type', 'post' )
591 + ->where( 'object_sub_type', $post_type )
592 + ->where_raw( '( ' . \implode( ' OR ', $likes ) . ' )', \array_values( $params ) )
593 + ->order_by_desc( 'object_last_modified' )
594 + ->order_by_desc( 'id' )
595 + ->limit( $page_size )
596 + ->offset( $offset )
597 + ->find_many();
598 +
599 + return \array_map( [ $this, 'upgrade_indexable' ], $indexables );
515 600 }
516 601
517 602 /**
518 603 * Returns the most recently modified cornerstone content of a post type.