| @@ -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 = [] ) { |
| @@ -514,8 +539,68 @@ | ||
| 514 | 539 | return $query->find_many(); |
| 515 | 540 | } |
| 516 | 541 | |
| 517 | 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 ); | |
| 600 | + } | |
| 601 | + | |
| 602 | + /** | |
| 518 | 603 | * Returns the most recently modified cornerstone content of a post type. |
| 519 | 604 | * |
| 520 | 605 | * @param string $post_type The post type. |
| 521 | 606 | * @param int|null $limit The maximum number of posts to return. |
| @@ -534,8 +619,26 @@ | ||
| 534 | 619 | $query->limit( $limit ); |
| 535 | 620 | } |
| 536 | 621 | |
| 537 | 622 | return $query->find_many(); |
| 623 | + } | |
| 624 | + | |
| 625 | + /** | |
| 626 | + * Returns the most recently modified about page based on schema_page_type. | |
| 627 | + * | |
| 628 | + * @param string $post_type The post type. | |
| 629 | + * | |
| 630 | + * @return Indexable|false The about page if its there. | |
| 631 | + */ | |
| 632 | + public function get_most_recent_about_page( $post_type ) { | |
| 633 | + $query = $this->query() | |
| 634 | + ->where( 'object_type', 'post' ) | |
| 635 | + ->where( 'object_sub_type', $post_type ) | |
| 636 | + ->where( 'schema_page_type', 'AboutPage' ) | |
| 637 | + ->where_raw( '( is_public IS NULL OR is_public = 1 )' ) | |
| 638 | + ->order_by_desc( 'object_last_modified' ); | |
| 639 | + | |
| 640 | + return $query->find_one(); | |
| 538 | 641 | } |
| 539 | 642 | |
| 540 | 643 | /** |
| 541 | 644 | * Returns the most recently modified posts with keywords of a post type. |