Usage
nebula()->rest_autocomplete_search()
Parameters
term
(Required) (String) Search keyword or phrase
Default: None
Parameter Notes
term is passed in an AJAX data object.
Examples
Globally exclude attachments and menus from autocomplete search results.
add_filter('nebula_autocomplete_ignore_types', function($ignore_post_types){
$ignore_post_types[] = 'attachment';
$ignore_post_types[] = 'menu';
return $ignore_post_types;
});
Globally ignore individual posts from autocomplete search results.
add_filter('nebula_autocomplete_ignore_ids', function($ignore_post_ids){
$ignore_post_ids[] = 123;
$ignore_post_ids[] = 456;
return $ignore_post_ids;
});
Additional Notes
This functions searches post data as well as custom field data.
Post types can specifically be excluded from autocomplete searches with the filter nebula_autocomplete_ignore_types.
Individual posts can specifically be excluded from autocomplete searches with the filter nebula_autocomplete_ignore_ids.
See examples.
Also, installing/activating Relevanssi will bypass the manual query logic (and therefore the need to ignore post types).
Source File
Located in /libs/Functions.php on line 2602.
3 Hooks
Find these filters and actions in the source code below to hook into them. Use do_action() and add_filter() in your functions file or plugin.
Filters
"nebula_autocomplete_ignore_ids""nebula_autocomplete_ignore_types"
"nebula_autocomplete_ignore_ids"
Need a new filter hook? Request one here.
Actions
This function has no action hooks available. Request one?Note: This function contains 4 to-do comments.
public function rest_autocomplete_search(){
if ( $this->is_minimal_mode() ){return null;}
$timer_name = $this->timer('Autocomplete Search', 'start', '[Nebula] Search');
if ( isset($this->super->get['term']) ){
ini_set('memory_limit', '256M'); //@todo Nebula 0: Remove these when possible...
$term = sanitize_text_field(trim($this->super->get['term']));
if ( empty($term) ){
return null;
}
$types = 'any';
if ( isset($this->super->get['types']) ){
$types = explode(',', sanitize_text_field(trim($this->super->get['types'])));
//This is not necessary
$types = array_filter($types, function($type){
return $type !== 'nebula_cf7_submits';
});
}
//Prepare the standard WP search query parameters (do not include custom fields here).
//This is used by both Relevanssi -or- the combined query later
$initial_query_args = array(
'post_type' => $types,
'post_status' => 'publish',
'posts_per_page' => 10, //Do not use numberposts or any other parameter instead here
's' => $term,
);
if ( function_exists('relevanssi_do_query') ){ //If the Relevanssi plugin is active use its engine
$should_be_sorted = false; //Let Relevanssi determine result order
$relevanssi_query_prep = new WP_Query();
$relevanssi_query_prep->parse_query($initial_query_args); //Must be performed this way to retrieve Relevanssi results
$relevanssi_autocomplete = relevanssi_do_query($relevanssi_query_prep); //Run the query
foreach ( $relevanssi_autocomplete as $post ){
$ignore_post_ids = apply_filters('nebula_autocomplete_ignore_ids', array()); //Allow individual posts to be globally ignored from autocomplete search
if ( in_array($post->ID, $ignore_post_ids) || !get_the_title($post->ID) ){ //Ignore results without titles
continue;
}
$suggestion = array();
similar_text(strtolower($term), strtolower(esc_html(get_the_title($post->ID))), $suggestion['similarity']); //Determine how similar the query is to this post title
$suggestion['label'] = esc_html(get_the_title($post->ID));
$suggestion['link'] = get_permalink($post->ID);
$suggestion['classes'] = 'type-' . get_post_type($post->ID) . ' id-' . $post->ID . ' slug-' . $post->post_name . ' similarity-' . str_replace('.', '_', number_format($suggestion['similarity'], 2));
if ( $post->ID == get_option('page_on_front') ){
$suggestion['classes'] .= ' page-home';
} elseif ( is_sticky($post->ID) ){ //@TODO "Nebula" 0: If sticky post. is_sticky() does not work here?
$suggestion['classes'] .= ' sticky-post';
}
$suggestion['classes'] .= $this->close_or_exact($suggestion['similarity']);
$suggestions[] = $suggestion;
}
} else { //Manually find relevant posts
$query1 = new WP_Query($initial_query_args); //Run the first query with the initial arguments now
//Search custom fields now too
$query2 = new WP_Query(array(
'post_type' => $types,
'post_status' => 'publish',
'posts_per_page' => 10, //Do not use numberposts or any other parameter instead here
'meta_query' => array(
array(
'value' => $term,
'compare' => 'LIKE'
)
)
));
//Combine the above queries
$autocomplete_query = new WP_Query();
$autocomplete_query->posts = array_unique(array_merge($query1->posts, $query2->posts), SORT_REGULAR); //Is this the right way to do it? Or should we use parse_query here?
$autocomplete_query->post_count = count($autocomplete_query->posts);
$ignore_post_types = apply_filters('nebula_autocomplete_ignore_types', array('nebula_cf7_submits')); //Allow post types to be globally ignored from autocomplete search
$ignore_post_ids = apply_filters('nebula_autocomplete_ignore_ids', array()); //Allow individual posts to be globally ignored from autocomplete search
$suggestions = array();
$should_be_sorted = true; //Re-sort results by our similarity score
//Loop through the posts
if ( $autocomplete_query->have_posts() ){
while ( $autocomplete_query->have_posts() ){
$autocomplete_query->the_post();
if ( in_array(get_the_id(), $ignore_post_ids) || !get_the_title() ){ //Ignore results without titles
continue;
}
$post = get_post();
$suggestion = array();
similar_text(strtolower($term), strtolower(esc_html(get_the_title())), $suggestion['similarity']); //Determine how similar the query is to this post title
$suggestion['label'] = esc_html(get_the_title());
$suggestion['link'] = get_permalink();
$suggestion['classes'] = 'type-' . get_post_type() . ' id-' . get_the_id() . ' slug-' . $post->post_name . ' similarity-' . str_replace('.', '_', number_format($suggestion['similarity'], 2));
if ( get_the_id() == get_option('page_on_front') ){
$suggestion['classes'] .= ' page-home';
} elseif ( is_sticky() ){ //@TODO "Nebula" 0: If sticky post. is_sticky() does not work here?
$suggestion['classes'] .= ' sticky-post';
}
$suggestion['classes'] .= $this->close_or_exact($suggestion['similarity']);
$suggestions[] = $suggestion;
}
}
//Find media library items
if ( !$this->in_array_any(array('attachment'), $ignore_post_types) && $this->in_array_any(array('any', 'attachment'), $types) ){
$attachments = get_posts(array('post_type' => 'attachment', 's' => $term, 'numberposts' => 10, 'post_status' => null));
if ( $attachments ){
$attachment_count = 0;
foreach ( $attachments as $attachment ){
if ( in_array($attachment->ID, $ignore_post_ids) || str_contains(get_attachment_link($attachment->ID), '?attachment_id=') ){ //Skip if media item is not associated with a post
continue;
}
$suggestion = array();
$attachment_meta = wp_get_attachment_metadata($attachment->ID);
if ( isset($attachment_meta['file']) ){
$path_parts = pathinfo($attachment_meta['file']);
$attachment_search_meta = ( get_the_title($attachment->ID) != '' )? get_the_title($attachment->ID) : $path_parts['filename'];
similar_text(strtolower($term), strtolower($attachment_search_meta), $suggestion['similarity']);
if ( $suggestion['similarity'] >= 50 ){
$suggestion['label'] = ( get_the_title($attachment->ID) != '' )? get_the_title($attachment->ID) : $path_parts['basename'];
$suggestion['classes'] = 'type-attachment file-' . $path_parts['extension'];
$suggestion['classes'] .= $this->close_or_exact($suggestion['similarity']);
if ( in_array(strtolower($path_parts['extension']), array('jpg', 'jpeg', 'png', 'gif', 'bmp')) ){
$suggestion['link'] = get_attachment_link($attachment->ID);
} else {
$suggestion['link'] = wp_get_attachment_url($attachment->ID);
$suggestion['external'] = true;
$suggestion['classes'] .= ' external-link';
}
$suggestion['similarity'] = $suggestion['similarity']-0.001; //Force lower priority than posts/pages.
$suggestions[] = $suggestion;
$attachment_count++;
}
if ( $attachment_count >= 2 ){
break;
}
}
}
}
}
//Find menu items
if ( !$this->in_array_any(array('menu'), $ignore_post_types) && $this->in_array_any(array('any', 'menu'), $types) ){
$menus = nebula()->transient('nebula_autocomplete_menus', function(){
return get_terms('nav_menu');
}, WEEK_IN_SECONDS); //This transient is deleted when a post is updated or Nebula Options are saved.
foreach ( $menus as $menu ){
$menu_items = wp_get_nav_menu_items($menu->term_id);
foreach ( $menu_items as $key => $menu_item ){
$suggestion = array();
similar_text(strtolower($term), strtolower($menu_item->title), $menu_title_similarity);
similar_text(strtolower($term), strtolower($menu_item->attr_title), $menu_attr_similarity);
if ( $menu_title_similarity >= 65 || $menu_attr_similarity >= 65 ){
if ( $menu_title_similarity >= $menu_attr_similarity ){
$suggestion['similarity'] = $menu_title_similarity;
$suggestion['label'] = $menu_item->title;
} else {
$suggestion['similarity'] = $menu_attr_similarity;
$suggestion['label'] = $menu_item->attr_title;
}
if ( !empty($menu_item->url) ){
$suggestion['link'] = $menu_item->url;
$path_parts = pathinfo($menu_item->url);
$suggestion['classes'] = 'type-menu-item';
if ( $path_parts['extension'] ){
$suggestion['classes'] .= ' file-' . $path_parts['extension'];
$suggestion['external'] = true;
} elseif ( !str_contains($suggestion['link'], $this->url_components('domain')) ){
$suggestion['classes'] .= ' external-link';
$suggestion['external'] = true;
}
$suggestion['classes'] .= $this->close_or_exact($suggestion['similarity']);
$suggestion['similarity'] = $suggestion['similarity']-0.001; //Force lower priority than posts/pages.
$suggestions[] = $suggestion;
break;
}
}
}
}
}
//Find categories
if ( !$this->in_array_any(array('category', 'cat'), $ignore_post_types) && $this->in_array_any(array('any', 'category', 'cat'), $types) ){
$categories = nebula()->transient('nebula_autocomplete_categories', function(){
return get_categories();
}, WEEK_IN_SECONDS); //This transient is deleted when a post is updated or Nebula Options are saved.
foreach ( $categories as $category ){
$suggestion = array();
$cat_count = 0;
similar_text(strtolower($term), strtolower($category->name), $suggestion['similarity']);
if ( $suggestion['similarity'] >= 65 ){
$suggestion['label'] = $category->name;
$suggestion['link'] = get_category_link($category->term_id);
$suggestion['classes'] = 'type-category';
$suggestion['classes'] .= $this->close_or_exact($suggestion['similarity']);
$suggestions[] = $suggestion;
$cat_count++;
}
if ( $cat_count >= 2 ){
break;
}
}
}
//Find tags
if ( !$this->in_array_any(array('tag'), $ignore_post_types) && $this->in_array_any(array('any', 'tag'), $types) ){
$tags = nebula()->transient('nebula_autocomplete_tags', function(){
return get_tags();
}, WEEK_IN_SECONDS); //This transient is deleted when a post is updated or Nebula Options are saved.
foreach ( $tags as $tag ){
$suggestion = array();
$tag_count = 0;
similar_text(strtolower($term), strtolower($tag->name), $suggestion['similarity']);
if ( $suggestion['similarity'] >= 65 ){
$suggestion['label'] = $tag->name;
$suggestion['link'] = get_tag_link($tag->term_id);
$suggestion['classes'] = 'type-tag';
$suggestion['classes'] .= $this->close_or_exact($suggestion['similarity']);
$suggestions[] = $suggestion;
$tag_count++;
}
if ( $tag_count >= 2 ){
break;
}
}
}
//Find authors (if author bios are enabled)
if ( $this->get_option('author_bios') && !$this->in_array_any(array('author'), $ignore_post_types) && $this->in_array_any(array('any', 'author'), $types) ){
$authors = nebula()->transient('nebula_autocomplete_authors', function(){
return get_users(array('role' => 'author', 'has_published_posts' => true, 'role__not_in' => array('subscriber')));
}, WEEK_IN_SECONDS); //This transient is deleted when a post is updated or Nebula Options are saved.
foreach ( $authors as $author ){
$author_name = ( $author->first_name != '' )? $author->first_name . ' ' . $author->last_name : $author->display_name; //might need adjusting here
if ( strtolower($author_name) === strtolower($term) ){ //todo: if similarity of author name and query term is higher than X. Return only 1 or 2.
$suggestion = array();
$suggestion['label'] = $author_name;
$suggestion['link'] = get_author_posts_url($author->ID);
$suggestion['classes'] = 'type-user';
$suggestion['classes'] .= $this->close_or_exact($suggestion['similarity']);
$suggestion['similarity'] = ''; //todo: save similarity to array too
$suggestions[] = $suggestion;
break;
}
}
}
}
//Now do stuff to the resulting suggestions array
if ( !empty($suggestions) ){
if ( $should_be_sorted ){
//Order by match similarity to page title (DESC).
function autocomplete_similarity_compare($a, $b){
return $b['similarity'] - $a['similarity'];
}
usort($suggestions, "autocomplete_similarity_compare");
}
//Remove any duplicate links (higher similarity = higher priority)
$outputArray = array(); //This array is where unique results will be stored
$keysArray = array(); //This array stores values to check duplicates against.
foreach ( $suggestions as $suggestion ){
if ( !in_array($suggestion['link'], $keysArray) ){
$keysArray[] = $suggestion['link'];
$outputArray[] = $suggestion;
}
}
$outputArray = array_slice($outputArray, 0, 9); //Limit to a maximum amount of results (they are already ordered by similarity)
}
//Add a link to search at the end of the list
//@TODO "Nebula" 0: The empty result is not working for some reason... (Press Enter... is never appearing)
$suggestion = array();
$suggestion['label'] = ( !empty($suggestions) )? __('...more results for', 'nebula') . ' "' . $term . '"' : __('Press enter to search for', 'nebula') . ' "' . $term . '"';
$suggestion['link'] = home_url('/') . '?s=' . str_replace(' ', '%20', $term);
$suggestion['classes'] = ( !empty($suggestions) )? 'more-results search-link' : 'no-results search-link';
$outputArray[] = $suggestion;
$this->timer($timer_name, 'end');
return $outputArray;
}
}
Override
This function can not be short-circuited with an override filter. Request one?