A Node.js Diary RAG System with Milvus, from Schema to Q&A
Building a Milvus Vector Knowledge Base from Scratch: A Complete Node.js Implementation of Diary RAG Retrieval
Vector databases are the core foundation of the RAG (Retrieval-Augmented Generation) architecture. Milvus, as a mainstream open-source vector database, combined with the Zilliz Cloud managed service, allows you to quickly build a production-grade vector retrieval system. This article will implement the complete workflow of vector storage, retrieval, and intelligent Q&A for a diary knowledge base based on the Node.js ecosystem, fully covering environment setup, collection design, indexing principles, text vectorization, batch insertion, similarity search, and the closed loop of large model Q&A.
1. Technology Selection and Prerequisites
1.1 Core Technology Stack
- Vector Database: Milvus (Zilliz Cloud managed), eliminating local deployment and maintenance costs
- Development Language: Node.js + ES Module
- Vectorization Model: OpenAI-compatible Embedding interface (supports replacement with Tongyi Qianwen, local models, etc.)
- Large Language Model: ChatOpenAI-compatible interface, responsible for generating natural language answers based on retrieved content
- Development Framework: LangChain.js, which uniformly encapsulates vectorization and large model invocation capabilities
- Environment Management: dotenv for managing keys and configuration
1.2 Dependency Installation
The core project dependencies are the official Milvus SDK, vectorization tools, and the large model SDK:
bash
pnpm add @zilliz/milvus2-sdk-node dotenv @langchain/openai
Pitfall Alert: On Windows, if the project is located in a OneDrive sync directory, a
PNPM EPERMfile locking error will occur. It is recommended to move the project to a purely local path or pause OneDrive sync before executing the installation.
1.3 Environment Variable Configuration
Create a .env file in the project root directory and fill in the Zilliz cluster, vectorization, and large model interface configurations:
env
# Zilliz Cloud Connection Info
MILVUS_ADDRESS=https://xxx.ali-cn-hangzhou.vectordb.zillizcloud.com:19530
MILVUS_TOKEN=Your_API_Key
# Embedding Vectorization Configuration
OPENAI_API_KEY=sk-xxx
OPENAI_BASE_URL=https://dashscope.aliyuncs.com/compatible-mode/v1
EMBEDDINGS_MODEL_NAME=text-embedding-v3
# Large Model Q&A Configuration
MODEL_NAME=qwen-plus
2. Milvus Core Concepts Explained
Before writing code, clarify the core concepts of a vector database. The corresponding relationships are as follows:
Table
| MySQL Concept | Milvus Concept | Description |
|---|---|---|
| Database | Project | Resource isolation level |
| Table | Collection | The basic unit for storing vectors and metadata |
| Column | Field | Includes vector fields and scalar fields |
| Index | Vector Index | Accelerates vector similarity search, avoiding full brute-force calculation |
| Row | Entity | A complete record of vector + metadata |
2.1 Vector Index: The Core of Retrieval Speed
Without an index, every query must traverse all vectors in the database to calculate similarity one by one, with a time complexity of O(n). Performance degrades severely as data volume increases.
This project uses IVF_FLAT (Inverted File Index):
- Principle: Pre-clusters all vectors into N buckets. During retrieval, only the few closest buckets are matched, significantly reducing the calculation scope.
- Advantages: Simple parameters, controllable precision, balanced overall performance for data volumes up to millions.
- Applicable Scenarios: Small to medium-scale vector scenarios such as knowledge bases and diary retrieval.
2.2 Similarity Metric: COSINE Similarity
Text vectorization scenarios uniformly use cosine similarity:
- Calculates the angle between vector directions, unaffected by vector length.
- Score range is [-1, 1]. The closer to 1, the higher the semantic similarity.
- It is the industry-standard metric for Embedding text retrieval.
3. Complete Code Implementation: Diary Vector Knowledge Base
3.1 Import Dependencies and Initialize Client
javascript
import "dotenv/config";
import {
MilvusClient,
MetricType,
IndexType,
DataType,
} from "@zilliz/milvus2-sdk-node";
import { OpenAIEmbeddings, ChatOpenAI } from "@langchain/openai";
// Global constant configuration
const ADDRESS = process.env.MILVUS_ADDRESS;
const TOKEN = process.env.MILVUS_TOKEN;
const COLLECTION_NAME = "ai_diary";
const VECTOR_DIM = 1024;
// Initialize vectorization tool
const embeddings = new OpenAIEmbeddings({
apiKey: process.env.OPENAI_API_KEY,
model: process.env.EMBEDDINGS_MODEL_NAME,
configuration: {
baseURL: process.env.OPENAI_BASE_URL,
},
dimensions: VECTOR_DIM,
});
// Initialize large language model
const model = new ChatOpenAI({
temperature: 0.1,
model: process.env.MODEL_NAME,
apiKey: process.env.OPENAI_API_KEY,
configuration: {
baseURL: process.env.OPENAI_BASE_URL,
},
});
// Initialize Milvus client
const client = new MilvusClient({
address: ADDRESS,
token: TOKEN,
ssl: true, // Zilliz Cloud enforces SSL
});
// Encapsulate text-to-vector function
const getEmbedding = async (text) => {
const result = await embeddings.embedQuery(text);
return result;
};
Code Analysis:
MilvusClientis the core entry point of the SDK, responsible for all database operations.ssl: trueis mandatory for the cloud; it can be omitted for local deployment.temperature: 0.1controls the stability of the large model's output. A lower temperature is recommended for Q&A scenarios to reduce hallucinations.getEmbeddinguniformly encapsulates the vectorization logic. To replace the model later, only this function needs modification.
3.2 Collection Schema Design
The diary scenario requires storing vectors + business metadata. The field design is as follows:
javascript
async function createCollection() {
await client.createCollection({
collection_name: COLLECTION_NAME,
fields: [
{
name: "id",
data_type: DataType.VarChar,
max_length: 50,
is_primary_key: true,
},
{
name: "vector",
data_type: DataType.FloatVector,
dim: VECTOR_DIM,
},
{
name: "content",
data_type: DataType.VarChar,
max_length: 5000,
},
{
name: "date",
data_type: DataType.VarChar,
max_length: 50,
},
{
name: "mood",
data_type: DataType.VarChar,
max_length: 50,
},
{
name: "tags",
data_type: DataType.Array,
element_type: DataType.VarChar,
max_capacity: 10,
max_length: 50,
},
],
});
console.log("Collection created successfully");
}
Design Points:
- The primary key uses a custom string ID, making it easy for the business side to associate with the original diary.
- The dimension of the
vectorfield must strictly match the output dimension of the Embedding model; otherwise, an insertion error will occur. tagsuses the array type, supporting multi-tag storage and filtering.- Compared to MySQL, Milvus supports dynamic fields. Undefined fields can also be inserted, offering higher flexibility.
3.3 Create Vector Index and Load Collection
javascript
async function createIndexAndLoad() {
// Create vector index
await client.createIndex({
collection_name: COLLECTION_NAME,
field_name: "vector",
index_type: IndexType.IVF_FLAT,
metric_type: MetricType.COSINE,
params: { nlist: 128 }, // Number of cluster buckets
});
console.log("Index created successfully");
// Load collection into memory (must be executed before retrieval)
await client.loadCollection({
collection_name: COLLECTION_NAME,
});
console.log("Collection loaded");
}
Key Notes:
nlistis the core parameter of IVF_FLAT. For millions of data points, 128 is recommended; it can be increased as data volume grows.loadCollectionis a necessary step in Milvus. An unloaded collection cannot perform retrieval.
3.4 Batch Vectorization and Data Insertion
javascript
async function insertDiaries() {
// Raw diary data
const diaryContents = [
{
id: "diary_001",
content: "The weather was great today. I went for a walk in the park and felt happy. I saw many flowers blooming; spring is truly beautiful.",
date: "2026-01-10",
mood: "happy",
tags: ["life", "walk"],
},
{
id: "diary_002",
content: "Work was very busy today. I completed an important project milestone. The teamwork was pleasant, and I felt a great sense of accomplishment.",
date: "2026-01-11",
mood: "excited",
tags: ["work", "achievement"],
},
{
id: "diary_003",
content: "Went hiking with friends over the weekend. The weather was great, and I felt very relaxed. Enjoying nature feels so good.",
date: "2026-01-12",
mood: "relaxed",
tags: ["outdoors", "friends"],
},
{
id: "diary_004",
content: "Studied the Milvus vector database today and found it very interesting. Vector search technology is truly powerful.",
date: "2026-01-12",
mood: "curious",
tags: ["study", "technology"],
},
{
id: "diary_005",
content: "Made a hearty dinner tonight and tried a new recipe. My family said it was delicious, and I felt very accomplished.",
date: "2026-01-13",
mood: "proud",
tags: ["food", "family"],
},
];
console.log("Generating vectors in batch...");
// Concurrently call the vectorization interface to improve processing speed
const diaryData = await Promise.all(
diaryContents.map(async (diary) => ({
...diary,
vector: await getEmbedding(diary.content),
}))
);
// Batch insert into Milvus
const insertResult = await client.insert({
collection_name: COLLECTION_NAME,
data: diaryData,
});
console.log(`${insertResult.insert_cnt} records inserted successfully`);
}
Performance Optimization Points:
- Using
Promise.allto generate vectors concurrently speeds up the process several times compared to serialawait. - Batch insertion is more performant than single-record loop insertion. In production, batch writing in chunks is recommended.
- When the data volume is too large, concurrency must be controlled to avoid triggering the Embedding interface rate limit.
3.5 Vector Similarity Search (Basic Test Version)
A single file for quick verification of retrieval capability, which can be run and debugged independently:
javascript
async function searchTest() {
try {
console.log("Connection to Milvus...");
await client.connectPromise; // Wait for connection handshake to complete
console.log("Connected");
const query = "I want to see diaries about outdoor activities";
console.log(`QUERY: ${query}`);
// 1. Generate vector from query text
const queryVector = await getEmbedding(query);
// 2. Execute vector similarity search
const searchResult = await client.search({
collection_name: COLLECTION_NAME,
data: [queryVector], // Query vector, in 2D array format
limit: 2,
metric_type: MetricType.COSINE,
output_fields: ["id", "content", "date", "mood", "tags"],
});
// 3. Format and print results
console.log(`Found ${searchResult.results[0].length} results`);
searchResult.results[0].forEach((item, index) => {
console.log(`${index + 1}. [Score: ${item.score.toFixed(4)}]`);
console.log(`
ID: ${item.id}
Date: ${item.date}
Mood: ${item.mood}
Tags: ${item.tags?.join(", ")}
Content: ${item.content}
`);
});
} catch (err) {
console.error("Search failed:", err.message);
}
}
Search Parameter Description:
data: Passes in the query vector array in 2D format, supporting batch multi-vector queries.limit: Returns the Top K most similar results.output_fields: Specifies the scalar fields to return. If not specified, only the ID and similarity score are returned.item.score: Cosine similarity score. The closer to 1, the higher the semantic match.connectPromise: Waits for the Milvus client to complete the TCP handshake, avoiding errors from initiating requests before connection.
4. Complete Implementation of a Modular RAG Q&A System
The core idea of RAG is a two-stage architecture of Retrieval + Generation: first, recall relevant knowledge from the vector database, then pass the knowledge as context to the large model, allowing the AI to answer questions based on private data. This project splits retrieval and generation into independent modules, with clear responsibilities for easy maintenance.
4.1 Retrieval Module Encapsulation: retrieveRelevantDiaries
Independently encapsulates the vector retrieval logic, so upper-level business does not need to care about the low-level details of Milvus:
javascript
async function retrieveRelevantDiaries(question, k = 2) {
try {
const queryVector = await getEmbedding(question);
const searchResult = await client.search({
collection_name: COLLECTION_NAME,
data: [queryVector],
limit: k,
metric_type: MetricType.COSINE,
output_fields: ["id", "content", "date", "mood", "tags"],
});
// Return the array of matched diary entities
return searchResult.results[0];
} catch (err) {
console.log("Error retrieving diaries", err.message);
return [];
}
}
Design Advantages:
- Single Responsibility: Only responsible for vector recall, not processing business logic.
- Exception Fallback: Returns an empty array on retrieval failure, allowing upper-level business to degrade gracefully.
- Reusability: This function can be called in multiple scenarios such as Q&A, search, and recommendation.
4.2 Q&A Core: Complete Implementation of answerDiaryQuestion
Integrates the entire process of retrieval, context concatenation, Prompt construction, and large model invocation:
javascript
async function answerDiaryQuestion(question, k = 2) {
try {
// Print a separator line for easy debugging log distinction
console.log("=".repeat(80));
console.log(`Question: ${question}`);
console.log("=".repeat(80));
// Stage 1: Vector retrieval to recall relevant diaries
console.log("Retrieving relevant diaries");
const retrievedDiaries = await retrieveRelevantDiaries(question, k);
if (retrievedDiaries.length === 0) {
console.log("No relevant diaries found");
return;
}
// Print retrieval results and similarity for debugging recall effectiveness
retrievedDiaries.forEach((diary, i) => {
console.log(`Diary ${i + 1} Similarity: ${diary.score.toFixed(4)}
Content: ${diary.content}
`);
});
// Stage 2: Concatenate formatted context
const context = retrievedDiaries
.map((diary, i) => `
[Diary ${i + 1}]
Date: ${diary.date}
Mood: ${diary.mood}
Tags: ${diary.tags?.join(", ")}
Content: ${diary.content}
`)
.join("\n\n----\n\n");
// Stage 3: Build system prompt + context + user question
const prompt = `You are a warm and caring AI diary assistant. Answer questions based on the user's diary content,
using a friendly and natural tone. Please answer the following question based on the diary content:
${context}
User Question: ${question}
Answer Requirements:
1. If there is relevant information in the diaries, provide a detailed and warm answer based on the diary content.
2. You can summarize the content of multiple diaries to find common points or trends.
3. If there is no relevant information in the diaries, gently inform the user.
4. Use the first-person "you" to address the diary author.
5. The answer should be empathetic, making the user feel understood and cared for.
AI Assistant's Answer:
`;
// Stage 4: Invoke the large model to generate an answer
console.log("[AI Answer]");
const response = await model.invoke(prompt);
console.log(response.content);
} catch (err) {
console.log(err.message);
}
}
Core Design Analysis:
Modular Splitting: Strictly follows the two-stage RAG architecture. The retrieval logic is independently encapsulated, and the Q&A function is only responsible for process orchestration.
Prompt Engineering:
- Sets a character persona: a warm and caring diary assistant, unifying the answering style.
- Defines clear answering rules: 5 constraints to reduce hallucinations, forcing answers to be based on diary content.
- Person Specification: Uses "you" to address the author, enhancing conversational immersion.
Debug-Friendly: Prints similarity scores and recalled original text, making it easy to troubleshoot whether an "inaccurate answer" is a recall problem or a generation problem.
Exception Fallback: Provides a friendly prompt when retrieval is empty, preventing the large model from throwing an error due to empty context.
4.3 Main Function Entry and Process Orchestration
javascript
async function main() {
try {
console.log("Connecting to Milvus...");
await client.connectPromise; // Wait for connection to be ready
console.log("Connected");
// Execute diary intelligent Q&A
await answerDiaryQuestion("What have I done recently that made me feel happy?", 2);
// Close connection
await client.close();
} catch (err) {
console.error("Program execution exception:", err);
}
}
main().catch(console.error);
5. Common Pitfalls and Solutions
- Error on Duplicate Collection Creation: Before each run, use
hasCollectionto check if it already exists to avoid re-executing DDL. - No Search Results / Error:
loadCollectionmust be executed to load the collection into memory; otherwise, queries are impossible. - Vector Dimension Mismatch: The
dimduring table creation must strictly match the output dimension of the Embedding. - Cannot Retrieve After Insertion: Newly inserted data needs to be persisted to disk. You can manually execute
flushto force persistence. - Windows Dependency Installation EPERM: Move the project out of the OneDrive sync directory, or pause cloud drive syncing.
- Search Parameter Error: The SDK standard parameter is
data(a 2D array), notvector. Pay attention to the format. - Large Model Answer Hallucination: Lowering the
temperature, strongly constraining the Prompt to "answer only based on diary content," and increasing the number of recalled entries can effectively mitigate this.
6. Summary
This article fully implemented a diary vector knowledge base and RAG intelligent Q&A system based on Milvus, covering the entire chain from environment setup, Schema design, and indexing principles to batch insertion, similarity search, and large model Q&A. Through modular splitting, the retrieval layer and generation layer are completely decoupled. Subsequent replacement of the vector model, large model, or vector database will not require major changes to the business code.
Vector database + RAG is currently the most mature solution for implementing private knowledge bases. After mastering this foundational architecture, it can be quickly expanded to more scenarios such as document Q&A, customer service robots, semantic search, and personal memory assistants.