PluginProbe
BetterDocs – AI Documentation, Knowledge Base, MCP Server, Docs, Wikis, FAQ & Chatbot / 4.9.4
BetterDocs – AI Documentation, Knowledge Base, MCP Server, Docs, Wikis, FAQ & Chatbot v4.9.4
4.9.4 4.9.3 4.9.2 4.9.1 4.9.0 4.8.2 4.8.1 4.8.0 4.7.0 4.6.2 4.6.1 4.6.0 4.5.6 4.5.5 4.5.4 4.5.3 4.5.2 4.5.1 4.5.0 4.4.1 4.4.0 3.3.4 3.4.0 3.4.1 3.4.2 All 202 releases
← All changes | includes/FrontEnd/SearchExtender.php +66 -11 4.5.2 → 4.9.4 View file →
@@ -1,13 +1,19 @@
1 1 <?php
2 2
3 3 namespace WPDeveloper\BetterDocs\FrontEnd;
4 4
5 +if ( ! defined( 'ABSPATH' ) ) {
6 + exit;
7 +}
8 +
5 9 /**
6 10 * Extends WordPress search SQL on `docs` queries to also match docs whose
7 11 * assigned `doc_tag` or `doc_category` term names contain the search term.
8 12 *
9 - * The filter is registered globally so it applies to every search query against
13 + * Also ranks exact and leading title matches first in those searches.
14 + *
15 + * The filters are registered globally so they apply to every search query against
10 16 * the `docs` post type — including WP core's `GET /wp/v2/docs?search=...`,
11 17 * BetterDocs' `/betterdocs/v1/search`, and the shortcode/widget AJAX paths.
12 18 */
13 19 class SearchExtender {
@@ -12,8 +18,9 @@
12 18 */
13 19 class SearchExtender {
14 20 public function __construct() {
15 21 add_filter( 'posts_search', [ $this, 'extend_search' ], 20, 2 );
22 + add_filter( 'posts_search_orderby', [ $this, 'rank_title_matches' ], 20, 2 );
16 23 }
17 24
18 25 /**
19 26 * Inject an OR clause that matches docs whose related taxonomy term names
@@ -25,21 +32,12 @@
25 32 */
26 33 public function extend_search( $search, $query ) {
27 34 global $wpdb;
28 35
29 - if ( empty( $search ) ) {
36 + if ( empty( $search ) || ! $this->is_docs_query( $query ) ) {
30 37 return $search;
31 38 }
32 39
33 - $post_type = isset( $query->query_vars['post_type'] ) ? $query->query_vars['post_type'] : '';
34 - if ( is_array( $post_type ) ) {
35 - if ( ! in_array( 'docs', $post_type, true ) ) {
36 - return $search;
37 - }
38 - } elseif ( $post_type !== 'docs' ) {
39 - return $search;
40 - }
41 -
42 40 $search_term = isset( $query->query_vars['s'] ) ? (string) $query->query_vars['s'] : '';
43 41 if ( $search_term === '' ) {
44 42 return $search;
45 43 }
@@ -50,8 +48,11 @@
50 48 $placeholders = implode( ',', array_fill( 0, count( $taxonomies ), '%s' ) );
51 49 $args = $taxonomies;
52 50 $args[] = $like;
53 51
52 + // $placeholders is a generated run of %s tokens bound via $args below; all
53 + // interpolated identifiers are $wpdb core table names, not user input.
54 + // phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared
54 55 $subquery = $wpdb->prepare(
55 56 "{$wpdb->posts}.ID IN (
56 57 SELECT DISTINCT tr.object_id
57 58 FROM {$wpdb->term_relationships} tr
@@ -60,8 +61,62 @@
60 61 WHERE tt.taxonomy IN ({$placeholders}) AND t.name LIKE %s
61 62 )",
62 63 $args
63 64 );
65 + // phpcs:enable WordPress.DB.PreparedSQL.InterpolatedNotPrepared
64 66
65 67 return preg_replace( '/^\s*AND\s*\(/', " AND ({$subquery} OR ", $search, 1 );
68 + }
69 +
70 + /**
71 + * Rank docs whose title is the search term first, then titles that start
72 + * with it, ahead of WordPress's own relevance buckets. Equal matches are
73 + * listed A to Z instead of by publish date, which sorted the oldest partial
74 + * matches above a newer exact match (betterdocs/betterdocs#182).
75 + *
76 + * Only refines WordPress's relevance ordering: when a query asks for an
77 + * explicit orderby, WordPress passes an empty clause and it is left alone.
78 + *
79 + * @param string $search_orderby The relevance ORDER BY clause built by WordPress.
80 + * @param \WP_Query $query The WP_Query instance.
81 + * @return string Modified ORDER BY clause.
82 + */
83 + public function rank_title_matches( $search_orderby, $query ) {
84 + global $wpdb;
85 +
86 + if ( empty( $search_orderby ) || ! $this->is_docs_query( $query ) ) {
87 + return $search_orderby;
88 + }
89 +
90 + // Match the title the way a visitor typed it: ignore surrounding and
91 + // repeated whitespace, and a pair of quotes around the whole term.
92 + $search_term = isset( $query->query_vars['s'] ) ? (string) $query->query_vars['s'] : '';
93 + $search_term = trim( preg_replace( '/\s+/u', ' ', $search_term ) );
94 + if ( strlen( $search_term ) > 1 && '"' === $search_term[0] && '"' === substr( $search_term, -1 ) ) {
95 + $search_term = trim( substr( $search_term, 1, -1 ) );
96 + }
97 + if ( $search_term === '' ) {
98 + return $search_orderby;
99 + }
100 +
101 + // LIKE without a wildcard is a case-insensitive exact match.
102 + $title_rank = $wpdb->prepare(
103 + "(CASE WHEN {$wpdb->posts}.post_title LIKE %s THEN 0 WHEN {$wpdb->posts}.post_title LIKE %s THEN 1 ELSE 2 END)",
104 + $wpdb->esc_like( $search_term ),
105 + $wpdb->esc_like( $search_term ) . '%'
106 + );
107 +
108 + return "{$title_rank}, {$search_orderby}, {$wpdb->posts}.post_title ASC";
109 + }
110 +
111 + /**
112 + * Whether a query searches the `docs` post type.
113 + *
114 + * @param \WP_Query $query The WP_Query instance.
115 + * @return bool
116 + */
117 + private function is_docs_query( $query ) {
118 + $post_type = isset( $query->query_vars['post_type'] ) ? $query->query_vars['post_type'] : '';
119 +
120 + return is_array( $post_type ) ? in_array( 'docs', $post_type, true ) : 'docs' === $post_type;
66 121 }
67 122 }