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 +58 -11 4.9.0 → 4.9.4 View file →
@@ -9,9 +9,11 @@
9 9 /**
10 10 * Extends WordPress search SQL on `docs` queries to also match docs whose
11 11 * assigned `doc_tag` or `doc_category` term names contain the search term.
12 12 *
13 - * 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
14 16 * the `docs` post type — including WP core's `GET /wp/v2/docs?search=...`,
15 17 * BetterDocs' `/betterdocs/v1/search`, and the shortcode/widget AJAX paths.
16 18 */
17 19 class SearchExtender {
@@ -16,8 +18,9 @@
16 18 */
17 19 class SearchExtender {
18 20 public function __construct() {
19 21 add_filter( 'posts_search', [ $this, 'extend_search' ], 20, 2 );
22 + add_filter( 'posts_search_orderby', [ $this, 'rank_title_matches' ], 20, 2 );
20 23 }
21 24
22 25 /**
23 26 * Inject an OR clause that matches docs whose related taxonomy term names
@@ -29,21 +32,12 @@
29 32 */
30 33 public function extend_search( $search, $query ) {
31 34 global $wpdb;
32 35
33 - if ( empty( $search ) ) {
36 + if ( empty( $search ) || ! $this->is_docs_query( $query ) ) {
34 37 return $search;
35 38 }
36 39
37 - $post_type = isset( $query->query_vars['post_type'] ) ? $query->query_vars['post_type'] : '';
38 - if ( is_array( $post_type ) ) {
39 - if ( ! in_array( 'docs', $post_type, true ) ) {
40 - return $search;
41 - }
42 - } elseif ( $post_type !== 'docs' ) {
43 - return $search;
44 - }
45 -
46 40 $search_term = isset( $query->query_vars['s'] ) ? (string) $query->query_vars['s'] : '';
47 41 if ( $search_term === '' ) {
48 42 return $search;
49 43 }
@@ -70,6 +64,59 @@
70 64 );
71 65 // phpcs:enable WordPress.DB.PreparedSQL.InterpolatedNotPrepared
72 66
73 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;
74 121 }
75 122 }