Intelligent Search Engine with FastAPI
Build a full-stack search engine with autocomplete, spell check, and recommendations.
Introduction
30 Second Summary
Every time you type into a search bar and see suggestions pop up before you finish typing, there's a set of clever algorithms working behind the scenes. They predict what you want, correct your typos, and recommend related topics.
In this project, you will build a full-stack intelligent search engine web application. The Python backend implements autocomplete, spell checking, and search ranking algorithms. The React frontend provides a modern browser interface to interact with them in real time.
What You'll Build
A full-stack search engine with a FastAPI backend exposing REST API endpoints and a React + TypeScript frontend with live autocomplete, spell correction, and recommendations.
By the end of this project, you'll have:
- A FastAPI backend with 5 API endpoints for search, autocomplete, recent searches, recommendations, and analytics.
- A React + TypeScript frontend with a search bar, autocomplete dropdown, spell correction prompts, and recommendation panels.
- A shared word database that persists search frequencies between sessions using file I/O.
- Secret Mission: Add a visual analytics dashboard page with bar charts showing your most searched terms.
Are there any prerequisites?
No prior experience with Python, React, or APIs is required. This guide includes setup instructions for all the tools you need, including Python, Node.js, and VS Code.
Install Tools and Scaffold Both Projects
Every full-stack project starts with the same question: where does the code live? In this project, you need two separate codebases that talk to each other. A Python backend handles search logic, and a React frontend handles the user interface.
In this step, you will create a clean project folder on your Desktop, set up a Python environment with FastAPI installed, and scaffold a React + TypeScript frontend using Vite.
In this step, get ready to:
- Verify your Python and Node.js installations are working.
- Create the backend project with a virtual environment and FastAPI.
- Scaffold the frontend project with Vite and React TypeScript.
Confirm your Python and Node.js installations
Before building anything, confirm that Python and Node.js are available in your terminal.
- In your VS Code terminal, check your Python installation by running:
python --version
- Check your Node.js installation by running:
node --version
- Check that npm is available by running:
npm --version
You should see Python 3.14.x for the first command, v24.x.x for the second, and a version number for the third.
Not seeing a version number?
If any command returns an error, the tool may not be on your system PATH. Try closing and reopening VS Code so the terminal picks up any recent PATH changes.
Help me troubleshoot why python or node isn't recognized in my terminal
Create the backend project structure
The backend and frontend live in separate folders inside one parent project folder. You will create this structure on your Desktop so it is easy to find.
- Move to your Desktop by running this command:
cd ~/Desktop
- Create the project folder and the backend subfolder by running these commands:
mkdir search-engine-web
cd search-engine-web
mkdir backend
cd backend
- Confirm you are in the right place by running:
pwd
You should see a path ending in search-engine-web/backend.
What does this structure look like?
You now have a search-engine-web folder on your Desktop with a backend subfolder inside. Your terminal is positioned inside backend/, ready for the next commands.
A virtual environment keeps this project's packages separate from your system Python. This prevents conflicts between projects.
- Create a virtual environment by running:
python -m venv venv
- Activate the virtual environment by running the command for your operating system:
macOS
source venv/bin/activate
Windows
.\venv\Scripts\activate
You should see (venv) appear at the start of your terminal prompt. This confirms the virtual environment is active.
Don't see (venv) in your prompt?
On Windows, you may need to allow script execution first. Run this command and then try the activation command again:
Set-ExecutionPolicy -Scope CurrentUser -ExecutionPolicy RemoteSigned
Help me activate my Python virtual environment
- Install FastAPI and its dependencies by running:
pip install "fastapi[standard]"
What is FastAPI?
FastAPI is a Python web framework for building APIs. The [standard] extra installs everything you need to run a development server and auto-generate interactive API documentation.
- Verify FastAPI installed correctly by running:
python -c "import fastapi; print(fastapi.__version__)"
You should see a version number printed (like 0.139.0).
Seeing a ModuleNotFoundError?
Make sure (venv) appears in your prompt before running pip install. If you installed without the venv active, activate it and run the install command again.
Help me fix my FastAPI installation
- In VS Code, click File in the top menu bar.
- Click Open Folder.
- Navigate to your Desktop, select the search-engine-web folder, and click Open.
Seeing a trust prompt?
VS Code may ask if you trust the authors of files in this folder. Click Yes, I trust the authors to continue.
You should see the backend folder with a venv subfolder in the Explorer sidebar on the left.
- Right-click the backend folder in the Explorer sidebar.
- Select New File.
- Type database.txt and press Enter.
- Paste the following word database into database.txt:
algorithm 5
array 3
binary 4
boolean 2
class 7
compiler 3
computer 8
data 6
database 5
debug 2
encryption 1
exception 2
fibonacci 3
function 6
graph 4
hardware 2
inheritance 3
integer 4
java 5
javascript 6
kernel 2
lambda 3
linux 4
loop 5
machine 3
memory 4
network 5
object 6
operator 3
pointer 4
program 5
programming 4
processor 3
python 7
queue 3
recursion 4
runtime 2
software 5
stack 4
string 6
template 3
thread 2
ubuntu 1
variable 5
vector 4
windows 3
What is this file?
This is the word database for your search engine. Each line has a word followed by a number representing how often it has been searched.
The backend will load these words into the Trie data structure and use the frequencies for ranking results.
- Save database.txt by pressing Cmd+S (macOS) or Ctrl+S (Windows).
- Right-click the backend folder again in the Explorer sidebar.
- Select New File.
- Type main.py and press Enter.
Leave main.py empty for now. You will write all the backend code in the next step.
✔️ Awesome, I've got everything!
Great. Make sure you have saved database.txt and that main.py exists in the backend folder.
ⓧ I'd like to double check the full code
Your database.txt file should look exactly like this:
algorithm 5
array 3
binary 4
boolean 2
class 7
compiler 3
computer 8
data 6
database 5
debug 2
encryption 1
exception 2
fibonacci 3
function 6
graph 4
hardware 2
inheritance 3
integer 4
java 5
javascript 6
kernel 2
lambda 3
linux 4
loop 5
machine 3
memory 4
network 5
object 6
operator 3
pointer 4
program 5
programming 4
processor 3
python 7
queue 3
recursion 4
runtime 2
software 5
stack 4
string 6
template 3
thread 2
ubuntu 1
variable 5
vector 4
windows 3
Your main.py should be an empty file in the backend folder.
Scaffold the frontend project
The frontend is a React app written in TypeScript. You will use Vite to scaffold it in seconds.
- Open a new terminal in VS Code by pressing Ctrl+` (the backtick key above Tab).
- Scaffold the React TypeScript project by running this command:
npm create vite@latest frontend -- --template react-ts
What does this command do?
npm create vite@latest downloads the Vite project scaffolder. The frontend argument names the folder to create.
The --template react-ts flag tells Vite to set up a React project with TypeScript, skipping the interactive prompts.
- Move into the new frontend folder and install its dependencies by running:
cd frontend
npm install
You should see a progress bar followed by a summary of installed packages.
npm install failed?
Make sure you are in the frontend folder when running npm install. Run pwd (macOS) or cd (Windows) to check your current directory.
Help me troubleshoot my npm install
- Verify the dev server starts by running:
npm run dev
You should see output showing the local server URL at http://localhost:5173.
- Press Ctrl+C in the terminal to stop the dev server.
Dev server won't start?
If npm run dev shows a port conflict, another process may be using port 5173. Close other dev servers and try again.
Help me troubleshoot my Vite frontend setup
Your project structure is ready. Next up, you will write the entire backend in main.py with all the search algorithms exposed as API endpoints.
Build the FastAPI Backend
Your development environment is ready with Python, Node.js, and both project folders scaffolded. Now it's time to write the actual search engine logic.
In this step, you'll implement five core algorithms in Python and expose them as REST API endpoints using FastAPI. The algorithms include a Trie for autocomplete, Levenshtein distance for spell checking, frequency-based ranking, an LRU cache for recent searches, and a graph for recommendations.
In this step, get ready to:
- Implement all data structure classes (Trie, SpellChecker, Ranking, LRUCache, Recommendation, Database).
- Create five FastAPI endpoints that expose the algorithms as a REST API.
- Test every endpoint using FastAPI's built-in Swagger UI.
Implement the Trie and SpellChecker classes
The Trie is a tree-shaped data structure optimized for prefix lookups. It powers the autocomplete feature by quickly finding all words that start with the letters a user has typed so far.
- In VS Code, open main.py inside the backend folder.
- Replace the empty file with the following code to set up the FastAPI app, CORS middleware, and the first two data structures:
from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware
from pydantic import BaseModel
from collections import OrderedDict
import os
app = FastAPI()
# Allow the React frontend to make requests to this backend
app.add_middleware(
CORSMiddleware,
allow_origins=["http://localhost:5173"],
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
# --- Data Structures ---
class TrieNode:
def __init__(self):
self.children = {}
self.is_end_of_word = False
class Trie:
def __init__(self):
self.root = TrieNode()
def insert(self, word: str):
# Walk down the tree, creating nodes as needed
node = self.root
for char in word.lower():
if char not in node.children:
node.children[char] = TrieNode()
node = node.children[char]
node.is_end_of_word = True
def search(self, word: str) -> bool:
# Walk down the tree, return False if path breaks
node = self.root
for char in word.lower():
if char not in node.children:
return False
node = node.children[char]
return node.is_end_of_word
def _collect_words(self, node: TrieNode, prefix: str, results: list):
# Recursively gather all words below this node
if node.is_end_of_word:
results.append(prefix)
for char, child in node.children.items():
self._collect_words(child, prefix + char, results)
def get_suggestions(self, prefix: str, max_results: int = 5) -> list:
# Navigate to the prefix node, then collect words below it
node = self.root
lower_prefix = prefix.lower()
for char in lower_prefix:
if char not in node.children:
return []
node = node.children[char]
results = []
self._collect_words(node, lower_prefix, results)
return results[:max_results]
class SpellChecker:
def __init__(self):
self.dictionary = []
def load_dictionary(self, words: list):
self.dictionary = [w.lower() for w in words]
def levenshtein_distance(self, s1: str, s2: str) -> int:
# Calculate minimum edits to transform s1 into s2
m, n = len(s1), len(s2)
prev = list(range(n + 1))
curr = [0] * (n + 1)
for i in range(1, m + 1):
curr[0] = i
for j in range(1, n + 1):
if s1[i - 1] == s2[j - 1]:
curr[j] = prev[j - 1]
else:
curr[j] = 1 + min(prev[j], curr[j - 1], prev[j - 1])
prev = curr[:]
return prev[n]
def is_correct(self, word: str) -> bool:
return word.lower() in self.dictionary
def correct(self, word: str) -> str:
# Find the closest word in the dictionary by edit distance
lower = word.lower()
if self.is_correct(lower):
return lower
if not self.dictionary:
return word
best_match = self.dictionary[0]
min_dist = self.levenshtein_distance(lower, best_match)
for w in self.dictionary[1:]:
dist = self.levenshtein_distance(lower, w)
if dist < min_dist:
min_dist = dist
best_match = w
return best_match
What does this code do?
The imports bring in FastAPI itself, CORS middleware (so the frontend can talk to the backend), Pydantic for request validation, OrderedDict for the LRU cache later, and os for file path checking.
The Trie class stores words character-by-character in a tree. Searching for "pro" walks down p, r, o and then collects every word below that point (program, programming, processor).
The SpellChecker uses the Levenshtein distance algorithm to count how many single-character edits (insert, delete, replace) transform one string into another. It picks the dictionary word with the smallest distance as the correction.
You won't see a change yet. The next chunk adds the remaining data structures.
- Below the SpellChecker class, add the Ranking, LRUCache, Recommendation, and Database classes:
class Ranking:
def __init__(self):
self.frequency = {}
def record_search(self, query: str):
# Increment the search count for this word
self.frequency[query] = self.frequency.get(query, 0) + 1
def get_frequency(self, query: str) -> int:
return self.frequency.get(query, 0)
def rank_results(self, results: list) -> list:
# Sort results by how often they've been searched
return sorted(results, key=lambda x: self.frequency.get(x, 0), reverse=True)
def get_all_frequencies(self) -> dict:
return self.frequency
def set_frequency(self, word: str, freq: int):
self.frequency[word] = freq
class LRUCache:
def __init__(self, capacity: int = 10):
self.capacity = capacity
self.cache = OrderedDict()
def add(self, query: str):
# Move to end if exists, otherwise add and evict oldest if full
if query in self.cache:
self.cache.move_to_end(query)
else:
self.cache[query] = True
if len(self.cache) > self.capacity:
self.cache.popitem(last=False)
def get_recent(self) -> list:
# Return items newest-first
return list(reversed(self.cache.keys()))
class Recommendation:
def __init__(self):
self.graph = {}
def add_relation(self, word1: str, word2: str):
# Connect two words bidirectionally in the graph
if word1 == word2:
return
if word1 not in self.graph:
self.graph[word1] = set()
if word2 not in self.graph:
self.graph[word2] = set()
self.graph[word1].add(word2)
self.graph[word2].add(word1)
def get_recommendations(self, query: str) -> list:
if query in self.graph:
return list(self.graph[query])
return []
class Database:
def __init__(self, filename: str = "database.txt"):
self.filename = filename
def load_words(self) -> list:
# Read just the word column from each line
words = []
if not os.path.exists(self.filename):
return words
with open(self.filename, "r") as f:
for line in f:
parts = line.strip().split()
if parts:
words.append(parts[0])
return words
def load_frequencies(self) -> dict:
# Read word and frequency from each line
frequencies = {}
if not os.path.exists(self.filename):
return frequencies
with open(self.filename, "r") as f:
for line in f:
parts = line.strip().split()
if len(parts) >= 2:
frequencies[parts[0]] = int(parts[1])
return frequencies
def save(self, data: dict):
# Write all word frequencies back to disk
with open(self.filename, "w") as f:
for word, freq in data.items():
f.write(f"{word} {freq}\n")
What do these classes do?
Ranking tracks how often each word has been searched. It sorts autocomplete suggestions so frequently searched terms appear first.
LRUCache (Least Recently Used) keeps the 10 most recent searches using Python's OrderedDict for O(1) add and evict operations. When the cache is full, the oldest entry gets removed automatically.
Recommendation builds a graph of related words. When you search "python" then "java", those words become connected. Next time you search "python", the engine suggests "java".
Database handles reading from and writing to database.txt so word frequencies persist between server restarts.
- Below the Database class, add the initialization code that creates instances of each class and loads the word database:
# --- Initialize all modules ---
db = Database("database.txt")
trie = Trie()
spell_checker = SpellChecker()
ranking = Ranking()
cache = LRUCache(10)
recommendation = Recommendation()
search_history = []
# Load words from the database file into the trie and spell checker
words = db.load_words()
frequencies = db.load_frequencies()
for word in words:
trie.insert(word)
spell_checker.load_dictionary(words)
# Restore saved search frequencies from previous sessions
for word, freq in frequencies.items():
ranking.set_frequency(word, freq)
What does this initialization do?
This code runs once when the server starts. It creates one instance of each data structure, then populates the Trie and SpellChecker with all 46 words from database.txt. It also restores the frequency counts so rankings carry over between sessions.
- Save main.py.
- Verify the module loads correctly by running this command in your terminal:
python -c "from main import trie; print(trie.get_suggestions('pro'))"
You should see a list of words starting with "pro", like ['program', 'programming', 'processor'].
Seeing an ImportError or ModuleNotFoundError?
Make sure your virtual environment is activated (you should see (venv) at the start of your terminal prompt). Also confirm your terminal is in the backend folder, not the project root.
help me fix my Python import error when testing the Trie class
Create the API endpoints
With the data structures working, you need to expose them over HTTP so the React frontend can call them. Each endpoint handles one piece of search functionality.
- Below the initialization code in main.py, add the Pydantic request and response models:
# --- Request/Response Models ---
class SearchRequest(BaseModel):
query: str
class SearchResponse(BaseModel):
found: bool
query: str
corrected: bool
correction: str | None = None
recommendations: list[str] = []
class AutocompleteResponse(BaseModel):
suggestions: list[dict]
class RecentResponse(BaseModel):
recent: list[str]
class RecommendationResponse(BaseModel):
recommendations: list[str]
class AnalyticsResponse(BaseModel):
top_words: list[dict]
What are Pydantic models?
Pydantic models define the exact shape of data going in and out of each endpoint. FastAPI uses them to automatically validate incoming requests and generate documentation. The str | None syntax means the field can be a string or null.
- Below the response models, add all five API endpoint functions:
# --- API Endpoints ---
@app.post("/search", response_model=SearchResponse)
def search(request: SearchRequest):
query = request.query.lower().strip()
corrected = False
correction = None
# Check spelling and suggest a correction if needed
if not spell_checker.is_correct(query):
correction = spell_checker.correct(query)
corrected = True
search_word = correction if corrected else query
found = trie.search(search_word)
# Update all tracking systems
ranking.record_search(search_word)
cache.add(search_word)
search_history.append(search_word)
# Build recommendation graph from consecutive searches
if len(search_history) >= 2:
recommendation.add_relation(search_history[-2], search_history[-1])
# Persist frequencies to disk
db.save(ranking.get_all_frequencies())
recs = recommendation.get_recommendations(search_word)
return SearchResponse(found=found, query=search_word, corrected=corrected, correction=correction, recommendations=recs)
@app.get("/autocomplete", response_model=AutocompleteResponse)
def autocomplete(prefix: str = ""):
# Get trie suggestions and sort by search frequency
suggestions = trie.get_suggestions(prefix)
ranked = ranking.rank_results(suggestions)
result = [{"word": w, "frequency": ranking.get_frequency(w)} for w in ranked]
return AutocompleteResponse(suggestions=result)
@app.get("/recent", response_model=RecentResponse)
def recent():
return RecentResponse(recent=cache.get_recent())
@app.get("/recommendations", response_model=RecommendationResponse)
def recommendations(word: str = ""):
recs = recommendation.get_recommendations(word.lower())
return RecommendationResponse(recommendations=recs)
@app.get("/analytics", response_model=AnalyticsResponse)
def analytics():
# Return top 5 most searched words
all_freqs = ranking.get_all_frequencies()
sorted_words = sorted(all_freqs.items(), key=lambda x: x[1], reverse=True)[:5]
result = [{"word": w, "frequency": f} for w, f in sorted_words]
return AnalyticsResponse(top_words=result)
What do these endpoints do?
- POST /search performs a full search: spell-checks the query, records the search for ranking, adds it to the LRU cache, builds recommendation links, and saves frequencies to disk.
- GET /autocomplete returns up to 5 word suggestions for a given prefix, sorted by how often each word has been searched.
- GET /recent returns the 10 most recent searches from the LRU cache.
- GET /recommendations returns words connected to the given word in the recommendation graph.
- GET /analytics returns the top 5 most searched words with their frequency counts.
- Save main.py.
✔️ Awesome, I've got everything!
Great. Double check you've saved main.py and move on to testing.
ⓧ I'd like to double check the full code
from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware
from pydantic import BaseModel
from collections import OrderedDict
import os
app = FastAPI()
app.add_middleware(
CORSMiddleware,
allow_origins=["http://localhost:5173"],
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
class TrieNode:
def __init__(self):
self.children = {}
self.is_end_of_word = False
class Trie:
def __init__(self):
self.root = TrieNode()
def insert(self, word: str):
node = self.root
for char in word.lower():
if char not in node.children:
node.children[char] = TrieNode()
node = node.children[char]
node.is_end_of_word = True
def search(self, word: str) -> bool:
node = self.root
for char in word.lower():
if char not in node.children:
return False
node = node.children[char]
return node.is_end_of_word
def _collect_words(self, node: TrieNode, prefix: str, results: list):
if node.is_end_of_word:
results.append(prefix)
for char, child in node.children.items():
self._collect_words(child, prefix + char, results)
def get_suggestions(self, prefix: str, max_results: int = 5) -> list:
node = self.root
lower_prefix = prefix.lower()
for char in lower_prefix:
if char not in node.children:
return []
node = node.children[char]
results = []
self._collect_words(node, lower_prefix, results)
return results[:max_results]
class SpellChecker:
def __init__(self):
self.dictionary = []
def load_dictionary(self, words: list):
self.dictionary = [w.lower() for w in words]
def levenshtein_distance(self, s1: str, s2: str) -> int:
m, n = len(s1), len(s2)
prev = list(range(n + 1))
curr = [0] * (n + 1)
for i in range(1, m + 1):
curr[0] = i
for j in range(1, n + 1):
if s1[i - 1] == s2[j - 1]:
curr[j] = prev[j - 1]
else:
curr[j] = 1 + min(prev[j], curr[j - 1], prev[j - 1])
prev = curr[:]
return prev[n]
def is_correct(self, word: str) -> bool:
return word.lower() in self.dictionary
def correct(self, word: str) -> str:
lower = word.lower()
if self.is_correct(lower):
return lower
if not self.dictionary:
return word
best_match = self.dictionary[0]
min_dist = self.levenshtein_distance(lower, best_match)
for w in self.dictionary[1:]:
dist = self.levenshtein_distance(lower, w)
if dist < min_dist:
min_dist = dist
best_match = w
return best_match
class Ranking:
def __init__(self):
self.frequency = {}
def record_search(self, query: str):
self.frequency[query] = self.frequency.get(query, 0) + 1
def get_frequency(self, query: str) -> int:
return self.frequency.get(query, 0)
def rank_results(self, results: list) -> list:
return sorted(results, key=lambda x: self.frequency.get(x, 0), reverse=True)
def get_all_frequencies(self) -> dict:
return self.frequency
def set_frequency(self, word: str, freq: int):
self.frequency[word] = freq
class LRUCache:
def __init__(self, capacity: int = 10):
self.capacity = capacity
self.cache = OrderedDict()
def add(self, query: str):
if query in self.cache:
self.cache.move_to_end(query)
else:
self.cache[query] = True
if len(self.cache) > self.capacity:
self.cache.popitem(last=False)
def get_recent(self) -> list:
return list(reversed(self.cache.keys()))
class Recommendation:
def __init__(self):
self.graph = {}
def add_relation(self, word1: str, word2: str):
if word1 == word2:
return
if word1 not in self.graph:
self.graph[word1] = set()
if word2 not in self.graph:
self.graph[word2] = set()
self.graph[word1].add(word2)
self.graph[word2].add(word1)
def get_recommendations(self, query: str) -> list:
if query in self.graph:
return list(self.graph[query])
return []
class Database:
def __init__(self, filename: str = "database.txt"):
self.filename = filename
def load_words(self) -> list:
words = []
if not os.path.exists(self.filename):
return words
with open(self.filename, "r") as f:
for line in f:
parts = line.strip().split()
if parts:
words.append(parts[0])
return words
def load_frequencies(self) -> dict:
frequencies = {}
if not os.path.exists(self.filename):
return frequencies
with open(self.filename, "r") as f:
for line in f:
parts = line.strip().split()
if len(parts) >= 2:
frequencies[parts[0]] = int(parts[1])
return frequencies
def save(self, data: dict):
with open(self.filename, "w") as f:
for word, freq in data.items():
f.write(f"{word} {freq}\n")
db = Database("database.txt")
trie = Trie()
spell_checker = SpellChecker()
ranking = Ranking()
cache = LRUCache(10)
recommendation = Recommendation()
search_history = []
words = db.load_words()
frequencies = db.load_frequencies()
for word in words:
trie.insert(word)
spell_checker.load_dictionary(words)
for word, freq in frequencies.items():
ranking.set_frequency(word, freq)
class SearchRequest(BaseModel):
query: str
class SearchResponse(BaseModel):
found: bool
query: str
corrected: bool
correction: str | None = None
recommendations: list[str] = []
class AutocompleteResponse(BaseModel):
suggestions: list[dict]
class RecentResponse(BaseModel):
recent: list[str]
class RecommendationResponse(BaseModel):
recommendations: list[str]
class AnalyticsResponse(BaseModel):
top_words: list[dict]
@app.post("/search", response_model=SearchResponse)
def search(request: SearchRequest):
query = request.query.lower().strip()
corrected = False
correction = None
if not spell_checker.is_correct(query):
correction = spell_checker.correct(query)
corrected = True
search_word = correction if corrected else query
found = trie.search(search_word)
ranking.record_search(search_word)
cache.add(search_word)
search_history.append(search_word)
if len(search_history) >= 2:
recommendation.add_relation(search_history[-2], search_history[-1])
db.save(ranking.get_all_frequencies())
recs = recommendation.get_recommendations(search_word)
return SearchResponse(found=found, query=search_word, corrected=corrected, correction=correction, recommendations=recs)
@app.get("/autocomplete", response_model=AutocompleteResponse)
def autocomplete(prefix: str = ""):
suggestions = trie.get_suggestions(prefix)
ranked = ranking.rank_results(suggestions)
result = [{"word": w, "frequency": ranking.get_frequency(w)} for w in ranked]
return AutocompleteResponse(suggestions=result)
@app.get("/recent", response_model=RecentResponse)
def recent():
return RecentResponse(recent=cache.get_recent())
@app.get("/recommendations", response_model=RecommendationResponse)
def recommendations(word: str = ""):
recs = recommendation.get_recommendations(word.lower())
return RecommendationResponse(recommendations=recs)
@app.get("/analytics", response_model=AnalyticsResponse)
def analytics():
all_freqs = ranking.get_all_frequencies()
sorted_words = sorted(all_freqs.items(), key=lambda x: x[1], reverse=True)[:5]
result = [{"word": w, "frequency": f} for w, f in sorted_words]
return AnalyticsResponse(top_words=result)
Test the API using Swagger UI
FastAPI automatically generates an interactive documentation page called Swagger UI. You can use it to test every endpoint without writing any frontend code.
- Start the development server by running:
fastapi dev main.py
You should see output that includes Application startup complete and the server URL http://127.0.0.1:8000.
Seeing an error on startup?
If you see a SyntaxError or IndentationError, double check your main.py against the full code in the tab above. Python is sensitive to indentation and missing colons.
If you see a ModuleNotFoundError for fastapi, confirm your virtual environment is active by checking for (venv) in your terminal prompt.
help me debug my FastAPI server startup error
- Open your browser and navigate to http://127.0.0.1:8000/docs.
- You should see the Swagger UI page listing all five endpoints.
- Click on GET /autocomplete to expand it.
- Click Try it out.
- Enter pro in the prefix field.
- Click Execute.
You should see a JSON response containing suggestions like "program", "programming", and "processor" with their frequencies.
- Now test the spell checker by clicking POST /search.
- Click Try it out.
- Replace the request body with:
{"query": "pyhton"}
- Click Execute.
The response should show "corrected": true and "correction": "python". The Levenshtein distance algorithm detected the typo and found the closest match.
Why test with Swagger UI?
FastAPI generates Swagger UI automatically from your Pydantic models and endpoint decorators. It lets you interact with every endpoint visually, see the exact request format, and inspect responses. You get a fully testable API without writing a single line of frontend code.
Your backend is fully operational. Next up, you'll build the React frontend that connects to these endpoints and gives users a real search interface.
Build the React Frontend
Your backend API is live and serving search results, autocomplete suggestions, and recommendations. Now you need a way for users to actually interact with it through their browser.
In this step, you will build the React + TypeScript frontend that communicates with your FastAPI backend. The frontend will feature a search bar with live autocomplete, spell correction prompts, and sidebar panels for recent searches and recommendations.
In this step, get ready to:
- Define TypeScript interfaces and set up the app's CSS styling.
- Build a search bar with debounced live autocomplete and spell correction.
- Build the Recent Searches and Recommendations sidebar panels.
Set up the project structure and TypeScript interfaces
Before writing any logic, you need to define the shape of the data your frontend will send and receive from the API. TypeScript interfaces enforce these shapes at development time, catching mismatches before you ever run the app.
- In VS Code, expand the frontend/src folder in the Explorer sidebar.
- Open App.tsx.
- Select all the existing content and delete it.
- Replace it with the following imports, API base URL, and TypeScript interfaces:
import { useState, useEffect, useCallback } from "react";
import "./App.css";
// Base URL for all API calls to the FastAPI backend
const API_BASE = "http://127.0.0.1:8000";
// Shape of each autocomplete suggestion from the API
interface Suggestion {
word: string;
frequency: number;
}
// Shape of the response from the /search endpoint
interface SearchResult {
found: boolean;
query: string;
corrected: boolean;
correction: string | null;
recommendations: string[];
}
// Shape of each item in the analytics data
interface AnalyticsItem {
word: string;
frequency: number;
}
What does this code do?
API_BASE points to your running FastAPI server. Every fetch call in the app will use this as its root URL.
The three interfaces (Suggestion, SearchResult, AnalyticsItem) mirror the JSON shapes your backend returns. TypeScript will warn you if you try to access a field that doesn't exist on these objects.
- Save App.tsx.
Now let's set up the full stylesheet for the app. The CSS defines the layout grid, search bar styling, autocomplete dropdown, sidebar panels, and the analytics chart.
- Open App.css in the same frontend/src folder.
- Select all the existing content and delete it.
- Replace it with the following styles:
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
background-color: #f5f5f5;
color: #333;
}
.app {
max-width: 900px;
margin: 0 auto;
padding: 2rem;
}
.header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 2rem;
}
.header h1 {
font-size: 1.5rem;
color: #1a1a2e;
}
.tab-button {
padding: 0.5rem 1rem;
background: #1a1a2e;
color: white;
border: none;
border-radius: 6px;
cursor: pointer;
font-size: 0.9rem;
}
.tab-button:hover {
background: #16213e;
}
.main-content {
display: grid;
grid-template-columns: 1fr 280px;
gap: 2rem;
}
.search-section {
position: relative;
}
.search-bar {
display: flex;
gap: 0.5rem;
}
.search-input {
flex: 1;
padding: 0.75rem 1rem;
border: 2px solid #ddd;
border-radius: 8px;
font-size: 1rem;
outline: none;
transition: border-color 0.2s;
}
.search-input:focus {
border-color: #1a1a2e;
}
.search-button {
padding: 0.75rem 1.5rem;
background: #1a1a2e;
color: white;
border: none;
border-radius: 8px;
cursor: pointer;
font-size: 1rem;
}
.search-button:hover {
background: #16213e;
}
.suggestions-dropdown {
position: absolute;
top: 100%;
left: 0;
right: 70px;
background: white;
border: 1px solid #ddd;
border-radius: 8px;
list-style: none;
margin-top: 4px;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
z-index: 10;
}
.suggestion-item {
padding: 0.6rem 1rem;
cursor: pointer;
transition: background 0.15s;
}
.suggestion-item:hover {
background: #f0f0f0;
}
.suggestion-freq {
color: #888;
font-size: 0.85rem;
}
.search-result {
margin-top: 1.5rem;
padding: 1rem;
background: white;
border-radius: 8px;
border: 1px solid #eee;
}
.correction {
color: #e63946;
margin-bottom: 0.5rem;
}
.inline-recommendations {
margin-top: 0.75rem;
display: flex;
align-items: center;
gap: 0.5rem;
flex-wrap: wrap;
}
.rec-chip {
padding: 0.25rem 0.6rem;
background: #e8f4fd;
border: 1px solid #b3d9f2;
border-radius: 16px;
cursor: pointer;
font-size: 0.85rem;
color: #1a1a2e;
}
.rec-chip:hover {
background: #d0ebf7;
}
.sidebar {
display: flex;
flex-direction: column;
gap: 1.5rem;
}
.panel {
background: white;
border-radius: 8px;
padding: 1rem;
border: 1px solid #eee;
}
.panel h3 {
font-size: 0.95rem;
margin-bottom: 0.75rem;
color: #1a1a2e;
}
.panel-list {
list-style: none;
}
.panel-item {
padding: 0.4rem 0;
cursor: pointer;
color: #555;
transition: color 0.15s;
}
.panel-item:hover {
color: #1a1a2e;
}
.empty-message {
color: #999;
font-size: 0.85rem;
}
.analytics-container {
background: white;
border-radius: 8px;
padding: 2rem;
border: 1px solid #eee;
}
.analytics-container h2 {
margin-bottom: 1.5rem;
color: #1a1a2e;
}
.chart {
display: flex;
flex-direction: column;
gap: 0.75rem;
}
.chart-row {
display: flex;
align-items: center;
gap: 1rem;
}
.chart-label {
width: 120px;
text-align: right;
font-size: 0.9rem;
color: #555;
}
.chart-bar-container {
flex: 1;
height: 28px;
background: #f0f0f0;
border-radius: 4px;
overflow: hidden;
}
.chart-bar {
height: 100%;
background: linear-gradient(90deg, #1a1a2e, #16213e);
border-radius: 4px;
transition: width 0.3s ease;
}
.chart-value {
width: 40px;
font-size: 0.85rem;
color: #888;
}
What does this CSS do?
- .main-content uses CSS Grid to create a two-column layout: the search area on the left and a 280px sidebar on the right.
- .suggestions-dropdown is positioned absolutely below the search bar so autocomplete results float over other content.
- .chart-bar uses a gradient background and a CSS transition on width so the analytics bars animate smoothly when data loads.
- Save App.css.
You won't see a visual change yet because the component has no JSX to render. The next chunk adds the logic layer.
Seeing red underlines in App.tsx?
TypeScript might show errors because the file is incomplete (no component exported yet). This is expected and will resolve once you add the full component in the next chunks.
Help me understand the TypeScript errors in my App.tsx file.
Build the search bar with live autocomplete
The search bar needs to do three things: fetch autocomplete suggestions as the user types (debounced to avoid flooding the API), submit full searches to the backend, and display spell corrections when the backend detects a typo.
- In App.tsx, below the AnalyticsItem interface, add the App component function with all state variables, fetch functions, and event handlers:
function App() {
// State for the search input value
const [query, setQuery] = useState("");
// Autocomplete suggestions from the API
const [suggestions, setSuggestions] = useState<Suggestion[]>([]);
// List of recent searches from the LRU cache
const [recentSearches, setRecentSearches] = useState<string[]>([]);
// Related word recommendations from the graph
const [recommendations, setRecommendations] = useState<string[]>([]);
// The most recent search result (includes spell correction info)
const [lastResult, setLastResult] = useState<SearchResult | null>(null);
// Toggle between search view and analytics view
const [showAnalytics, setShowAnalytics] = useState(false);
// Analytics data for the bar chart
const [analyticsData, setAnalyticsData] = useState<AnalyticsItem[]>([]);
// Controls visibility of the autocomplete dropdown
const [showSuggestions, setShowSuggestions] = useState(false);
// Fetch the list of recent searches from the LRU cache endpoint
const fetchRecent = async () => {
const res = await fetch(`${API_BASE}/recent`);
const data = await res.json();
setRecentSearches(data.recent);
};
// Fetch related word recommendations for a given word
const fetchRecommendations = async (word: string) => {
const res = await fetch(
`${API_BASE}/recommendations?word=${encodeURIComponent(word)}`
);
const data = await res.json();
setRecommendations(data.recommendations);
};
// Fetch top searched words for the analytics chart
const fetchAnalytics = async () => {
const res = await fetch(`${API_BASE}/analytics`);
const data = await res.json();
setAnalyticsData(data.top_words);
};
// Fetch autocomplete suggestions for a given prefix
const fetchAutocomplete = useCallback(async (prefix: string) => {
if (prefix.length === 0) {
setSuggestions([]);
return;
}
const res = await fetch(
`${API_BASE}/autocomplete?prefix=${encodeURIComponent(prefix)}`
);
const data = await res.json();
setSuggestions(data.suggestions);
setShowSuggestions(true);
}, []);
// Debounce autocomplete: wait 300ms after the user stops typing
useEffect(() => {
const timer = setTimeout(() => {
fetchAutocomplete(query);
}, 300);
return () => clearTimeout(timer);
}, [query, fetchAutocomplete]);
// Submit a search to the backend and update all panels
const handleSearch = async (searchWord?: string) => {
const word = searchWord || query;
if (!word.trim()) return;
const res = await fetch(`${API_BASE}/search`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ query: word }),
});
const data: SearchResult = await res.json();
setLastResult(data);
setQuery(data.query);
setShowSuggestions(false);
setSuggestions([]);
fetchRecent();
fetchRecommendations(data.query);
};
// Allow pressing Enter to submit a search
const handleKeyDown = (e: React.KeyboardEvent) => {
if (e.key === "Enter") {
handleSearch();
}
};
// When user clicks a suggestion, search for that word immediately
const handleSuggestionClick = (word: string) => {
setQuery(word);
setShowSuggestions(false);
handleSearch(word);
};
// Load recent searches when the app first mounts
useEffect(() => {
fetchRecent();
}, []);
What does this code do?
- The useEffect with the 300ms setTimeout creates a debounce. Each keystroke resets the timer, so the API call only fires after the user pauses typing for 300 milliseconds.
- fetchAutocomplete is wrapped in useCallback so React doesn't re-create it on every render. This keeps the debounce useEffect stable.
- handleSearch sends a POST request to /search, then refreshes both the recent searches and recommendations panels with the latest data.
What is debouncing?
Without debouncing, typing "python" would fire 6 separate API requests (one per character). Debouncing waits until the user pauses, then sends a single request for the final prefix. This reduces server load and prevents flickering results.
- Save App.tsx.
You won't see a change yet because the component still has no return statement. The next chunk adds the visual UI.
Seeing errors about missing return statements?
TypeScript requires every component function to return JSX. The file is intentionally incomplete right now. The errors will disappear once you add the return block in the next section.
Help me fix TypeScript errors about missing returns in my React component.
Build the Recent Searches and Recommendations panels
Now you will add the JSX that renders everything the user sees. This includes the search bar, the autocomplete dropdown, spell correction messages, and the two sidebar panels that display recent searches and recommendations.
- In App.tsx, directly below the closing }, []); of the fetchRecent useEffect, add the complete render logic and close the component:
// Analytics view: show bar chart of top searched words
if (showAnalytics) {
const maxFreq =
analyticsData.length > 0
? Math.max(...analyticsData.map((item) => item.frequency))
: 1;
return (
<div className="app">
<header className="header">
<h1>Intelligent Search Engine</h1>
<button
className="tab-button"
onClick={() => setShowAnalytics(false)}
>
Back to Search
</button>
</header>
<main className="analytics-container">
<h2>Search Analytics</h2>
{analyticsData.length === 0 ? (
<p>No search data yet. Start searching to build analytics!</p>
) : (
<div className="chart">
{analyticsData.map((item) => (
<div key={item.word} className="chart-row">
<span className="chart-label">{item.word}</span>
<div className="chart-bar-container">
<div
className="chart-bar"
style={{
width: `${(item.frequency / maxFreq) * 100}%`,
}}
></div>
</div>
<span className="chart-value">({item.frequency})</span>
</div>
))}
</div>
)}
</main>
</div>
);
}
// Main search view
return (
<div className="app">
<header className="header">
<h1>Intelligent Search Engine</h1>
<button
className="tab-button"
onClick={() => {
fetchAnalytics();
setShowAnalytics(true);
}}
>
Analytics
</button>
</header>
<main className="main-content">
<div className="search-section">
<div className="search-bar">
<input
type="text"
value={query}
onChange={(e) => setQuery(e.target.value)}
onKeyDown={handleKeyDown}
placeholder="Type to search..."
className="search-input"
/>
<button onClick={() => handleSearch()} className="search-button">
Search
</button>
</div>
{showSuggestions && suggestions.length > 0 && (
<ul className="suggestions-dropdown">
{suggestions.map((s) => (
<li
key={s.word}
onClick={() => handleSuggestionClick(s.word)}
className="suggestion-item"
>
{s.word}{" "}
<span className="suggestion-freq">({s.frequency})</span>
</li>
))}
</ul>
)}
{lastResult && (
<div className="search-result">
{lastResult.corrected && (
<p className="correction">
Corrected to: <strong>{lastResult.correction}</strong>
</p>
)}
<p>
{lastResult.found
? `Found: "${lastResult.query}"`
: `"${lastResult.query}" not found in database.`}
</p>
{lastResult.recommendations.length > 0 && (
<div className="inline-recommendations">
<span>Related: </span>
{lastResult.recommendations.map((r) => (
<button
key={r}
className="rec-chip"
onClick={() => handleSearch(r)}
>
{r}
</button>
))}
</div>
)}
</div>
)}
</div>
<aside className="sidebar">
<div className="panel">
<h3>Recent Searches</h3>
{recentSearches.length === 0 ? (
<p className="empty-message">No recent searches.</p>
) : (
<ul className="panel-list">
{recentSearches.map((item, index) => (
<li
key={index}
onClick={() => handleSearch(item)}
className="panel-item"
>
{item}
</li>
))}
</ul>
)}
</div>
<div className="panel">
<h3>Recommendations</h3>
{recommendations.length === 0 ? (
<p className="empty-message">
Search more to build recommendations!
</p>
) : (
<ul className="panel-list">
{recommendations.map((item) => (
<li
key={item}
onClick={() => handleSearch(item)}
className="panel-item"
>
{item}
</li>
))}
</ul>
)}
</div>
</aside>
</main>
</div>
);
}
export default App;
What does this code do?
- The if (showAnalytics) block conditionally renders a bar chart view. Each bar's width is calculated as a percentage of the most-searched word's frequency, so the top word always spans 100% width.
- The autocomplete dropdown only appears when showSuggestions is true AND suggestions exist. Clicking a suggestion triggers handleSuggestionClick which performs a full search for that word.
- The spell correction prompt appears when lastResult.corrected is true, showing the user what their typo was corrected to in red text.
- The Recent Searches panel lists the last 10 searches from the LRU cache. Clicking any item re-searches that word.
- The Recommendations panel shows words related to your last search, pulled from the backend's graph data structure. Both panels refresh after every search.
- Save App.tsx.
TypeScript errors should now be gone. The component is complete with a valid export.
Still seeing TypeScript errors?
- Check that the opening function App() { and the final closing } brace are balanced. Every open brace needs a matching close.
- Verify that export default App; appears as the very last line of the file, outside the function.
- Make sure there are no duplicate import lines at the top of the file.
Help me debug TypeScript errors in my React search engine frontend.
✔️ Awesome, I've got everything!
Great. Double check you have saved both App.tsx and App.css.
ⓧ I'd like to double check the full code
Here is the complete App.tsx file:
import { useState, useEffect, useCallback } from "react";
import "./App.css";
const API_BASE = "http://127.0.0.1:8000";
interface Suggestion {
word: string;
frequency: number;
}
interface SearchResult {
found: boolean;
query: string;
corrected: boolean;
correction: string | null;
recommendations: string[];
}
interface AnalyticsItem {
word: string;
frequency: number;
}
function App() {
const [query, setQuery] = useState("");
const [suggestions, setSuggestions] = useState<Suggestion[]>([]);
const [recentSearches, setRecentSearches] = useState<string[]>([]);
const [recommendations, setRecommendations] = useState<string[]>([]);
const [lastResult, setLastResult] = useState<SearchResult | null>(null);
const [showAnalytics, setShowAnalytics] = useState(false);
const [analyticsData, setAnalyticsData] = useState<AnalyticsItem[]>([]);
const [showSuggestions, setShowSuggestions] = useState(false);
const fetchRecent = async () => {
const res = await fetch(`${API_BASE}/recent`);
const data = await res.json();
setRecentSearches(data.recent);
};
const fetchRecommendations = async (word: string) => {
const res = await fetch(
`${API_BASE}/recommendations?word=${encodeURIComponent(word)}`
);
const data = await res.json();
setRecommendations(data.recommendations);
};
const fetchAnalytics = async () => {
const res = await fetch(`${API_BASE}/analytics`);
const data = await res.json();
setAnalyticsData(data.top_words);
};
const fetchAutocomplete = useCallback(async (prefix: string) => {
if (prefix.length === 0) {
setSuggestions([]);
return;
}
const res = await fetch(
`${API_BASE}/autocomplete?prefix=${encodeURIComponent(prefix)}`
);
const data = await res.json();
setSuggestions(data.suggestions);
setShowSuggestions(true);
}, []);
useEffect(() => {
const timer = setTimeout(() => {
fetchAutocomplete(query);
}, 300);
return () => clearTimeout(timer);
}, [query, fetchAutocomplete]);
const handleSearch = async (searchWord?: string) => {
const word = searchWord || query;
if (!word.trim()) return;
const res = await fetch(`${API_BASE}/search`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ query: word }),
});
const data: SearchResult = await res.json();
setLastResult(data);
setQuery(data.query);
setShowSuggestions(false);
setSuggestions([]);
fetchRecent();
fetchRecommendations(data.query);
};
const handleKeyDown = (e: React.KeyboardEvent) => {
if (e.key === "Enter") {
handleSearch();
}
};
const handleSuggestionClick = (word: string) => {
setQuery(word);
setShowSuggestions(false);
handleSearch(word);
};
useEffect(() => {
fetchRecent();
}, []);
if (showAnalytics) {
const maxFreq =
analyticsData.length > 0
? Math.max(...analyticsData.map((item) => item.frequency))
: 1;
return (
<div className="app">
<header className="header">
<h1>Intelligent Search Engine</h1>
<button
className="tab-button"
onClick={() => setShowAnalytics(false)}
>
Back to Search
</button>
</header>
<main className="analytics-container">
<h2>Search Analytics</h2>
{analyticsData.length === 0 ? (
<p>No search data yet. Start searching to build analytics!</p>
) : (
<div className="chart">
{analyticsData.map((item) => (
<div key={item.word} className="chart-row">
<span className="chart-label">{item.word}</span>
<div className="chart-bar-container">
<div
className="chart-bar"
style={{
width: `${(item.frequency / maxFreq) * 100}%`,
}}
></div>
</div>
<span className="chart-value">({item.frequency})</span>
</div>
))}
</div>
)}
</main>
</div>
);
}
return (
<div className="app">
<header className="header">
<h1>Intelligent Search Engine</h1>
<button
className="tab-button"
onClick={() => {
fetchAnalytics();
setShowAnalytics(true);
}}
>
Analytics
</button>
</header>
<main className="main-content">
<div className="search-section">
<div className="search-bar">
<input
type="text"
value={query}
onChange={(e) => setQuery(e.target.value)}
onKeyDown={handleKeyDown}
placeholder="Type to search..."
className="search-input"
/>
<button onClick={() => handleSearch()} className="search-button">
Search
</button>
</div>
{showSuggestions && suggestions.length > 0 && (
<ul className="suggestions-dropdown">
{suggestions.map((s) => (
<li
key={s.word}
onClick={() => handleSuggestionClick(s.word)}
className="suggestion-item"
>
{s.word}{" "}
<span className="suggestion-freq">({s.frequency})</span>
</li>
))}
</ul>
)}
{lastResult && (
<div className="search-result">
{lastResult.corrected && (
<p className="correction">
Corrected to: <strong>{lastResult.correction}</strong>
</p>
)}
<p>
{lastResult.found
? `Found: "${lastResult.query}"`
: `"${lastResult.query}" not found in database.`}
</p>
{lastResult.recommendations.length > 0 && (
<div className="inline-recommendations">
<span>Related: </span>
{lastResult.recommendations.map((r) => (
<button
key={r}
className="rec-chip"
onClick={() => handleSearch(r)}
>
{r}
</button>
))}
</div>
)}
</div>
)}
</div>
<aside className="sidebar">
<div className="panel">
<h3>Recent Searches</h3>
{recentSearches.length === 0 ? (
<p className="empty-message">No recent searches.</p>
) : (
<ul className="panel-list">
{recentSearches.map((item, index) => (
<li
key={index}
onClick={() => handleSearch(item)}
className="panel-item"
>
{item}
</li>
))}
</ul>
)}
</div>
<div className="panel">
<h3>Recommendations</h3>
{recommendations.length === 0 ? (
<p className="empty-message">
Search more to build recommendations!
</p>
) : (
<ul className="panel-list">
{recommendations.map((item) => (
<li
key={item}
onClick={() => handleSearch(item)}
className="panel-item"
>
{item}
</li>
))}
</ul>
)}
</div>
</aside>
</main>
</div>
);
}
export default App;
And here is the complete App.css file:
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
background-color: #f5f5f5;
color: #333;
}
.app {
max-width: 900px;
margin: 0 auto;
padding: 2rem;
}
.header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 2rem;
}
.header h1 {
font-size: 1.5rem;
color: #1a1a2e;
}
.tab-button {
padding: 0.5rem 1rem;
background: #1a1a2e;
color: white;
border: none;
border-radius: 6px;
cursor: pointer;
font-size: 0.9rem;
}
.tab-button:hover {
background: #16213e;
}
.main-content {
display: grid;
grid-template-columns: 1fr 280px;
gap: 2rem;
}
.search-section {
position: relative;
}
.search-bar {
display: flex;
gap: 0.5rem;
}
.search-input {
flex: 1;
padding: 0.75rem 1rem;
border: 2px solid #ddd;
border-radius: 8px;
font-size: 1rem;
outline: none;
transition: border-color 0.2s;
}
.search-input:focus {
border-color: #1a1a2e;
}
.search-button {
padding: 0.75rem 1.5rem;
background: #1a1a2e;
color: white;
border: none;
border-radius: 8px;
cursor: pointer;
font-size: 1rem;
}
.search-button:hover {
background: #16213e;
}
.suggestions-dropdown {
position: absolute;
top: 100%;
left: 0;
right: 70px;
background: white;
border: 1px solid #ddd;
border-radius: 8px;
list-style: none;
margin-top: 4px;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
z-index: 10;
}
.suggestion-item {
padding: 0.6rem 1rem;
cursor: pointer;
transition: background 0.15s;
}
.suggestion-item:hover {
background: #f0f0f0;
}
.suggestion-freq {
color: #888;
font-size: 0.85rem;
}
.search-result {
margin-top: 1.5rem;
padding: 1rem;
background: white;
border-radius: 8px;
border: 1px solid #eee;
}
.correction {
color: #e63946;
margin-bottom: 0.5rem;
}
.inline-recommendations {
margin-top: 0.75rem;
display: flex;
align-items: center;
gap: 0.5rem;
flex-wrap: wrap;
}
.rec-chip {
padding: 0.25rem 0.6rem;
background: #e8f4fd;
border: 1px solid #b3d9f2;
border-radius: 16px;
cursor: pointer;
font-size: 0.85rem;
color: #1a1a2e;
}
.rec-chip:hover {
background: #d0ebf7;
}
.sidebar {
display: flex;
flex-direction: column;
gap: 1.5rem;
}
.panel {
background: white;
border-radius: 8px;
padding: 1rem;
border: 1px solid #eee;
}
.panel h3 {
font-size: 0.95rem;
margin-bottom: 0.75rem;
color: #1a1a2e;
}
.panel-list {
list-style: none;
}
.panel-item {
padding: 0.4rem 0;
cursor: pointer;
color: #555;
transition: color 0.15s;
}
.panel-item:hover {
color: #1a1a2e;
}
.empty-message {
color: #999;
font-size: 0.85rem;
}
.analytics-container {
background: white;
border-radius: 8px;
padding: 2rem;
border: 1px solid #eee;
}
.analytics-container h2 {
margin-bottom: 1.5rem;
color: #1a1a2e;
}
.chart {
display: flex;
flex-direction: column;
gap: 0.75rem;
}
.chart-row {
display: flex;
align-items: center;
gap: 1rem;
}
.chart-label {
width: 120px;
text-align: right;
font-size: 0.9rem;
color: #555;
}
.chart-bar-container {
flex: 1;
height: 28px;
background: #f0f0f0;
border-radius: 4px;
overflow: hidden;
}
.chart-bar {
height: 100%;
background: linear-gradient(90deg, #1a1a2e, #16213e);
border-radius: 4px;
transition: width 0.3s ease;
}
.chart-value {
width: 40px;
font-size: 0.85rem;
color: #888;
}
Your frontend code is complete. In the next step, you will start both servers and test the full search flow end-to-end in your browser.
Connect Frontend to Backend and Test
Your backend API is running and your frontend code is complete. Now it's time to bring them together and see all those algorithms come alive in the browser.
In this step, you'll launch the frontend dev server alongside the backend, then test each feature end-to-end. You'll also confirm that search data persists across server restarts.
In this step, get ready to:
- Confirm the frontend-to-backend connection configuration.
- Run both servers and test autocomplete, spell correction, and recommendations.
- Verify data persistence across backend restarts.
Confirm the API connection configuration
For the frontend to talk to the backend, two pieces need to line up. The frontend needs the correct backend URL, and the backend needs to accept requests from the frontend's origin.
- In VS Code, open App.tsx and find this line near the top:
const API_BASE = "http://127.0.0.1:8000";
- Now open main.py and find the CORS middleware configuration near the top:
app.add_middleware(
CORSMiddleware,
allow_origins=["http://localhost:5173"],
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
What is CORS?
CORS (Cross-Origin Resource Sharing) is a browser security feature. When your frontend at http://localhost:5173 makes a request to your backend at http://127.0.0.1:8000, the browser considers them different origins.
Without the CORS middleware, the browser would block the request. The allow_origins setting tells FastAPI to include headers that permit the frontend to access the API.
Both pieces are already in place from the earlier steps. The frontend points to port 8000 and the backend allows requests from port 5173.
Run both servers and test the full flow
You need two terminal sessions running at the same time. One serves the backend API, and the other serves the frontend UI.
- In the terminal where your backend is already running, confirm you see output from fastapi dev main.py. Leave it running.
Backend not running?
If you stopped the backend earlier, reactivate it by navigating to your backend folder, activating the virtual environment, then running the server:
cd backend
.\venv\Scripts\activate
fastapi dev main.py
You should see a message indicating the server is running at http://127.0.0.1:8000.
- Open a second terminal in VS Code by clicking **Terminal** in the top menu bar, then **New Terminal**.
- Start the frontend dev server by running these commands in the new terminal:
cd frontend
npm run dev
What does this command do?
- cd frontend moves into the frontend project folder.
- npm run dev launches the Vite development server, which serves your React app and hot-reloads on file changes.
- You should see output showing the dev server running at http://localhost:5173.
- Open http://localhost:5173 in your browser.
You should see the Intelligent Search Engine interface with a search bar on the left and two sidebar panels (Recent Searches and Recommendations) on the right.
Seeing a blank page or network error?
- Make sure both terminals are running. The backend must be active at port 8000 and the frontend at port 5173.
- Check that you ran npm run dev from inside the frontend folder, not the root search-engine-web folder.
- Open your browser's DevTools console (press F12) and look for CORS or network errors.
Help me debug why my frontend can't reach the backend.
Now test each feature to make sure all the algorithms are working through the API.
- Type pro into the search bar. You should see a dropdown with autocomplete suggestions like program, programming, and processor.
- Click one of the suggestions (or type python and press Enter) to submit a search.
- Check that the Recent Searches panel on the right updates with your search.
- Search a second word like java. After this second search, you should see recommendations appear because the recommendation graph links consecutive searches.
- Test spell correction by typing a misspelled word like pythn and pressing Enter. You should see a red correction message saying it was corrected to python.
What should I see?
After a few searches, your interface should show recent searches in the top-right panel, related word recommendations in the bottom-right panel, and inline recommendation chips below search results.
Each autocomplete suggestion also displays a frequency count in parentheses. Words you search more often will appear higher in the autocomplete results.
Verify data persistence
The backend saves word frequencies to database.txt every time you search. This means your search history survives server restarts.
- Search a word a few times in the browser (for example, search python three times in a row).
- Switch to the terminal running your backend.
- Stop the backend server by pressing Ctrl+C.
- Open backend/database.txt in VS Code. You should see that the frequency number next to python has increased from its original value.
- Restart the backend by running:
fastapi dev main.py
- Go back to http://localhost:5173 in your browser.
- Type py in the search bar. The autocomplete dropdown should show python with the updated frequency count, confirming the data was loaded from disk.
Why does this work?
The Database class in main.py writes all word frequencies to database.txt after every search. When the server starts up, it reads that file and loads the frequencies back into the ranking system.
This is the same persistence pattern the original C++ project used, but saving on every request instead of only on exit. This protects against data loss from unexpected crashes.
Frequency didn't change?
- Make sure you actually pressed Enter or clicked Search after typing the word. Autocomplete alone does not trigger a search.
- Confirm the backend was running when you searched. If the backend was stopped, the frontend's fetch calls would fail silently.
- Check that database.txt is in the backend folder (the same folder as main.py).
Help me figure out why my search frequencies aren't persisting.
Your full-stack search engine is working end-to-end. The frontend sends requests, the backend processes them through the Trie, SpellChecker, and Ranking algorithms, and results flow back to the UI in real time. Next up, you'll tackle the Secret Mission and add a visual analytics dashboard.
Secret mission
Add a Visual Analytics Dashboard
Your search engine collects data on every search. Can you turn that raw frequency data into a visual bar chart that shows your most searched terms at a glance? The analytics code is already wired up in your app. Your mission is to understand how it works and prove it delivers accurate proportional visualizations.
Clean Up Your Resources
Clean Up Your Resources
Decide whether to keep your resources running, pause them to come back later, or delete them entirely.
This project runs entirely locally, so there are no ongoing costs.
Resources you used:
- search-engine-web folder on your Desktop with backend/ and frontend/ subdirectories.
- Python virtual environment (venv) in backend/.
- node_modules in frontend/.
Keep everything running
No action needed. Choose this if you plan to keep building on this project or want to revisit it soon.
- Your project stays on disk after you close the terminals and VS Code.
- No running processes persist after you close both terminal windows.
- To restart the project later, open VS Code, activate the backend virtual environment, run fastapi dev main.py in one terminal, and run npm run dev in the frontend/ folder in another.
Pause - I'll come back to this later
Shut down running processes to free up system resources. Your files remain on disk for later.
- In the terminal running the backend server, press Ctrl+C to stop the FastAPI server.
- In the terminal running the frontend server, press Ctrl+C to stop the Vite dev server.
- Close VS Code and both terminal windows.
- Your search-engine-web folder remains on your Desktop. Reopen it in VS Code whenever you want to continue.
Delete - I don't want to use this again
Remove all project files and start fresh. This permanently deletes your code and the word database.
- In the terminal running the backend server, press Ctrl+C to stop the FastAPI server.
- In the terminal running the frontend server, press Ctrl+C to stop the Vite dev server.
- Delete the entire project folder. On macOS/Linux, run:
rm -rf ~/Desktop/search-engine-web
On Windows, run:
rmdir /s /q %USERPROFILE%\Desktop\search-engine-web
- This removes the backend, frontend, virtual environment, node_modules, and database.txt all at once.
Nice Work!
Nice Work!
You just built a full-stack intelligent search engine from scratch. Your app combines classic computer science algorithms with a modern web architecture, all running locally on your machine.
You've learned how to:
- Build a Trie data structure and Levenshtein distance spell checker in Python, then expose them as REST API endpoints via FastAPI.
- Implement an LRU Cache, frequency-based ranking, and a graph-based recommendation engine that persist between sessions using file I/O.
- Connect a React + TypeScript frontend to a FastAPI backend with live autocomplete, spell correction, recent searches, and recommendations.
- Secret Mission: Added a visual analytics dashboard with proportional bar charts showing search frequency patterns.
Ready to quiz yourself?