This procedure describes how to create a WordPress chatbot using FAISS for RAG and an external LLM API. We start by scanning the database of WordPress posts, to create a FAISS vector database. We then create an API wrapper that combines hinting information from the local FAISS database with a call to a remote LLM API. This API wrapper is then called by a chatbot, which is then integrated into WordPress as a plugin. The user interface for the chatbot is added to the sidebar of the WordPress blog by adding a shortcode widget that references the chatbot’s PHP, JavaScript, and cascading stylesheet (CSS) elements.
The chatbot accepts natural language queries, submits the queries to the RAG API wrapper, and displays results that contain the remote LLM API’s responses based on the text of blog posts scanned by the RAG system. Links to relevant blog posts are listed in the responses.
Using a recent Linux distribution to support Python 3.12 and some machine learning tools
In order to implement this procedure, we need a recent Linux distribution to support Python 3.12 and some machine learning tools. For this procedure we are using Ubuntu Server 24.04 LTS.
Using a server with relatively modest specifications
Most public-facing websites are hosted in virtual machines (VMs) on cloud servers, with relatively modest specifications. Because we are able to use an external LLM API service, we only need enough processing power to host the WordPress blog itself, as well as some Python and PHP code that implements the FAISS vector database, the RAG API wrapper, and the chatbot itself. For this procedure, we are deploying on a cloud server with 2GB RAM, 2 x vCPU, and 50GB SSD drive space.
Escalating to the root user
Enter the following command:
sudo su
Installing operating system dependencies
Enter the following command:
apt install php-curl libmariadb-dev python3-pip python3-venv
Creating a virtual environment for Python
Enter the following commands:
cd /var/www/html
mkdir ragblog_workdir
cd ragblog_workdir
python3 -m venv ragblog_env
source ragblog_env/bin/activate
Installing Python dependencies
Enter the following command:
pip install faiss-cpu sentence-transformers numpy fastapi uvicorn requests python-dotenv mariadb
Creating a .env file
Enter the following command:
nano .env
Use the nano editor to add the following text. Substitute values as appropriate for your environment:
EXTERNAL_LLM_API=https://api.lemonfox.ai/v1/chat/completions
EXTERNAL_LLM_API_KEY=xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
BLOG_URL_BASE=https://yourdomain/blog
DB_USER=db_user
DB_PASSWORD=db_password
DB_HOST=localhost
DB_NAME=db_name
Save and exit the file.
Creating the FAISS indexing script
Enter the following command:
nano rag_faiss.py
Use the nano editor to add the following text:
import faiss
import numpy as np
import json
import os
import mariadb
from sentence_transformers import SentenceTransformer
from dotenv import load_dotenv
# MIT license Gordon Buchan 2025
# see https://opensource.org/license/mit
# some of the code was generated with the assistance of AI tools.
# Load environment variables from .env file
load_dotenv(dotenv_path="./.env")
DB_USER = os.getenv('DB_USER')
DB_PASSWORD = os.getenv('DB_PASSWORD')
DB_HOST = os.getenv('DB_HOST')
DB_NAME = os.getenv('DB_NAME')
# Load embedding model
model = SentenceTransformer("all-MiniLM-L6-v2")
# FAISS setup
embedding_dim = 384
index_file = "faiss_index.bin"
metadata_file = "faiss_metadata.json"
# Load FAISS index and metadata
if os.path.exists(index_file):
index = faiss.read_index(index_file)
with open(metadata_file, "r") as f:
metadata = json.load(f)
metadata = {int(k): v for k, v in metadata.items()} # Ensure integer keys
print(f"📂 Loaded existing FAISS index with {index.ntotal} embeddings.")
else:
index = faiss.IndexHNSWFlat(embedding_dim, 32)
metadata = {}
print("🆕 Created a new FAISS index.")
def chunk_text(text, chunk_size=500):
"""Split text into smaller chunks"""
words = text.split()
return [" ".join(words[i:i + chunk_size]) for i in range(0, len(words), chunk_size)]
def get_blog_posts():
"""Fetch published blog posts from WordPress database."""
try:
conn = mariadb.connect(
user=DB_USER,
password=DB_PASSWORD,
host=DB_HOST,
database=DB_NAME
)
cursor = conn.cursor()
cursor.execute("""
SELECT ID, post_title, post_content
FROM wp_posts
WHERE post_status='publish' AND post_type='post'
""")
posts = cursor.fetchall()
conn.close()
return posts
except mariadb.Error as e:
print(f"❌ Database error: {e}")
return []
def index_blog_posts():
"""Index only new blog posts in FAISS"""
blog_posts = get_blog_posts()
if not blog_posts:
print("❌ No blog posts found. Check database connection.")
return
vectors = []
new_metadata = {}
current_index = len(metadata)
print(f"📝 Found {len(blog_posts)} blog posts to check for indexing.")
for post_id, title, content in blog_posts:
if any(str(idx) for idx in metadata if metadata[idx]["post_id"] == post_id):
print(f"🔄 Skipping already indexed post: {title} (ID: {post_id})")
continue
chunks = chunk_text(content)
for chunk in chunks:
embedding = model.encode(chunk, normalize_embeddings=True) # Normalize embeddings
vectors.append(embedding)
new_metadata[current_index] = {
"post_id": post_id,
"title": title,
"chunk_text": chunk
}
current_index += 1
if vectors:
faiss_vectors = np.array(vectors, dtype=np.float32)
index.add(faiss_vectors)
metadata.update(new_metadata)
faiss.write_index(index, index_file)
with open(metadata_file, "w") as f:
json.dump(metadata, f, indent=4)
print(f"✅ Indexed {len(new_metadata)} new chunks.")
else:
print("✅ No new posts to index.")
if __name__ == "__main__":
index_blog_posts()
print("✅ Indexing completed.")
Save and exit the file.
Creating the FAISS retrieval API
Enter the following command:
nano faiss_search.py
Use the nano editor to add text to the file:
import os
import faiss
import numpy as np
import json
from sentence_transformers import SentenceTransformer # ✅ Ensure this is imported
# MIT license Gordon Buchan 2025
# see https://opensource.org/license/mit
# some of the code was generated with the assistance of AI tools.
# ✅ Load the same embedding model used in `rag_api_wrapper.py`
model = SentenceTransformer("all-MiniLM-L6-v2")
# ✅ Load FAISS index and metadata
index_file = "faiss_index.bin"
metadata_file = "faiss_metadata.json"
embedding_dim = 384
if os.path.exists(index_file):
index = faiss.read_index(index_file)
with open(metadata_file, "r") as f:
metadata = json.load(f)
else:
index = faiss.IndexFlatL2(embedding_dim)
metadata = {}
def search_faiss(query_text, top_k=10):
"""Search FAISS index and retrieve relevant metadata"""
query_embedding = model.encode(query_text).reshape(1, -1) # ✅ Ensure `model` is used correctly
_, indices = index.search(query_embedding, top_k)
results = []
for idx in indices[0]:
if str(idx) in metadata: # ✅ Convert index to string to match JSON keys
results.append(metadata[str(idx)])
return results
Save and exit the file.
Creating the RAG API wrapper
Enter the following command:
nano rag_api_wrapper.py
Use the nano editor to add the following text:
from fastapi import FastAPI, HTTPException
import requests
import os
import json
import faiss
import numpy as np
from dotenv import load_dotenv
from sentence_transformers import SentenceTransformer
# MIT license Gordon Buchan 2025
# see https://opensource.org/license/mit
# some of the code was generated with the assistance of AI tools.
# Load environment variables
load_dotenv(dotenv_path="./.env")
EXTERNAL_LLM_API = os.getenv('EXTERNAL_LLM_API')
EXTERNAL_LLM_API_KEY = os.getenv('EXTERNAL_LLM_API_KEY')
BLOG_URL_BASE = os.getenv('BLOG_URL_BASE')
# Load FAISS index and metadata
embedding_dim = 384
index_file = "faiss_index.bin"
metadata_file = "faiss_metadata.json"
if os.path.exists(index_file):
index = faiss.read_index(index_file)
with open(metadata_file, "r") as f:
metadata = json.load(f)
metadata = {int(k): v for k, v in metadata.items()} # Ensure integer keys
print(f"📂 Loaded FAISS index with {index.ntotal} embeddings.")
else:
index = faiss.IndexHNSWFlat(embedding_dim, 32)
metadata = {}
print("❌ No FAISS index found.")
# Load embedding model
model = SentenceTransformer("all-MiniLM-L6-v2")
app = FastAPI()
def search_faiss(query_text, top_k=3):
"""Retrieve top K relevant chunks from FAISS index"""
if index.ntotal == 0:
return []
query_embedding = model.encode(query_text, normalize_embeddings=True).reshape(1, -1)
distances, indices = index.search(query_embedding, top_k)
results = []
for idx in indices[0]:
if idx in metadata:
post_id = metadata[idx]["post_id"]
title = metadata[idx]["title"]
chunk_text = metadata[idx]["chunk_text"]
post_url = f"{BLOG_URL_BASE}/?p={post_id}"
# Limit chunk text to 300 characters for cleaner display
short_chunk = chunk_text[:300] + "..." if len(chunk_text) > 300 else chunk_text
results.append(f"📌 {title}: {short_chunk} (Read more: {post_url})")
return results[:3] # Limit to max 3 sources
@app.post("/v1/chat/completions")
def chat_completions(request: dict):
if "messages" not in request:
raise HTTPException(status_code=400, detail="No messages provided.")
user_query = request["messages"][-1]["content"]
# Retrieve relevant blog context
context_snippets = search_faiss(user_query)
context_text = "\n".join(context_snippets) if context_snippets else "No relevant sources found."
# Send query with context to LLM API
payload = {
"model": "llama-8b-chat",
"messages": [
{"role": "system", "content": "Use the following blog snippets to provide a detailed response."},
{"role": "user", "content": f"{user_query}\n\nContext:\n{context_text}"}
]
}
headers = {"Authorization": f"Bearer {EXTERNAL_LLM_API_KEY}"}
response = requests.post(EXTERNAL_LLM_API, json=payload, headers=headers)
if response.status_code != 200:
raise HTTPException(status_code=500, detail="External LLM API request failed.")
llm_response = response.json()
response_text = llm_response["choices"][0]["message"]["content"]
return {
"id": llm_response.get("id", "generated_id"),
"object": "chat.completion",
"created": llm_response.get("created", 1700000000),
"model": llm_response.get("model", "llama-8b-chat"),
"choices": [
{
"message": {
"role": "assistant",
"content": f"{response_text}\n\n📚 Sources:\n{context_text}"
}
}
],
"usage": llm_response.get("usage", {"prompt_tokens": 0, "completion_tokens": 0, "total_tokens": 0})
}
Save and exit the file.
Running the rag_faiss.py file manually to create the FAISS vector database
Enter the following command:
python3 rag_faiss.py
Starting the RAG API wrapper manually to test the system
Enter the following command:
uvicorn rag_api_wrapper:app --host 127.0.0.1 --port 8000
Testing the RAG API wrapper with a curl command
Enter the following command:
curl -X POST http://localhost:8000/v1/chat/completions -H "Content-Type: application/json" -d '{"messages": [{"role": "user", "content": "How do I use an external LLM API?"}]}'
Creating the ragblog-faiss service
Enter the following command:
nano ragblog-faiss.service
Use the nano editor to add the following text:
[Unit]
Description=ragblog-faiss
After=network.target
[Service]
User=root
WorkingDirectory=/var/www/html/ragblog_workdir
ExecStart=/usr/bin/bash -c "source /var/www/html/ragblog_workdir/ragblog_env/bin/activate && python3 /var/www/html/ragblog_workdir/rag_faiss.py"
Restart=always
Environment=PYTHONUNBUFFERED=1
StandardOutput=journal
StandardError=journal
[Install]
WantedBy=multi-user.target
Save and exit the file.
Creating the ragblog-faiss timer
Enter the following command:
nano ragblog-faiss.timer
Use the nano editor to add the following text:
[Unit]
Description=Run monitoring tasks every 5 minutes
[Timer]
OnBootSec=5min
OnUnitActiveSec=5min
[Install]
WantedBy=timers.target
Save and exit the file.
Enabling and starting the ragblog-faiss service and the ragblog-faiss timer
Enter the following commands:
systemctl daemon-reload
systemctl enable ragblog-faiss.service
systemctl start ragblog-faiss.service
systemctl enable ragblog-faiss.timer
systemctl start ragblog-faiss.timer
Creating the ragblog-api-wrapper service
Enter the following command:
nano /etc/systemd/system/ragblog-api-wrapper.service
Use the nano editor to add the following text:
[Unit]
Description=ragblog-api-wrapper service
After=network.target
[Service]
User=root
WorkingDirectory=/var/www/html/ragblog_workdir
ExecStart=/usr/bin/bash -c "source /var/www/html/ragblog_workdir/ragblog_env/bin/activate && uvicorn rag_api_wrapper:app --host 127.0.0.1 --port 8000"
Restart=always
Environment=PYTHONUNBUFFERED=1
StandardOutput=journal
StandardError=journal
[Install]
WantedBy=multi-user.target
Save and exit the file.
Enabling and starting the ragblog-api wrapper service
Enter the following commands:
systemctl daemon-reload
systemctl enable ragblog-api-wrapper.service
systemctl start ragblog-api.service
Creating the chatbot.php file
Enter the following command (adjust values to match your environment):
nano /var/www/html/yourdomain.com/blog/chatbot.php
Use the nano editor to add the following text:
<?php
header("Content-Type: text/html");
// MIT license Gordon Buchan 2025
// see https://opensource.org/license/mit
// some of the code was generated with the assistance of AI tools.
// If this is a POST request, process the chatbot response
if ($_SERVER["REQUEST_METHOD"] === "POST") {
header("Content-Type: application/json");
// Read raw POST input
$raw_input = file_get_contents("php://input");
// Debugging: Log received input
error_log("Received input: " . $raw_input);
// If raw input is empty, fallback to $_POST
if (!$raw_input) {
$raw_input = json_encode($_POST);
}
// Decode JSON input
$data = json_decode($raw_input, true);
// Check if JSON decoding was successful
if (json_last_error() !== JSON_ERROR_NONE) {
echo json_encode(["error" => "Invalid JSON format"]);
exit;
}
// Validate the message field
if (!isset($data["message"]) || empty(trim($data["message"]))) {
echo json_encode(["error" => "Invalid input: Message is empty"]);
exit;
}
$user_message = trim($data["message"]); // Sanitize input
// API request to FastAPI server
$api_url = "http://127.0.0.1:8000/v1/chat/completions";
$payload = json_encode([
"messages" => [
["role" => "user", "content" => $user_message]
]
]);
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $api_url);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_HTTPHEADER, ["Content-Type: application/json"]);
curl_setopt($ch, CURLOPT_POSTFIELDS, $payload);
curl_setopt($ch, CURLOPT_TIMEOUT, 10);
$response = curl_exec($ch);
$http_status = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
// Handle errors
if ($http_status !== 200) {
echo json_encode(["error" => "API error: HTTP $http_status"]);
exit;
}
// Return API response
echo $response;
exit;
}
// If not a POST request, show the chatbot UI
?>
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Standalone PHP Chatbot</title>
<style>
#chat-container {
width: 100%;
max-width: 350px;
padding: 10px;
border: 1px solid #ccc;
background-color: #fff;
}
#chat-input {
width: calc(100% - 60px);
padding: 5px;
margin-right: 5px;
}
button {
padding: 6px 10px;
cursor: pointer;
}
#chat-output {
margin-top: 10px;
padding: 5px;
background-color: #f9f9f9;
max-height: 200px;
overflow-y: auto; /* Enables scrolling */
border: 1px solid #ddd;
}
</style>
</head>
<body>
<div id="chat-container">
<input type="text" id="chat-input" placeholder="Ask me something...">
<button onclick="sendMessage()">Send</button>
<div id="chat-output"></div>
</div>
<script>
async function sendMessage() {
let userMessage = document.getElementById("chat-input").value.trim();
let chatOutput = document.getElementById("chat-output");
if (!userMessage || userMessage.length > 500) {
chatOutput.innerHTML = "<i>Invalid input. Please enter 1-500 characters.</i>";
return;
}
chatOutput.innerHTML = "<i>Loading...</i>"; // Show loading text
try {
let response = await fetch("chatbot.php", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ message: userMessage })
});
let data = await response.json();
if (data.error) {
throw new Error(data.error);
}
let formattedResponse = data.choices[0].message.content.replace(
/(https?:\/\/[^\s]+)/g,
'<a href="$1" target="_blank">$1</a>'
);
chatOutput.innerHTML = `<p>${formattedResponse}</p>`;
} catch (error) {
console.error("Error:", error);
chatOutput.innerHTML = `<i>Error: ${error.message}</i>`;
} finally {
document.getElementById("chat-input").value = "";
}
}
</script>
</body>
</html>
Save and exit the file.
Testing the chatbot.php file
Use a web browser to visit the address of the chatbot (modify values to match your environment):
https://yourdomain.com/blog/chatbot.php
Suggested query: “Tell me about LLMs.”
![](https://blog.gordonbuchan.com/blog/wp-content/uploads/2025/02/image-3.png)
Creating the WordPress plugin directory
Enter the following commands:
cd /var/www/html/yourdomain.com/blog/wp-content/plugins
mkdir rag-chatbot
cd rag-chatbot
Creating the rag-chatbot.php file
Enter the following command:
nano rag-chatbot.php
Use the nano editor to add the following text:
<?php
/**
* Plugin Name: RAG Chatbot
* Description: A WordPress chatbot powered by chatbot.php.
* Version: 1.3
* Author: Gordon Buchan
*/
// MIT license Gordon Buchan 2025
// see https://opensource.org/license/mit
// some of the code was generated with the assistance of AI tools.
function rag_chatbot_enqueue_scripts() {
// ✅ Load scripts from the plugin directory
wp_enqueue_script('rag-chatbot-js', plugin_dir_url(__FILE__) . 'rag-chatbot.js', array(), '1.3', true);
wp_enqueue_style('rag-chatbot-css', plugin_dir_url(__FILE__) . 'rag-chatbot.css', array(), '1.3');
}
add_action('wp_enqueue_scripts', 'rag_chatbot_enqueue_scripts');
function rag_chatbot_shortcode() {
ob_start(); ?>
<div id="chat-container">
<input type="text" id="chat-input" placeholder="Ask me something...">
<button onclick="sendMessage()">Send</button>
<div id="chat-output"></div>
</div>
<?php
return ob_get_clean();
}
add_shortcode('rag_chatbot', 'rag_chatbot_shortcode');
?>
Save and exit the file.
Creating the rag-chatbot.js file
Enter the following command:
nano rag-chatbot.js
Use the nano editor to add the following text:
async function sendMessage() {
let userMessage = document.getElementById("chat-input").value.trim();
let chatOutput = document.getElementById("chat-output");
console.log("User input:", userMessage); // Debugging log
if (!userMessage || userMessage.length > 500) {
chatOutput.innerHTML = "<i>Invalid input. Please enter 1-500 characters.</i>";
return;
}
chatOutput.innerHTML = "<i>Loading...</i>";
try {
let response = await fetch("/blog/chatbot.php", { // ✅ Calls chatbot.php in /blog/
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ message: userMessage })
});
console.log("Response received:", response);
let data = await response.json();
if (data.error) {
throw new Error(data.error);
}
chatOutput.innerHTML = `<p>${data.choices[0].message.content.replace(
/(https?:\/\/[^\s]+)/g,
'<a href="$1" target="_blank">$1</a>'
)}</p>`;
} catch (error) {
console.error("Error:", error);
chatOutput.innerHTML = `<i>Error: ${error.message}</i>`;
} finally {
document.getElementById("chat-input").value = "";
}
}
Save and exit the file.
Creating the rag-chatbot.css file
Enter the following command:
nano rag-chatbot.css
Use the nano editor to add the following text:
/* Chatbot container */
#chat-container {
width: 100%;
max-width: 350px; /* ✅ Works in sidebars and posts */
padding: 10px;
border: 1px solid #ccc;
background-color: #fff;
border-radius: 5px;
box-shadow: 2px 2px 10px rgba(0, 0, 0, 0.1);
font-family: Arial, sans-serif;
display: flex;
flex-direction: column;
gap: 10px;
}
/* Input field */
#chat-input {
width: calc(100% - 70px);
padding: 8px;
border: 1px solid #aaa;
border-radius: 3px;
font-size: 14px;
}
/* Send button */
button {
padding: 8px 12px;
cursor: pointer;
background-color: #0073aa; /* ✅ Matches WordPress admin blue */
color: #fff;
border: none;
border-radius: 3px;
font-size: 14px;
font-weight: bold;
}
button:hover {
background-color: #005d8c;
}
/* Chat output area */
#chat-output {
margin-top: 10px;
padding: 8px;
background-color: #f9f9f9;
max-height: 250px; /* ✅ Scrollable output */
overflow-y: auto;
border: 1px solid #ddd;
border-radius: 3px;
font-size: 14px;
}
/* Ensures long responses don’t overflow */
#chat-output p {
margin: 0;
padding: 5px;
word-wrap: break-word;
}
/* Links inside chatbot responses */
#chat-output a {
color: #0073aa;
text-decoration: underline;
}
#chat-output a:hover {
color: #005d8c;
}
/* Mobile responsiveness */
@media (max-width: 500px) {
#chat-container {
max-width: 100%;
}
#chat-input {
width: 100%;
}
}
Save and exit the file.
Activating the WordPress plugin
Enter the WordPress admin console.
Go to the section “Plugins”
Click on “Activate” for “RAG Chatbot”
Adding the shortcode widget to add the chatbot to the WordPress sidebar
Go to the section “Appearance” | “Widgets.”
Select the sidebar area. Click on the “+” symbol. Search for “shortcode,” click on the short code icon.
![](https://blog.gordonbuchan.com/blog/wp-content/uploads/2025/02/image-1.png)
In the text box marked “Write shortcode here…”
Enter the shortcode:
[rag_chatbot]
![](https://blog.gordonbuchan.com/blog/wp-content/uploads/2025/02/image-5.png)
Click on Update.
Checking that the chatbot has been added to the sidebar of the blog
Go to the main page of the blog. Look to ensure that the short code element has been added to the blog’s sidebar. Test the chatbot (suggested query: “Tell me about LLMs.”):
![](https://blog.gordonbuchan.com/blog/wp-content/uploads/2025/02/image-4.png)