跪拜 Guibai
← Back to the summary

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

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 EPERM file 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):

2.2 Similarity Metric: COSINE Similarity

Text vectorization scenarios uniformly use cosine similarity:

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:

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:

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:

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:

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:

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:

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:

  1. 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.

  2. 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.
  3. 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.

  4. 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

  1. Error on Duplicate Collection Creation: Before each run, use hasCollection to check if it already exists to avoid re-executing DDL.
  2. No Search Results / Error: loadCollection must be executed to load the collection into memory; otherwise, queries are impossible.
  3. Vector Dimension Mismatch: The dim during table creation must strictly match the output dimension of the Embedding.
  4. Cannot Retrieve After Insertion: Newly inserted data needs to be persisted to disk. You can manually execute flush to force persistence.
  5. Windows Dependency Installation EPERM: Move the project out of the OneDrive sync directory, or pause cloud drive syncing.
  6. Search Parameter Error: The SDK standard parameter is data (a 2D array), not vector. Pay attention to the format.
  7. 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.