Skip to main content

Design Search Suggestions System

Implementation of a search suggestions system with autocomplete functionality using trie data structure and priority queue for efficient result retrieval.

Designing a search suggestions system requires balancing efficient prefix matching with result ranking. This solution implements a trie-based approach with priority queues to handle multiple search queries efficiently while maintaining optimal performance characteristics for real-time user interactions.

1. Problem Restatement

Clarifying questions to ask the interviewer:

  1. What is the expected input size for products and search word? (e.g., up to 10^4 items)
  2. Are duplicate product names allowed in the products array?
  3. Should the result format be a list of lists where each inner list contains up to 3 suggestions?
  4. Can we mutate the input products array, or must we work with a copy?
  5. What's the target time complexity for each search query? (e.g., O(log n) per query)

Restatement in my own words: We need to build a search suggestions system that, given a list of product names and a search word, returns up to 3 lexicographically smallest product names that share the same prefix as the search word at each character insertion.

Clarification of inputs, outputs, and constraints:

  • Input: String[] products (1 ≤ products.length ≤ 10^4) and String searchWord
  • Output: List<List<String>> where each inner list contains up to 3 product suggestions
  • Products are unique strings with lowercase English letters only
  • Search word is also lowercase English letters only
  • Each search query should return suggestions matching the current prefix

Assumptions I'm making:

  • Products array is already sorted or can be sorted without affecting the problem
  • Product names don't have leading/trailing whitespace
  • Search word is not empty (as per typical search behavior)
  • We want to return suggestions in lexicographical order

Data shape and hidden structure: The problem exhibits a prefix matching pattern where we're looking for products that start with progressively longer prefixes of the search word. This suggests using a data structure that efficiently supports prefix-based searching, like a Trie (prefix tree) or binary search on a sorted array. The structure is hierarchical - each character of the search word builds upon the previous prefix, creating a sequence of increasingly specific queries.

2. Pattern Recognition

Identifying the most likely pattern or patterns

The most likely pattern here is Trie (Prefix Tree) combined with Binary Search or Sorting + Two Pointers.

Why this problem fits that pattern

The problem asks us to build a search suggestions system that, given a search word, returns up to 3 product suggestions that share a common prefix with the search word. This is fundamentally about efficiently finding and retrieving items based on their shared prefix.

The key phrases that signal this pattern:

  • "suggestions system" - suggests we need to store and retrieve related items
  • "prefix matching" - immediately signals prefix-based search patterns
  • "up to 3 suggestions" - indicates we need to maintain ordering and limit results

Core insight needed to solve it

The core insight is that we need to organize our product list in a way that allows us to quickly find all products sharing a given prefix. A Trie data structure naturally supports prefix-based searches, making it an ideal choice for efficiently retrieving matching products in lexicographical order.

Additionally, once we have all products sorted lexicographically, we can use binary search to find the starting point of our prefix matches, then collect up to 3 items from there. This approach leverages the fact that sorting gives us ordered access, and we can efficiently locate the relevant range.

The problem structure suggests that for each character added to the search word, we want to return the top 3 matching products. This naturally maps to a Trie where each node maintains the top 3 suggestions for that prefix, or a sorted list with binary search to find matching prefixes.

The key is recognizing that we're not just doing a single prefix lookup, but rather maintaining state as the search progresses character by character - this suggests we want to build a data structure that supports incremental prefix matching efficiently.

3. Approach Evolution

My first instinct is to use a brute force approach where for each search query, I check all products and filter those that match the current prefix. But that's O(n×m×p) where n is products length, m is average product name length, and p is search word length - that's inefficient because we're doing redundant work.

Let me think about how to optimize this. I notice that if I sort the products first, then for each search query, I can do binary search to find the starting point and then collect matching products. This would reduce to O(n log n + p×m) - still not optimal because we're doing the same prefix matching multiple times.

I notice that if I maintain a trie data structure with products already inserted, then for each search character, I can traverse the trie and collect up to 3 lexicographically smallest matches. This approach would be O(n×m + p×m) where we build the trie once and then search in linear time per character.

The key insight is that this problem asks for suggestions to be returned in lexicographical order, and we want the top 3 matches at each step. This suggests using a trie with a special structure to track the best suggestions at each node.

Let me think about this more carefully - I want to build a trie where each node stores up to 3 lexicographically smallest product names that pass through that node. When I search, I traverse the trie character by character, and at each step I get the suggestions from that node.

The invariant I need to maintain is that at every node in the trie, there are at most 3 product names stored in lexicographical order. When building the trie, I need to ensure that when inserting a new product, I maintain this property.

This approach is optimal because:

  1. Building the trie takes O(n×m) time where n is number of products and m is average product name length
  2. For each search query, I traverse at most p characters where p is the search word length
  3. At each node, I return up to 3 suggestions in O(1) time since they're pre-sorted
  4. Overall complexity is O(n×m + p×m) which is optimal for this problem

4. Data Structure Choice

For this search suggestions system, I need to carefully select data structures that balance fast lookup, efficient insertion, and proper ordering.

The primary data structure I'll use is a Trie (Prefix Tree). This is ideal because:

  • It naturally handles prefix-based searching
  • It allows efficient insertion and lookup operations
  • It maintains words in lexicographical order as we traverse
  • Each node can store up to 3 suggestions for quick access

I'll augment each Trie node with:

  • An array of up to 3 product suggestions (sorted lexicographically)
  • A list of child nodes for each letter (a to z)
  • A flag indicating if this node marks the end of a word

For the product suggestions storage, I'm using an array instead of a dynamic list because:

  • The maximum number of suggestions is fixed at 3
  • Arrays provide O(1) access and are more memory efficient than ArrayLists for small fixed sizes
  • It simplifies the logic for maintaining only top 3 suggestions

The choice of Trie over other structures like Hash Maps or Binary Search Trees is driven by the need to efficiently find all products with a given prefix, which is exactly what Trie excels at.

For the product list input, I'm assuming it's already provided as a String array, so no additional data structure is needed for that.

The key insight is that by storing top 3 suggestions at each Trie node, I can avoid having to sort or collect suggestions during search operations - they're already pre-sorted and maintained at each prefix level.

This approach gives us O(1) access to the top 3 suggestions for any prefix, which is optimal for this use case.

5. Algorithm Walkthrough

Let me trace through the algorithm with a concrete example to show how it works step by step.

Example: products = ["mobile","mouse","moneypot","monitor","mousepad"], searchWord = "mouse"

Step 1: Initialize data structures

  • products array: ["mobile","mouse","moneypot","monitor","mousepad"]
  • searchWord = "mouse"
  • suggestions list to store results for each character

Step 2: Sort products lexicographically

  • After sorting: ["mobile","moneypot","monitor","mouse","mousepad"]

Step 3: Process each character of searchWord

  • Character 'm':

    • Current prefix: "m"
    • Find products starting with "m": ["mobile","moneypot","monitor","mouse","mousepad"]
    • Take first 3: ["mobile","moneypot","monitor"]
    • Store in suggestions[0]
  • Character 'o':

    • Current prefix: "mo"
    • Find products starting with "mo": ["mobile","moneypot","monitor","mouse","mousepad"]
    • Take first 3: ["mobile","moneypot","monitor"]
    • Store in suggestions[1]
  • Character 'u':

    • Current prefix: "mou"
    • Find products starting with "mou": ["mouse","mousepad"]
    • Take first 3: ["mouse","mousepad"]
    • Store in suggestions[2]
  • Character 's':

    • Current prefix: "mous"
    • Find products starting with "mous": ["mouse","mousepad"]
    • Take first 3: ["mouse","mousepad"]
    • Store in suggestions[3]
  • Character 'e':

    • Current prefix: "mouse"
    • Find products starting with "mouse": ["mouse","mousepad"]
    • Take first 3: ["mouse","mousepad"]
    • Store in suggestions[4]

Step 4: Return result

  • Final suggestions: [["mobile","moneypot","monitor"],["mobile","moneypot","monitor"],["mouse","mousepad"],["mouse","mousepad"],["mouse","mousepad"]]

The algorithm correctly handles the prefix matching by leveraging the sorted order and maintains efficient lookup through binary search principles. Each step builds on the previous prefix, ensuring that we only consider products that match the current search prefix.

The key insight is that once we have a sorted list, we can efficiently find all products matching any prefix by scanning from the beginning and stopping when we exceed the prefix in lexicographic order.

6. Java Solution

import java.util.*;

public class SearchSuggestionsSystem {
    
    // Trie node structure to store words and their suggestions
    static class TrieNode {
        Map<Character, TrieNode> children;
        List<String> suggestions;
        
        public TrieNode() {
            children = new HashMap<>();
            suggestions = new ArrayList<>();
        }
    }
    
    private TrieNode root;
    
    public SearchSuggestionsSystem() {
        root = new TrieNode();
    }
    
    /**
     * Builds the trie structure from the products array
     * Time: O(M) where M is total characters in all products
     * Space: O(M) for storing the trie nodes
     */
    public void buildTrie(String[] products) {
        // Sort products lexicographically to ensure proper ordering
        Arrays.sort(products);
        
        for (String product : products) {
            insert(product);
        }
    }
    
    /**
     * Inserts a word into the trie and maintains up to 3 lexicographically smallest suggestions
     * at each node along the path
     */
    private void insert(String word) {
        TrieNode current = root;
        
        for (char c : word.toCharArray()) {
            current.children.putIfAbsent(c, new TrieNode());
            current = current.children.get(c);
            
            // Add the word to suggestions list at this node
            current.suggestions.add(word);
            
            // Keep only up to 3 lexicographically smallest suggestions
            Collections.sort(current.suggestions);
            if (current.suggestions.size() > 3) {
                current.suggestions.remove(current.suggestions.size() - 1);
            }
        }
    }
    
    /**
     * Returns list of lists containing up to 3 product suggestions for each character typed
     * Time: O(P) where P is length of searchWord
     * Space: O(1) excluding the result list
     */
    public List<List<String>> getSuggestions(String searchWord) {
        List<List<String>> result = new ArrayList<>();
        TrieNode current = root;
        
        for (int i = 0; i < searchWord.length(); i++) {
            char c = searchWord.charAt(i);
            
            // If no node exists for this character, no more suggestions possible
            if (!current.children.containsKey(c)) {
                // Fill remaining positions with empty lists
                while (result.size() < searchWord.length()) {
                    result.add(new ArrayList<>());
                }
                break;
            }
            
            current = current.children.get(c);
            result.add(new ArrayList<>(current.suggestions));
        }
        
        return result;
    }
    
    // Main method for testing
    public static void main(String[] args) {
        SearchSuggestionsSystem system = new SearchSuggestionsSystem();
        
        String[] products = {"mobile", "mouse", "moneypot", "monitor", "mousepad"};
        String searchWord = "mouse";
        
        system.buildTrie(products);
        List<List<String>> suggestions = system.getSuggestions(searchWord);
        
        System.out.println("Search suggestions for \"" + searchWord + "\":");
        for (int i = 0; i < suggestions.size(); i++) {
            System.out.println("Typed " + searchWord.substring(0, i + 1) + 
                             ": " + suggestions.get(i));
        }
    }
}

The solution uses a trie data structure where each node maintains up to 3 lexicographically smallest product suggestions. The key insight is that we can build the trie once and then efficiently retrieve suggestions for any search query by traversing the trie character by character. The trie ensures we maintain proper ordering and limit suggestions to 3 at each level, making the search operation very efficient.

7. Complexity Analysis

Time Complexity: O(n × m + k × log s))

Where:

  • n is the number of products
  • m is the average length of product names
  • k is the number of characters in the search word
  • s is the maximum number of matching products for any prefix

The preprocessing phase builds the trie structure with all products, which takes O(n × m) time. For each character in the search word, we traverse the trie and collect up to 3 matching products. In the worst case, this traversal takes O(k × log s) time where we might need to sort and limit results to 3 items.

Space Complexity: O(n × m)

The space complexity is dominated by the trie data structure that stores all product names. Each node in the trie can have up to 26 children (for each letter), and in the worst case, we might store all products' characters in the trie structure.

Why these bounds are correct:

  1. Trie construction: Each product is inserted once, and each character contributes to the trie structure. The total space is proportional to the total number of characters across all products.

  2. Search operation: For each character in search word, we traverse the trie path and collect matching products. The maximum number of matches per prefix is bounded by 3, so the search cost per character is constant.

  3. Sorting: When collecting matches for a prefix, we sort at most 3 items, which is O(1) work.

Optimality:

This solution is near-optimal for the given constraints. The preprocessing cost O(n × m) is unavoidable since we must read all product names to build the search structure. The query cost O(k × log s) is optimal for maintaining a fixed-size result set, as we must process each character and ensure we return the lexicographically smallest 3 matches.

The solution achieves a good balance between preprocessing cost and query efficiency, making it suitable for real-time search systems where products don't change frequently but queries are frequent.

8. Edge Cases & Pitfalls

Empty and Minimal Inputs

  • Empty input array: If products is empty, the result should be an empty list of lists
  • Single product: If there's only one product, all search queries should return that product in the results
  • Empty search query: If searchWord is empty, return an empty list of lists

Duplicate Values and Repeated States

  • Duplicate products: Multiple identical product strings should be handled correctly - the system should return the same top 3 results for each query
  • Duplicate search queries: When a character is repeated in the search word, we should maintain consistent ordering

Boundary Conditions

  • Search word length: When search word has only one character, we should return the top 3 matching products from that point
  • Product count less than 3: When there are fewer than 3 matching products, return all available matches
  • Exact match: When search word exactly matches a product, that product should be included in results

Common Implementation Mistakes

  • Incorrect sorting: Not properly sorting products lexicographically when there are ties (e.g., same prefix, different suffixes)
  • Not maintaining state: Using a fresh search for each character instead of building on previous search results
  • Incorrect result size: Returning more than 3 suggestions when there should only be up to 3
  • Null pointer exceptions: Not checking for null inputs or null product strings
  • Wrong indexing: Off-by-one errors when slicing or accessing arrays

Implementation-specific Pitfalls

  • Modifying input: The problem states that we can modify the input array, but in production systems, it's safer to work with copies
  • Memory allocation: Creating too many intermediate collections during search operations could impact performance
  • Character comparison: Not handling case sensitivity properly if the problem description allows for it

9. Alternative Approaches

There are several alternative approaches to solving the search suggestions problem, each with different trade-offs:

1. Trie-based Approach with DFS: Instead of using a priority queue, we can build a Trie where each node stores up to 3 lexicographically smallest suggestions. When searching, we traverse the Trie and collect suggestions from each node along the path. This approach has O(m) time complexity for searches where m is the length of search word, but requires more complex trie node implementation with suggestion storage.

2. Binary Search on Sorted Array: If the products array is pre-sorted, we can use binary search to find the starting position of words with the given prefix. Then we collect up to 3 products that start with that prefix. This approach works well when the product list is static and we're doing many searches, but requires O(n log n) preprocessing time to sort the products.

3. Sliding Window with Sorting: We can sort all products lexicographically once, then for each search query use two pointers to find the range of products matching the prefix. This is efficient for multiple queries but requires O(n log n) initial sorting.

4. Hash Map with Prefix Keys: Store products in a hash map where keys are prefixes and values are lists of matching products. This allows O(1) lookup for prefix matches but requires O(n × m²) space where n is number of products and m is average product length, which can be inefficient for large inputs.

The chosen solution using priority queues is preferred because it provides clean, intuitive logic with good performance characteristics while maintaining reasonable space usage. The trie approach offers better search time but increases implementation complexity significantly, while the sorting-based approaches require preprocessing that may not be acceptable in dynamic scenarios.

10. Test Cases

// Test case 1: Basic functionality with multiple matches
@Test
public void testBasicFunctionality() {
    String[] products = {"mobile", "mouse", "moneypot", "monitor", "mousepad"};
    String searchWord = "mouse";
    List<List<String>> expected = Arrays.asList(
        Arrays.asList("mobile", "moneypot", "monitor"),
        Arrays.asList("mobile", "moneypot", "monitor"),
        Arrays.asList("mouse", "mousepad"),
        Arrays.asList("mouse", "mousepad"),
        Arrays.asList("mouse", "mousepad")
    );
    assertEquals(expected, solution.suggestedProducts(products, searchWord));
}

// Test case 2: Empty search word
@Test
public void testEmptySearchWord() {
    String[] products = {"mobile", "mouse", "moneypot"};
    String searchWord = "";
    List<List<String>> expected = new ArrayList<>();
    assertEquals(expected, solution.suggestedProducts(products, searchWord));
}

// Test case 3: Single product match
@Test
public void testSingleMatch() {
    String[] products = {"havana"};
    String searchWord = "havana";
    List<List<String>> expected = Arrays.asList(
        Arrays.asList("havana"),
        Arrays.asList("havana"),
        Arrays.asList("havana"),
        Arrays.asList("havana"),
        Arrays.asList("havana"),
        Arrays.asList("havana")
    );
    assertEquals(expected, solution.suggestedProducts(products, searchWord));
}

// Test case 4: Products with common prefixes
@Test
public void testCommonPrefixes() {
    String[] products = {"abc", "ab", "abcd", "abcde"};
    String searchWord = "abc";
    List<List<String>> expected = Arrays.asList(
        Arrays.asList("abc", "abcd", "abcde"),
        Arrays.asList("abc", "abcd", "abcde"),
        Arrays.asList("abc", "abcd", "abcde")
    );
    assertEquals(expected, solution.suggestedProducts(products, searchWord));
}

// Test case 5: No matches found
@Test
public void testNoMatches() {
    String[] products = {"mobile", "mouse", "moneypot"};
    String searchWord = "xyz";
    List<List<String>> expected = new ArrayList<>();
    for (int i = 0; i < 3; i++) {
        expected.add(new ArrayList<>());
    }
    assertEquals(expected, solution.suggestedProducts(products, searchWord));
}

11. Final Summary

The core idea is to efficiently suggest product search terms by maintaining a sorted list of products and using binary search to find relevant suggestions for each character typed. This solution works by preprocessing all products into a sorted collection and then using efficient binary search operations to dynamically narrow down suggestions as the user types, achieving O(n log n) preprocessing time and O(m) query time where m is the length of the search term. The key efficiency comes from leveraging sorted data structures and early termination when no more matches are possible, making it optimal for the given constraints where we expect many search queries against a relatively static product catalog.

Was this article helpful?

Topics in this article

More from DSA Problems

  • Frontend LLD

    Search with Suggestions Low-Level Design

    Comprehensive LLD for search functionality with real-time suggestions, autocomplete, and performance optimization patterns

  • Frontend HLD

    Search and Autocomplete Frontend System Design

    Comprehensive frontend high-level design for scalable search and autocomplete functionality with real-time suggestions and performance optimization

Stay in the Loop

Get curated DSA patterns, coding problems, frontend and backend LLD/HLD, and AI/ML System Design insights delivered straight to your inbox. No spam, just focused interview and engineering content.