Skip to Content
Menu

rest_autocomplete_search()

Run an AJAX autocomplete search of all published posts types (including menu items and authors too).

PHP December 29, 2023

Usage

PHP
nebula()->rest_autocomplete_search()

Parameters

term
(Required) (String) Search keyword or phrase
Default: None

Parameter Notes

term is passed in an AJAX data object.

Request or provide clarification »

Examples

Globally exclude attachments and menus from autocomplete search results.

PHP
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.

PHP
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).

Was this page helpful? Yes No


    A feedback message is required to submit this form.


    Please check that you have entered a valid email address.

    Enter your email address if you would like a response.

    Thank you for your feedback!

    Source File

    Located in /libs/Functions.php on line 2455.

    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 6 to-do comments.

    PHP
            public function rest_autocomplete_search(){
                $timer_name = $this->timer('Autocomplete 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 false;
                    }
    
                    $types = 'any';
                    if ( isset($this->super->get['types']) ){
                        $types =  explode(',', sanitize_text_field(trim($this->super->get['types'])));
                    }
    
                    //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()); //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) || strpos(get_attachment_link($attachment->ID), '?attachment_id=') ){ //Skip if media item is not associated with a post. //@todo "Nebula" 0: Update strpos() to str_contains() in PHP8
                                        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 ( !strpos($suggestion['link'], $this->url_components('domain')) ){ //@todo "Nebula" 0: Update strpos() to str_contains() in PHP8
                                                $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?