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
betterdocs / includes / FrontEnd / SearchExtender.php

SearchExtender.php in BetterDocs – AI Documentation, Knowledge Base, MCP Server, Docs, Wikis, FAQ & Chatbot 4.9.4, at includes/FrontEnd/SearchExtender.php

123 lines 4.4 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 namespace WPDeveloper\BetterDocs\FrontEnd;
4
5 if ( ! defined( 'ABSPATH' ) ) {
6 exit;
7 }
8
9 /**
10 * Extends WordPress search SQL on `docs` queries to also match docs whose
11 * assigned `doc_tag` or `doc_category` term names contain the search term.
12 *
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
16 * the `docs` post type — including WP core's `GET /wp/v2/docs?search=...`,
17 * BetterDocs' `/betterdocs/v1/search`, and the shortcode/widget AJAX paths.
18 */
19 class SearchExtender {
20 public function __construct() {
21 add_filter( 'posts_search', [ $this, 'extend_search' ], 20, 2 );
22 add_filter( 'posts_search_orderby', [ $this, 'rank_title_matches' ], 20, 2 );
23 }
24
25 /**
26 * Inject an OR clause that matches docs whose related taxonomy term names
27 * contain the search term.
28 *
29 * @param string $search The search SQL clause (already begins with " AND (").
30 * @param \WP_Query $query The WP_Query instance.
31 * @return string Modified search SQL.
32 */
33 public function extend_search( $search, $query ) {
34 global $wpdb;
35
36 if ( empty( $search ) || ! $this->is_docs_query( $query ) ) {
37 return $search;
38 }
39
40 $search_term = isset( $query->query_vars['s'] ) ? (string) $query->query_vars['s'] : '';
41 if ( $search_term === '' ) {
42 return $search;
43 }
44
45 $taxonomies = [ 'doc_tag', 'doc_category' ];
46 $like = '%' . $wpdb->esc_like( $search_term ) . '%';
47
48 $placeholders = implode( ',', array_fill( 0, count( $taxonomies ), '%s' ) );
49 $args = $taxonomies;
50 $args[] = $like;
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
55 $subquery = $wpdb->prepare(
56 "{$wpdb->posts}.ID IN (
57 SELECT DISTINCT tr.object_id
58 FROM {$wpdb->term_relationships} tr
59 INNER JOIN {$wpdb->term_taxonomy} tt ON tr.term_taxonomy_id = tt.term_taxonomy_id
60 INNER JOIN {$wpdb->terms} t ON tt.term_id = t.term_id
61 WHERE tt.taxonomy IN ({$placeholders}) AND t.name LIKE %s
62 )",
63 $args
64 );
65 // phpcs:enable WordPress.DB.PreparedSQL.InterpolatedNotPrepared
66
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;
121 }
122 }
123