phrases.php
8.83 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
<?php
/**
* /lib/phrases.php
*
* @package Relevanssi
* @author Mikko Saari
* @license https://wordpress.org/about/gpl/ GNU General Public License
* @see https://www.relevanssi.com/
*/
/**
* Extracts phrases from the search query.
*
* Finds all phrases wrapped in quotes (curly or straight) from the search
* query.
*
* @param string $query The search query.
*
* @return array An array of phrases (strings).
*/
function relevanssi_extract_phrases( string $query ) {
// iOS uses “” or „“ as the default quotes, so Relevanssi needs to
// understand those as well.
$normalized_query = str_replace( array( '”', '“', '„' ), '"', $query );
$pos = relevanssi_stripos( $normalized_query, '"' );
$phrases = array();
while ( false !== $pos ) {
if ( $pos + 2 > relevanssi_strlen( $normalized_query ) ) {
$pos = false;
continue;
}
$start = relevanssi_stripos( $normalized_query, '"', $pos );
$end = false;
if ( false !== $start ) {
$end = relevanssi_stripos( $normalized_query, '"', $start + 2 );
}
if ( false === $end ) {
// Just one " in the query.
$pos = $end;
continue;
}
$phrase = relevanssi_substr(
$normalized_query,
$start + 1,
$end - $start - 1
);
$phrase = trim( $phrase );
// Do not count single-word phrases as phrases.
if ( relevanssi_is_multiple_words( $phrase ) ) {
$phrases[] = $phrase;
}
$pos = $end + 1;
}
return $phrases;
}
/**
* Generates the MySQL code for restricting the search to phrase hits.
*
* This function uses relevanssi_extract_phrases() to figure out the phrases in
* the search query, then generates MySQL queries to restrict the search to the
* posts containing those phrases in the title, content, taxonomy terms or meta
* fields.
*
* @global array $relevanssi_variables The global Relevanssi variables.
*
* @param string $search_query The search query.
* @param string $operator The search operator (AND or OR).
*
* @return string $queries If not phrase hits are found, an empty string;
* otherwise MySQL queries to restrict the search.
*/
function relevanssi_recognize_phrases( $search_query, $operator = 'AND' ) {
global $relevanssi_variables;
$phrases = relevanssi_extract_phrases( $search_query );
$all_queries = array();
if ( 0 === count( $phrases ) ) {
return $all_queries;
}
/**
* Filters the custom fields for phrase matching.
*
* If you don't want the phrase matching to target custom fields, you can
* have this filter hook return an empty array.
*
* @param array $custom_fields An array of custom field names.
*/
$custom_fields = apply_filters( 'relevanssi_phrase_custom_fields', relevanssi_get_custom_fields() );
/**
* Filters the taxonomies for phrase matching.
*
* If you don't want the phrase matching to target taxonomies, you can have
* this filter hook return an empty array.
*
* @param array $taxonomies An array of taxonomy names.
*/
$taxonomies = apply_filters( 'relevanssi_phrase_taxonomies', get_option( 'relevanssi_index_taxonomies_list', array() ) );
$excerpts = get_option( 'relevanssi_index_excerpt', 'off' );
$phrase_queries = array();
$queries = array();
if (
isset( $relevanssi_variables['phrase_targets'] ) &&
is_array( $relevanssi_variables['phrase_targets'] )
) {
$non_targeted_phrases = array();
foreach ( $phrases as $phrase ) {
if (
isset( $relevanssi_variables['phrase_targets'][ $phrase ] ) &&
function_exists( 'relevanssi_targeted_phrases' )
) {
$queries = relevanssi_targeted_phrases( $phrase );
} else {
$non_targeted_phrases[] = $phrase;
}
}
$phrases = $non_targeted_phrases;
}
$queries = array_merge(
$queries,
relevanssi_generate_phrase_queries(
$phrases,
$taxonomies,
$custom_fields,
$excerpts
)
);
$phrase_queries = array();
foreach ( $queries as $phrase => $p_queries ) {
$pq_array = array();
foreach ( $p_queries as $query ) {
$pq_array[] = "relevanssi.{$query['target']} IN {$query['query']}";
}
$p_queries = implode( ' OR ', $pq_array );
$all_queries[] = "($p_queries)";
$phrase_queries[ $phrase ] = $p_queries;
}
$operator = strtoupper( $operator );
if ( 'AND' !== $operator && 'OR' !== $operator ) {
$operator = 'AND';
}
if ( ! empty( $all_queries ) ) {
$all_queries = ' AND ( ' . implode( ' ' . $operator . ' ', $all_queries ) . ' ) ';
}
return array(
'and' => $all_queries,
'or' => $phrase_queries,
);
}
/**
* Generates the phrase queries from phrases.
*
* Takes in phrases and a bunch of parameters and generates the MySQL queries
* that restrict the main search query to only posts that have the phrase.
*
* @param array $phrases A list of phrases to handle.
* @param array $taxonomies An array of taxonomy names to use.
* @param array|string $custom_fields A list of custom field names to use,
* "visible", or "all".
* @param string $excerpts If 'on', include excerpts.
*
* @global object $wpdb The WordPress database interface.
*
* @return array An array of queries sorted by phrase.
*/
function relevanssi_generate_phrase_queries(
array $phrases,
array $taxonomies,
$custom_fields,
string $excerpts
): array {
global $wpdb;
$status = relevanssi_valid_status_array();
// Add "inherit" to the list of allowed statuses to include attachments.
if ( ! strstr( $status, 'inherit' ) ) {
$status .= ",'inherit'";
}
$phrase_queries = array();
foreach ( $phrases as $phrase ) {
$queries = array();
$phrase = $wpdb->esc_like( $phrase );
$phrase = str_replace( array( '‘', '’', "'", '"', '”', '“', '“', '„', '´' ), '_', $phrase );
$title_phrase = $phrase;
$phrase = htmlspecialchars( $phrase );
/**
* Filters each phrase before it's passed through esc_sql() and used in
* the MySQL query. You can use this filter hook to for example run
* htmlentities() on the phrase in case your database needs that.
*
* @param string $phrase The phrase after quotes are replaced with a
* MySQL wild card and the phrase has been passed through esc_like() and
* htmlspecialchars().
*/
$phrase = esc_sql( apply_filters( 'relevanssi_phrase', $phrase ) );
$excerpt = '';
if ( 'on' === $excerpts ) {
$excerpt = "OR post_excerpt LIKE '%$phrase%'";
}
$query = "(SELECT ID FROM $wpdb->posts
WHERE (post_content LIKE '%$phrase%'
OR post_title LIKE '%$title_phrase%' $excerpt)
AND post_status IN ($status))";
$queries[] = array(
'query' => $query,
'target' => 'doc',
);
if ( ! empty( $taxonomies ) ) {
$taxonomies_escaped = implode( "','", array_map( 'esc_sql', $taxonomies ) );
$taxonomies_sql = "AND s.taxonomy IN ('$taxonomies_escaped')";
$query = "(SELECT ID FROM
$wpdb->posts as p,
$wpdb->term_relationships as r,
$wpdb->term_taxonomy as s, $wpdb->terms as t
WHERE r.term_taxonomy_id = s.term_taxonomy_id
AND s.term_id = t.term_id AND p.ID = r.object_id
$taxonomies_sql
AND t.name LIKE '%$phrase%' AND p.post_status IN ($status))";
$queries[] = array(
'query' => $query,
'target' => 'doc',
);
}
if ( ! empty( $custom_fields ) ) {
$keys = '';
if ( is_array( $custom_fields ) ) {
if ( ! in_array( '_relevanssi_pdf_content', $custom_fields, true ) ) {
array_push( $custom_fields, '_relevanssi_pdf_content' );
}
if ( strpos( implode( ' ', $custom_fields ), '%' ) ) {
// ACF repeater fields involved.
$custom_fields_regexp = str_replace( '%', '.+', implode( '|', $custom_fields ) );
$keys = "AND m.meta_key REGEXP ('$custom_fields_regexp')";
} else {
$custom_fields_escaped = implode(
"','",
array_map(
'esc_sql',
$custom_fields
)
);
$keys = "AND m.meta_key IN ('$custom_fields_escaped')";
}
}
if ( 'visible' === $custom_fields ) {
$keys = "AND (m.meta_key NOT LIKE '\_%' OR m.meta_key = '_relevanssi_pdf_content')";
}
$query = "(SELECT ID
FROM $wpdb->posts AS p, $wpdb->postmeta AS m
WHERE p.ID = m.post_id
$keys
AND m.meta_value LIKE '%$phrase%'
AND p.post_status IN ($status))";
$queries[] = array(
'query' => $query,
'target' => 'doc',
);
}
/**
* Filters the phrase queries.
*
* Relevanssi Premium uses this filter hook to add Premium-specific
* phrase queries.
*
* @param array $queries The MySQL queries for phrase matching.
* @param string $phrase The current phrase.
* @param string $status A string containing post statuses.
*
* @return array An array of phrase queries, where each query is an
* array that has the actual MySQL query in 'query' and the target
* column ('doc' or 'item') in the Relevanssi index table in 'target'.
*/
$queries = apply_filters( 'relevanssi_phrase_queries', $queries, $phrase, $status );
$phrase_queries[ $phrase ] = $queries;
}
return $phrase_queries;
}