Cấu hình kết nối Database và biến môi trường
Thiết lập biến môi trường cho PostgreSQL
Chúng ta cần tách biệt thông tin nhạy cảm như mật khẩu ra khỏi code source để đảm bảo bảo mật. Việc này được thực hiện bằng file .env trong thư mục dự án backend.
Đầu tiên, tạo file .env tại thư mục gốc của dự án backend (ví dụ: /var/www/ai-backend). Nội dung này chứa thông tin kết nối đến PostgreSQL đã cài đặt trong Phần 1.
DB_HOST=localhost
DB_PORT=5432
DB_NAME=ai_db
DB_USER=ai_user
DB_PASSWORD=your_secure_password_here
DB_SSLMODE=disable
Sau khi lưu file, hãy kiểm tra quyền truy cập file này. Chỉ owner mới được đọc để tránh rò rỉ thông tin.
chmod 600 .env
Kết quả mong đợi: File .env được tạo với quyền -rw------- (600), đảm bảo không người dùng nào khác trên server đọc được mật khẩu.
Khởi tạo Connection Pool trong ứng dụng
Chúng ta sử dụng thư viện pg (node-postgres) để quản lý connection pool. Pool giúp tái sử dụng kết nối, giảm overhead khi mở/đóng connection liên tục cho mỗi request API.
Tạo file cấu hình config/database.js để đóng gói logic kết nối.
require('dotenv').config();
const { Pool } = require('pg');
const pool = new Pool({
host: process.env.DB_HOST,
port: process.env.DB_PORT,
database: process.env.DB_NAME,
user: process.env.DB_USER,
password: process.env.DB_PASSWORD,
ssl: {
rejectUnauthorized: false
},
max: 20, // Số lượng connection tối đa trong pool
idleTimeoutMillis: 30000, // Thời gian chờ connection rảnh
connectionTimeoutMillis: 2000,
});
// Kiểm tra kết nối khi khởi động
pool.on('error', (err) => {
console.error('Unexpected error on idle client', err);
process.exit(-1);
});
module.exports = pool;
Chạy lệnh test kết nối ngay sau khi tạo file để đảm bảo biến môi trường và thông tin DB chính xác.
node -e "const pool = require('./config/database'); pool.query('SELECT NOW()').then(() => { console.log('Connected to AI DB'); process.exit(0); }).catch(e => { console.error(e); process.exit(1); })"
Kết quả mong đợi: Xuất hiện dòng Connected to AI DB trên terminal. Nếu lỗi, kiểm tra lại file .env và trạng thái service PostgreSQL.
Triển khai API Endpoint Tìm kiếm tương tự
Cấu trúc thư viện Embedding (Vectorize)
Trước khi query DB, ứng dụng backend cần chuyển đổi văn bản đầu vào (query text) thành vector số (embedding). Chúng ta sử dụng thư viện transformers của Hugging Face (nền tảng phổ biến nhất cho AI) hoặc all-MiniLM-L6-v2 để chạy inference trực tiếp trên server.
Tạo file services/vectorize.js để đóng gói logic tạo vector.
const { pipeline } = require('@xenova/transformers'); // Sử dụng transformers.js cho Node.js
let embedder = null;
async function initEmbedder() {
if (!embedder) {
console.log('Loading embedding model...');
embedder = await pipeline('feature-extraction', 'Xenova/all-MiniLM-L6-v2');
console.log('Embedding model loaded successfully.');
}
return embedder;
}
async function textToVector(text) {
const model = await initEmbedder();
const result = await model(text, { pooling: 'mean', normalize: true });
// Convert tensor to plain array for SQL
return Array.from(result.data);
}
module.exports = { textToVector, initEmbedder };
Chạy lệnh cài đặt dependency cần thiết trước khi chạy code này.
npm install @xenova/transformers
Kết quả mong đợi: Khi khởi động server lần đầu, console sẽ log Loading embedding model... rồi Embedding model loaded successfully.. File model sẽ được cache trong thư mục ~/.cache/transformers.
Triển khai Endpoint API /search
Chúng ta tạo endpoint nhận JSON chứa query, limit và page. Logic sẽ: 1. Vectorize query, 2. Thực thi SQL với toán tử khoảng cách Cosine (<->), 3. Phân trang kết quả.
Tạo file routes/search.js sử dụng Express.
const express = require('express');
const router = express.Router();
const pool = require('../config/database');
const { textToVector } = require('../services/vectorize');
router.post('/search', async (req, res) => {
try {
const { query, limit = 10, page = 1 } = req.body;
if (!query) {
return res.status(400).json({ error: 'Query text is required' });
}
// 1. Chuyển đổi văn bản thành vector
const queryVector = await textToVector(query);
// 2. Chuẩn bị tham số cho SQL
const offset = (page - 1) * limit;
const vectorParam = `{${queryVector.join(',')}}`;
// 3. Thực thi truy vấn tìm kiếm tương tự (Cosine Similarity)
// Giả sử bảng 'documents' có cột 'embedding' loại vector(384)
const sql = `
SELECT
id,
title,
content,
1 - (embedding $1::vector) as similarity_score
FROM documents
ORDER BY embedding $1::vector
LIMIT $2
OFFSET $3
`;
const result = await pool.query(sql, [vectorParam, limit, offset]);
// 4. Trả về kết quả kèm metadata phân trang
res.json({
results: result.rows,
pagination: {
page: page,
limit: limit,
total_found: result.rows.length // Trong thực tế nên có query đếm riêng
},
query_vector_length: queryVector.length
});
} catch (error) {
console.error('Search error:', error);
res.status(500).json({ error: 'Internal server error during search' });
}
});
module.exports = router;
Tích hợp route này vào file index.js chính của ứng dụng Express.
const express = require('express');
const app = express();
const searchRouter = require('./routes/search');
app.use(express.json());
app.use('/api', searchRouter);
const PORT = process.env.PORT || 3000;
app.listen(PORT, () => {
console.log(`Backend AI server running on port ${PORT}`);
});
Kết quả mong đợi: Server chạy trên cổng 3000. Khi gửi POST request có nội dung văn bản, API trả về JSON chứa danh sách tài liệu có điểm tương đồng cao nhất.
Xử lý Logic Phân trang và Tối ưu hóa Truy vấn
Hiệu chỉnh toán tử khoảng cách cho phân trang
Phân trang trong vector search phức tạp hơn SQL thông thường vì thứ tự không cố định. Tuy nhiên, với OFFSET và LIMIT chuẩn của PostgreSQL, kết quả vẫn ổn định nếu query giống nhau. Lưu ý: OFFSET lớn sẽ làm giảm hiệu năng, nên giới hạn limit và page tối đa.
Thêm validation để giới hạn số lượng page và limit, tránh lỗi Memory Exhaustion hoặc Slow Query.
const MAX_LIMIT = 100;
const MAX_PAGE = 100;
if (limit > MAX_LIMIT) limit = MAX_LIMIT;
if (page > MAX_PAGE) {
return res.status(400).json({ error: 'Page number exceeds maximum allowed (100)' });
}
Đảm bảo query SQL sử dụng toán tử <-> (L2 distance) hoặc <#> (Negative Inner Product) tùy thuộc vào cách bạn chuẩn hóa vector. Nếu vector đã được normalize (unit vector), <-> tương đương với 1 - CosineSimilarity.
Verify kết quả phân trang
Sử dụng curl để gọi API với các tham số phân trang khác nhau, kiểm tra xem kết quả có trùng lặp hoặc nhảy lộn xộn không.
curl -X POST http://localhost:3000/api/search \
-H "Content-Type: application/json" \
-d '{"query": "machine learning", "limit": 5, "page": 1}'
curl -X POST http://localhost:3000/api/search \
-H "Content-Type: application/json" \
-d '{"query": "machine learning", "limit": 5, "page": 2}'
Kết quả mong đợi: Trang 1 và Trang 2 trả về các ID tài liệu khác nhau, điểm similarity_score giảm dần theo thứ tự. Không có tài liệu nào lặp lại giữa hai trang.
Bảo mật và Giám sát Kết nối
Chặn truy cập trực tiếp từ ngoài
Đảm bảo PostgreSQL chỉ lắng nghe trên 127.0.0.1 (localhost) nếu backend chạy trên cùng một máy. Điều này ngăn chặn hacker quét port 5432 từ internet.
cat /etc/postgresql/16/main/postgresql.conf | grep listen_addresses
Nếu kết quả là * hoặc 0.0.0.0, hãy sửa thành 127.0.0.1 trong file /etc/postgresql/16/main/postgresql.conf và restart service.
sudo systemctl restart postgresql
Kết quả mong đợi: Lệnh netstat -tlnp | grep 5432 chỉ hiện địa chỉ 127.0.0.1:5432.
Giới hạn số lượng connection từ backend
Dùng file pg_hba.conf để chỉ cho phép user ai_user kết nối từ 127.0.0.1 bằng phương thức md5 hoặc scram-sha-256.
sudo nano /etc/postgresql/16/main/pg_hba.conf
Thêm dòng sau vào cuối file:
local ai_db ai_user md5
host ai_db ai_user 127.0.0.1/32 scram-sha-256
Reload cấu hình PostgreSQL để áp dụng thay đổi.
sudo systemctl reload postgresql
Kết quả mong đợi: Nếu cố gắng kết nối từ IP khác hoặc user khác, PostgreSQL sẽ từ chối. Backend chạy trên localhost vẫn hoạt động bình thường.
Kiểm tra Connection Pool đang hoạt động
Chạy query trực tiếp trên PostgreSQL để xem số lượng connection đang được backend giữ.
sudo -u postgres psql -c "SELECT count(*) FROM pg_stat_activity WHERE datname = 'ai_db' AND application_name LIKE 'node%';"
Thực hiện tải thử (load test) nhẹ để xem pool tăng lên.
for i in {1..20}; do curl -s http://localhost:3000/api/search -d '{"query":"test"}' > /dev/null & done; sleep 2; sudo -u postgres psql -c "SELECT count(*) FROM pg_stat_activity WHERE datname = 'ai_db' AND application_name LIKE 'node%';"
Kết quả mong đợi: Số lượng connection tăng nhưng không vượt quá giá trị max: 20 đã thiết lập trong file config/database.js. Điều này chứng tỏ connection pool đang hoạt động hiệu quả.
Điều hướng series:
Mục lục: Series: Triển khai Database AI với PostgreSQL, pgvector và Ubuntu 24.04
« Phần 4: Triển khai truy vấn tìm kiếm tương tự (Similarity Search) bằng SQL
Phần 6: Tối ưu hóa hiệu năng, giám sát và xử lý sự cố »