JavaScript Search App Tutorial: Building a Simple Document Search Engine
This tutorial will guide you through building a simple web-based document search engine using the LocalVectorDB server API. You’ll create a single-page application that can search through documents using vector similarity, keyword search, or hybrid search - all without writing any Python code!
What We’ll Build
We’ll create a clean, responsive web application that:
Connects to a LocalVectorDB server via REST API
Provides a search interface with different search types
Displays search results with relevance scores
Shows document metadata and content previews
Includes basic error handling and loading states
No Python knowledge required - just HTML, CSS, and JavaScript!
Prerequisites
Before starting, ensure you have:
Python 3.12 or higher
A web browser (Chrome, Firefox, Safari, etc.)
Basic knowledge of HTML, CSS, and JavaScript
A text editor or IDE
Setting Up the Server
First, let’s set up and start the LocalVectorDB server using the command-line interface.
Install and Configure LocalVectorDB
# Install LocalVectorDB (requires Python 3.12+)
pip install localvectordb
# Create a basic configuration file
lvdb config init --interactive
During the interactive setup, choose these options:
Configuration file path: ./.lvdb-config.toml (default)
Server host: 127.0.0.1
Server port: 8000
Database directory: ./search-demo-db
Enable CORS: Yes (important for web applications!)
CORS origins: localhost or all (for development only)
API Authentication: No (to keep it simple)
Create Sample Documents and Database
Let’s create some sample documents to search through. First, create a folder for our documents:
# Create a folder for sample documents
mkdir server-docs
# Copy some files that you'd like to search
cp /path/to/file server-docs/file
# ...etc.
Now create the database and add all the documents:
# Create a new database for our demo
lvdb create search_demo
# Add all documents from the server-docs folder
# NOTE: only plaintext files can be added from the cli this way. You must convert PDFs or DOCX files first.
lvdb db search_demo add "server-docs/*.txt"
Start the Server
# Start the LocalVectorDB server
lvdb serve
You should see output indicating the server is running on http://127.0.0.1:8000. Keep this terminal window open while developing the web application.
Creating the Web Application
Now let’s build our single-page search application. We’ll create three files: HTML for structure, CSS for styling, and JavaScript for functionality.
HTML Structure (index.html)
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>LocalVectorDB Search Demo</title>
<link rel="stylesheet" href="styles.css">
</head>
<body>
<div class="container">
<!-- Header -->
<header class="header">
<h1>Document Search Engine</h1>
<p>Powered by LocalVectorDB</p>
</header>
<!-- Search Form -->
<section class="search-section">
<form id="searchForm" class="search-form">
<div class="search-input-container">
<input
type="text"
id="searchInput"
placeholder="Enter your search query..."
required
autocomplete="off"
>
<button type="submit" id="searchButton">
<span class="button-text">Search</span>
<span class="spinner" id="spinner">⟳</span>
</button>
</div>
<div class="search-options">
<label class="option-group">
<span>Search Type:</span>
<select id="searchType">
<option value="hybrid">Hybrid (Best Results)</option>
<option value="vector">Vector Similarity</option>
<option value="keyword">Keyword Search</option>
</select>
</label>
<label class="option-group">
<span>Max Results:</span>
<select id="maxResults">
<option value="5">5</option>
<option value="10" selected>10</option>
<option value="20">20</option>
</select>
</label>
</div>
</form>
</section>
<!-- Results Section -->
<section class="results-section">
<div id="resultsHeader" class="results-header hidden">
<h2>Search Results</h2>
<span id="resultsCount" class="results-count"></span>
</div>
<div id="resultsContainer" class="results-container">
<!-- Results will be inserted here -->
</div>
<div id="noResults" class="no-results hidden">
<h3>No results found</h3>
<p>Try adjusting your search terms or using a different search type.</p>
</div>
<div id="errorMessage" class="error-message hidden">
<h3>Something went wrong</h3>
<p id="errorText">Please check if the LocalVectorDB server is running.</p>
</div>
</section>
<!-- Footer -->
<footer class="footer">
<p>
Built with
<a href="https://github.com/thomas-villani/localvectordb" target="_blank">LocalVectorDB</a>
• Search powered by AI embeddings
</p>
</footer>
</div>
<script src="script.js"></script>
</body>
</html>
CSS Styling (styles.css)
/* Reset and base styles */
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
line-height: 1.6;
color: #333;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
min-height: 100vh;
}
.container {
max-width: 800px;
margin: 0 auto;
padding: 20px;
min-height: 100vh;
display: flex;
flex-direction: column;
}
/* Header */
.header {
text-align: center;
margin-bottom: 40px;
color: white;
}
.header h1 {
font-size: 2.5rem;
margin-bottom: 10px;
text-shadow: 0 2px 4px rgba(0,0,0,0.3);
}
.header p {
font-size: 1.1rem;
opacity: 0.9;
}
/* Search Section */
.search-section {
background: white;
border-radius: 12px;
padding: 30px;
box-shadow: 0 8px 32px rgba(0,0,0,0.1);
margin-bottom: 30px;
}
.search-form {
display: flex;
flex-direction: column;
gap: 20px;
}
.search-input-container {
display: flex;
gap: 12px;
align-items: center;
}
#searchInput {
flex: 1;
padding: 16px 20px;
border: 2px solid #e1e5e9;
border-radius: 8px;
font-size: 16px;
transition: border-color 0.3s ease;
}
#searchInput:focus {
outline: none;
border-color: #667eea;
box-shadow: 0 0 0 3px rgba(102, 126, 234, 0.1);
}
#searchButton {
padding: 16px 24px;
background: #667eea;
color: white;
border: none;
border-radius: 8px;
font-size: 16px;
font-weight: 600;
cursor: pointer;
transition: all 0.3s ease;
position: relative;
min-width: 100px;
}
#searchButton:hover {
background: #5a6fd8;
transform: translateY(-1px);
}
#searchButton:disabled {
background: #94a3b8;
cursor: not-allowed;
transform: none;
}
.spinner {
display: none;
animation: spin 1s linear infinite;
}
.searching .button-text {
display: none;
}
.searching .spinner {
display: inline;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
.search-options {
display: flex;
gap: 20px;
flex-wrap: wrap;
}
.option-group {
display: flex;
flex-direction: column;
gap: 8px;
font-weight: 500;
}
.option-group select {
padding: 8px 12px;
border: 2px solid #e1e5e9;
border-radius: 6px;
font-size: 14px;
background: white;
}
/* Results Section */
.results-section {
flex: 1;
}
.results-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20px;
color: white;
}
.results-header h2 {
font-size: 1.5rem;
}
.results-count {
background: rgba(255,255,255,0.2);
padding: 8px 16px;
border-radius: 20px;
font-size: 14px;
font-weight: 500;
}
.results-container {
display: flex;
flex-direction: column;
gap: 16px;
}
.result-card {
background: white;
border-radius: 8px;
padding: 24px;
box-shadow: 0 4px 16px rgba(0,0,0,0.1);
transition: transform 0.2s ease, box-shadow 0.2s ease;
}
.result-card:hover {
transform: translateY(-2px);
box-shadow: 0 8px 24px rgba(0,0,0,0.15);
}
.result-header {
display: flex;
justify-content: space-between;
align-items: flex-start;
margin-bottom: 12px;
}
.result-title {
font-size: 1.1rem;
font-weight: 600;
color: #1e293b;
}
.result-score {
background: #667eea;
color: white;
padding: 4px 12px;
border-radius: 12px;
font-size: 12px;
font-weight: 600;
}
.result-content {
color: #64748b;
line-height: 1.7;
margin-bottom: 12px;
}
.result-metadata {
display: flex;
gap: 16px;
font-size: 12px;
color: #94a3b8;
}
.metadata-item {
display: flex;
align-items: center;
gap: 4px;
}
/* Empty states */
.no-results, .error-message {
text-align: center;
padding: 60px 20px;
color: white;
}
.no-results-icon, .error-icon {
font-size: 4rem;
margin-bottom: 20px;
}
.no-results h3, .error-message h3 {
font-size: 1.5rem;
margin-bottom: 12px;
}
.no-results p, .error-message p {
opacity: 0.8;
font-size: 1.1rem;
}
/* Footer */
.footer {
text-align: center;
margin-top: 40px;
padding: 20px;
color: rgba(255,255,255,0.8);
}
.footer a {
color: white;
text-decoration: none;
font-weight: 500;
}
.footer a:hover {
text-decoration: underline;
}
/* Utility classes */
.hidden {
display: none !important;
}
/* Responsive design */
@media (max-width: 600px) {
.container {
padding: 16px;
}
.header h1 {
font-size: 2rem;
}
.search-section {
padding: 20px;
}
.search-input-container {
flex-direction: column;
}
#searchButton {
width: 100%;
}
.search-options {
flex-direction: column;
}
.results-header {
flex-direction: column;
gap: 12px;
align-items: flex-start;
}
.result-header {
flex-direction: column;
gap: 8px;
align-items: flex-start;
}
.result-metadata {
flex-direction: column;
gap: 8px;
}
}
JavaScript Functionality (script.js)
// Configuration
const API_BASE_URL = 'http://127.0.0.1:8000/api/v1';
const DATABASE_NAME = 'search_demo';
// DOM Elements
const searchForm = document.getElementById('searchForm');
const searchInput = document.getElementById('searchInput');
const searchButton = document.getElementById('searchButton');
const searchType = document.getElementById('searchType');
const maxResults = document.getElementById('maxResults');
const resultsHeader = document.getElementById('resultsHeader');
const resultsCount = document.getElementById('resultsCount');
const resultsContainer = document.getElementById('resultsContainer');
const noResults = document.getElementById('noResults');
const errorMessage = document.getElementById('errorMessage');
const errorText = document.getElementById('errorText');
// State
let isSearching = false;
// Initialize the application
document.addEventListener('DOMContentLoaded', function() {
console.log('LocalVectorDB Search Demo initialized');
// Check server connection on startup
checkServerConnection();
// Set up event listeners
searchForm.addEventListener('submit', handleSearch);
searchInput.addEventListener('keyup', handleSearchInputChange);
// Focus on search input
searchInput.focus();
});
/**
* Check if the LocalVectorDB server is accessible
*/
async function checkServerConnection() {
try {
const response = await fetch(`${API_BASE_URL}/health`);
if (response.ok) {
console.log('✅ Server connection successful');
} else {
throw new Error(`Server responded with status: ${response.status}`);
}
} catch (error) {
console.error('❌ Server connection failed:', error);
showError('Cannot connect to LocalVectorDB server. Please ensure it is running on http://127.0.0.1:8000');
}
}
/**
* Handle search form submission
*/
async function handleSearch(event) {
event.preventDefault();
if (isSearching) return;
const query = searchInput.value.trim();
if (!query) {
searchInput.focus();
return;
}
await performSearch(query);
}
/**
* Handle search input changes
*/
function handleSearchInputChange(event) {
// Enable search on Enter key
if (event.key === 'Enter') {
handleSearch(event);
}
}
/**
* Perform the actual search
*/
async function performSearch(query) {
setSearchingState(true);
hideAllStates();
try {
console.log(`Searching for: "${query}"`);
const searchParams = {
query: query,
search_type: searchType.value,
return_type: 'documents',
k: parseInt(maxResults.value),
score_threshold: 0.0
};
const response = await fetch(`${API_BASE_URL}/${DATABASE_NAME}/query`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(searchParams)
});
if (!response.ok) {
const errorData = await response.json().catch(() => ({}));
throw new Error(errorData.error?.message || `HTTP ${response.status}: ${response.statusText}`);
}
const data = await response.json();
console.log('Search results:', data);
displayResults(data.results, query);
} catch (error) {
console.error('Search failed:', error);
showError(`Search failed: ${error.message}`);
} finally {
setSearchingState(false);
}
}
/**
* Display search results
*/
function displayResults(results, query) {
hideAllStates();
if (!results || results.length === 0) {
showNoResults();
return;
}
// Show results header
resultsHeader.classList.remove('hidden');
resultsCount.textContent = `${results.length} result${results.length !== 1 ? 's' : ''} for "${query}"`;
// Clear previous results
resultsContainer.innerHTML = '';
// Create result cards
results.forEach((result, index) => {
const resultCard = createResultCard(result, index + 1);
resultsContainer.appendChild(resultCard);
});
// Scroll to results
resultsHeader.scrollIntoView({ behavior: 'smooth', block: 'start' });
}
/**
* Create a result card element
*/
function createResultCard(result, position) {
const card = document.createElement('div');
card.className = 'result-card';
// Format score as percentage
const scorePercent = Math.round(result.score * 100);
// Truncate content if too long
const maxContentLength = 300;
let content = result.content;
if (content.length > maxContentLength) {
content = content.substring(0, maxContentLength) + '...';
}
card.innerHTML = `
<div class="result-header">
<div class="result-title">Document #${position}</div>
<div class="result-score">${scorePercent}% match</div>
</div>
<div class="result-content">${escapeHtml(content)}</div>
<div class="result-metadata">
<div class="metadata-item">
<span>ID: ${escapeHtml(result.id)}</span>
</div>
<div class="metadata-item">
<span>Type: ${escapeHtml(result.type)}</span>
</div>
<div class="metadata-item">
<span>Length: ${result.content.length} chars</span>
</div>
</div>
`;
return card;
}
/**
* Set searching state
*/
function setSearchingState(searching) {
isSearching = searching;
if (searching) {
searchButton.classList.add('searching');
searchButton.disabled = true;
searchInput.disabled = true;
} else {
searchButton.classList.remove('searching');
searchButton.disabled = false;
searchInput.disabled = false;
}
}
/**
* Hide all result states
*/
function hideAllStates() {
resultsHeader.classList.add('hidden');
noResults.classList.add('hidden');
errorMessage.classList.add('hidden');
resultsContainer.innerHTML = '';
}
/**
* Show no results state
*/
function showNoResults() {
hideAllStates();
noResults.classList.remove('hidden');
}
/**
* Show error state
*/
function showError(message) {
hideAllStates();
errorText.textContent = message;
errorMessage.classList.remove('hidden');
}
/**
* Escape HTML to prevent XSS
*/
function escapeHtml(text) {
const div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
}
/**
* Utility function to format numbers
*/
function formatNumber(num) {
return new Intl.NumberFormat().format(num);
}
/**
* Add some helpful keyboard shortcuts
*/
document.addEventListener('keydown', function(event) {
// Focus search input when pressing '/' key
if (event.key === '/' && event.target !== searchInput) {
event.preventDefault();
searchInput.focus();
searchInput.select();
}
// Clear search when pressing Escape
if (event.key === 'Escape') {
if (document.activeElement === searchInput) {
searchInput.value = '';
hideAllStates();
} else {
searchInput.focus();
}
}
});
// Add some helpful console commands for development
if (typeof window !== 'undefined') {
window.searchDemo = {
search: performSearch,
checkConnection: checkServerConnection,
config: {
apiUrl: API_BASE_URL,
database: DATABASE_NAME
}
};
console.log('Development tools available at window.searchDemo');
}
Running the Application
Now let’s put it all together and run our search application:
Create the Project Structure
Create your web application files in the same directory where you have your configuration:
# You should already be in your main project directory
# Create the web application files
touch index.html styles.css script.js
Add the code from the sections above to each respective file.
Start the LocalVectorDB Server
The LocalVectorDB server is a FastAPI application that exposes the REST API under
/api/v1 – it does not serve your static HTML/CSS/JS files. We’ll run the API server
here and load the front-end files separately in the next step.
# Start the LocalVectorDB server (serves the API only)
lvdb serve
You should see output indicating the server is running on http://127.0.0.1:8000.
Access Your Search Engine
Because the LocalVectorDB server only serves the API (not static files), open the front-end yourself. You have two easy options:
Option A – open the file directly
Just double-click index.html (or drag it into your browser). It will load from a
file:// URL and call the API at http://127.0.0.1:8000. This is a cross-origin request,
which is exactly why we enabled CORS during configuration – without it the browser would
block the requests.
Option B – run a tiny static file server
Serve the three files over HTTP from their directory using Python’s built-in server:
# In the directory containing index.html, styles.css, script.js
python -m http.server 8080
Then:
Make sure your LocalVectorDB API server is running on port 8000 (
lvdb serve).Open your browser and navigate to
http://127.0.0.1:8080/index.html.(Either option) The page calls the API at
http://127.0.0.1:8000/api/v1– keep that server running too.
You should see your search engine! Try searching for terms like:
“programming language”
“machine learning”
“web development”
“artificial intelligence”
Testing Different Search Types
Your search engine supports three different search modes:
- Vector Similarity Search
Uses AI embeddings to find semantically similar content. Great for finding documents with similar meaning even if they use different words.
- Keyword Search
Traditional text search that looks for exact word matches. Fast and precise for finding specific terms.
- Hybrid Search
Combines both vector and keyword search for the best of both worlds. Usually provides the most relevant results.
Try the same query with different search types to see how the results differ!
Adding More Documents
You can easily add more documents to your search engine using the CLI:
# Add a single document file
lvdb db search_demo add /path/to/your/document.txt
# Add multiple documents from a folder
lvdb db search_demo add "/path/to/documents/*.txt"
# Add documents with custom metadata
lvdb db search_demo add server-docs/new-doc.txt --metadata '{"category":"tutorial","author":"you"}'
# Add all files from your server-docs folder again (if you add more)
lvdb db search_demo add "server-docs/*.txt"
Troubleshooting
- CORS Errors
Make sure you enabled CORS when configuring the server. You can also add CORS headers manually:
lvdb config set server.security.cors_enabled true lvdb config set server.security.cors_allowed_origins '["http://localhost:8080"]'
- Server Connection Failed
Verify the LocalVectorDB server is running:
# Check if server is responding curl http://127.0.0.1:8000/api/v1/health
- No Search Results
Make sure you have documents in your database:
lvdb db search_demo list
- JavaScript Errors
Open your browser’s developer console (F12) to see detailed error messages.
Enhancing the Application
Here are some ideas to extend your search engine:
Add Document Upload
The server has two distinct endpoints for adding documents, and they take different payloads:
POST /{db}/documents– JSON body for text you already have in memory ({"documents": [...], "metadata": [...]}).POST /{db}/upload– multipart/form-data for uploading actual files; the server extracts the text for you. File upload must be enabled on the server (lvdb config init --enable-file-upload/lvdb config set server.file_upload_enabled true).
Upload a real file (multipart) to the /upload endpoint:
// Wire this to an <input type="file"> element, e.g. input.files[0]
function handleFileUpload(file) {
const formData = new FormData();
formData.append('files', file); // the server extracts text from the file
formData.append('metadata', JSON.stringify({
filename: file.name,
uploaded_at: new Date().toISOString()
}));
return fetch(`${API_BASE_URL}/${DATABASE_NAME}/upload`, {
method: 'POST',
body: formData // do NOT set Content-Type; the browser sets the multipart boundary
});
}
Or send raw text you already have as JSON to the /documents endpoint:
function addText(text) {
return fetch(`${API_BASE_URL}/${DATABASE_NAME}/documents`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
documents: [text],
metadata: [{ uploaded_at: new Date().toISOString() }]
})
});
}
Add Result Filtering
// Add metadata filters to your search
const searchParams = {
query: query,
search_type: searchType.value,
filters: {
category: selectedCategory,
author: selectedAuthor
}
};
Add Search History
// Store searches in localStorage
function saveSearchHistory(query, resultCount) {
const history = JSON.parse(localStorage.getItem('searchHistory') || '[]');
history.unshift({ query, resultCount, timestamp: Date.now() });
localStorage.setItem('searchHistory', JSON.stringify(history.slice(0, 10)));
}
Add Real-time Search
// Debounced search as user types
let searchTimeout;
searchInput.addEventListener('input', function() {
clearTimeout(searchTimeout);
searchTimeout = setTimeout(() => {
if (searchInput.value.length > 2) {
performSearch(searchInput.value);
}
}, 500);
});
Deployment Options
- Static Hosting
Deploy your search engine to GitHub Pages, Netlify, or Vercel. Just make sure to update the API URL to point to your deployed server.
- Docker Container
Package both the server and web app in a Docker container for easy deployment.
- Cloud Deployment
Deploy the LocalVectorDB server to cloud platforms like Railway, Render, or DigitalOcean.
Conclusion
Congratulations! You’ve built a complete document search engine using LocalVectorDB and vanilla JavaScript. Your application demonstrates:
Core Features - Vector similarity search with AI embeddings - Multiple search types (vector, keyword, hybrid) - Clean, responsive user interface - Real-time search capabilities - Error handling and loading states
Technical Skills - REST API integration - Modern JavaScript (async/await, fetch) - Responsive CSS design - DOM manipulation - User experience best practices
LocalVectorDB Integration - Server configuration and management - Document ingestion via CLI - RESTful API usage - Search result processing
This foundation can be extended into more sophisticated applications like document management systems, knowledge bases, or AI-powered search engines. The modular design makes it easy to add features like authentication, file uploads, advanced filtering, and more.
Happy searching!