跪拜 Guibai
← Back to the summary

A 200-Line AI Diary That Actually Understands What 'Proud' Means

Building an AI Diary Assistant from Scratch: Using Milvus Vector Database + RAG to Let Machines Understand Your Every Day


Preface

If you are building an AI Agent, you will almost certainly need a piece of infrastructure: a vector database. It is not a replacement for MySQL, but rather fills the gap that traditional databases are worst at: semantic understanding.

This article uses a complete hands-on project — an AI Diary Assistant — as the main thread to guide you through building a RAG (Retrieval-Augmented Generation) system based on the Milvus vector database from scratch. We will write four JS files, step by step completing: connecting to the vector database → creating a collection → vectorizing and storing diaries → semantic search → generating answers with a large language model.

The project uses Zilliz Cloud (a fully managed service based on Milvus) + Alibaba Cloud DashScope (compatible with the OpenAI interface) as the Embedding and LLM backend.


1. Why Use a Vector Database?

1.1 The Boundaries of Traditional Databases

In everyday web development, we store data in MySQL, PostgreSQL, or SQLite, performing basic CRUD operations — exact lookups by ID, fuzzy matching with the LIKE keyword, and querying lists with joins. This model is very mature for handling structured data.

But it has a fundamental limitation: it doesn't understand semantics.

Consider a concrete example. Your diary contains this entry:

"Went hiking with friends over the weekend. The weather was great, and I felt very relaxed. It's so nice to enjoy nature."

If you want to search for "diaries about outdoor activities," what can SQL do for you? Use LIKE '%outdoor%' to match? The word "outdoor" doesn't appear in this diary at all; SQL will return an empty result.

This is the ceiling of keyword matching — it can only match literal strings, not understand meaning.

1.2 What a Vector Database Solves

The core idea of a vector database is completely different:

Text → Embedding Model → High-dimensional vector (a string of floats) → Store in vector database

During query:
Query text → Embedding Model → Query vector → Similarity calculation in the database → Return the most similar Top-K results

"Hiking" and "outdoor activities" are very close in vector space — although they are completely different literally, they are highly related semantically. This is the core capability of a vector database: semantic retrieval.

flowchart LR
    A["📝 Original Text<br/>Diary Content"] --> B["🧠 Embedding Model<br/>Text → Vector"]
    B --> C["🗄️ Milvus Vector DB<br/>Storage + Index"]
    
    D["❓ User Query<br/>'Outdoor Activities'"] --> E["🧠 Same Embedding Model"]
    E --> F["🔍 Similarity Search<br/>COSINE / IP / L2"]
    F --> G["📋 Return Top-K Results"]

1.3 Implementation in Code

In the project, we encapsulate a getEmbedding function that makes a single API call to convert any text into a string of 1024-dimensional float vectors:

// This function exists in index.mjs / query.mjs / rag.mjs
const getEmbedding = async (text) => {
  const result = await embeddings.embedQuery(text);
  return result;  // Returns number[], length = VECTOR_DIM (1024)
}

Behind this single line of code is a network request to an Embedding service (we use Alibaba Cloud DashScope's text-embedding-v3 model), mapping natural language text to a point in a high-dimensional space.


2. What is Milvus

2.1 Positioning

The readme has a very concise definition:

Milvus is an open-source vector database designed for handling massive high-dimensional vector data. AI Agent products all use vector stores like Milvus.

Three keywords here are worth expanding on:

Keyword Meaning
Vector Database A database system specifically optimized for storing and retrieving vector data, different from traditional relational databases.
Massive High-Dim A single vector can have hundreds to thousands of dimensions, and data volumes can reach billions.
Vector Store The memory and knowledge storage layer in an Agent architecture; the Agent puts "memories" in Milvus for semantic retrieval.

2.2 C/S Architecture

Milvus adopts a classic Client/Server (C/S) architecture, as noted in the code comments:

// index.mjs
import {
  MilvusClient,  // C/S Architecture     B/S Architecture
  MetricType,    // Similarity calculation method
  IndexType,
  DataType       // Field data type constraints
} from '@zilliz/milvus2-sdk-node';

This is the same pattern as the mysql2 driver for MySQL or ioredis for Redis. MilvusClient is your entry point object for operating the vector database.

2.3 Zilliz Cloud

A fully managed vector database service based on Milvus.

If you don't want to set up a Milvus server yourself, Zilliz Cloud provides an out-of-the-box cloud service (like RDS for MySQL). Our project uses a Zilliz Cloud Serverless instance, with the address configured in .env:

MILVUS_ADDRESS=https://in03-5d01b4cb0f9332d.serverless.ali-cn-hangzhou.cloud.zilliz.com.cn
MILVUS_TOKEN=your-auth-token

3. Environment Setup and Connection

3.1 Dependencies

The project uses three core dependencies:

Package Purpose
@zilliz/milvus2-sdk-node Milvus Node.js SDK (v3.0.3), used to operate the vector database.
@langchain/openai LangChain's OpenAI-compatible wrapper, providing OpenAIEmbeddings and ChatOpenAI.
dotenv Loads .env environment variables.

3.2 Embedding Model Initialization

// index.mjs / query.mjs / rag.mjs
const embeddings = new OpenAIEmbeddings({
  apiKey: process.env.OPENAI_API_KEY,         // API Key
  model: process.env.EMBEDDINGS_MODEL_NAME,    // Model name: text-embedding-v3
  configuration: {
    baseURL: process.env.OPENAI_BASE_URL       // Custom API endpoint
  },
  dimensions: VECTOR_DIM                        // Output vector dimension: 1024
});

configuration.baseURL is a configuration point worth noting. The official OpenAI API address is https://api.openai.com/v1, but when you use domestic large model services (like DeepSeek, Tongyi Qianwen, Zhipu, etc.), their interfaces are OpenAI-compatible — same protocol, different address. You just need to change baseURL to the corresponding address to switch seamlessly without modifying any business code. This is an abstraction at the protocol level: OpenAI defined the de facto standard for HTTP APIs, and various vendors reduce developer migration costs by implementing the same interface.

Here we use Alibaba Cloud DashScope:

OPENAI_BASE_URL=https://dashscope.aliyuncs.com/compatible-mode/v1
EMBEDDINGS_MODEL_NAME=text-embedding-v3

3.3 MilvusClient Initialization and Connection

// index.mjs / query.mjs / rag.mjs
const client = new MilvusClient({
  address: ADDRESS,   // Milvus service address
  token: TOKEN        // Auth token (required for Zilliz Cloud)
})

Our code comments note // SDK v3 auto-connects, no need for connectPromise(). This is because Milvus Node.js SDK v3 automatically calls the connect() method in the constructor to establish a gRPC connection. All subsequent API calls (checkHealth, createCollection, insert, search, etc.) will internally await this.connectPromise to ensure the connection is ready before executing.

// main.mjs / index.mjs
const checkHealth = await client.checkHealth();
if (!checkHealth.isHealthy) {
  console.error('Connection failed', checkHealth.reasons);
  return;
}
console.log('Connection successful, cluster status normal');

checkHealth() not only verifies network connectivity but also checks the running status of various Milvus cluster components. In a production environment, this is a good startup probe practice.


4. Indexing: How Vector Search Achieves Millisecond Speed

4.1 The Problem: The O(n) Dilemma of Brute-Force Search

main.mjs has a long comment that uses an excellent analogy to explain the necessity of indexes:

// Milvus stores high-dimensional vectors.
// Without an index, each query must calculate similarity between the query vector and every vector in the database one by one (O(n)).
// When data volume is large, it's too slow to use.
// Dictionary: pinyin, radical indexes quickly narrow the search scope.
// Library: finding a book "The Three-Body Problem" without an index means flipping through every book.
// Literature Hall / Fiction / Science Fiction
// IVF_FLAT cluster index, millisecond level
IndexType,

This comment is information-dense; let's break it down layer by layer:

Layer 1: Why indexes are needed. Suppose you have stored 10 million vectors. Each query would need to calculate similarity against all 10 million vectors one by one — time complexity O(n), which is unacceptable in a production environment. The purpose of an index is to avoid a full scan.

Layer 2: The dictionary analogy. When you look up a character in the Xinhua Dictionary, you don't flip from the first page to the last. You first locate the initial consonant area using the pinyin index, then the vowel area, and finally pinpoint the target page — three jumps narrow the scope from tens of thousands to 1. The underlying idea of vector indexes is exactly the same: through spatial partitioning, narrow the search scope from the entire database to a few regions.

Layer 3: The library analogy. A library without an index → you have to flip through every book to find "The Three-Body Problem." A library with an index → Literature Hall / Fiction / Science Fiction → located in three seconds. This is what IVF_FLAT does — divides the vector space into multiple clusters, finds the nearest few clusters during a query, and then only performs exact comparisons within those clusters.

4.2 Index Creation in Code

// index.mjs
await client.createIndex({
  collection_name: COLLECTION_NAME,
  field_name: 'vector',             // Create index on the vector field
  index_type: IndexType.IVF_FLAT,   // Inverted File index
  metric_type: MetricType.COSINE    // Cosine similarity
})

IVF_FLAT (Inverted File with Flat compression) is one of the most commonly used vector index types:

IVF_FLAT Working Principle:

Training Phase: Use K-Means to cluster all vectors into N clusters
  [Cluster 1] [Cluster 2] ... [Cluster N]

Query Phase:
  1. Calculate distance from query vector to N cluster centers → select the nearest M clusters
  2. Perform exact comparison only within these M clusters
  3. Return Top-K

Complexity: O(N + M), where M << total number of vectors

4.3 Similarity Algorithms

The code uses MetricType.COSINE (Cosine Similarity), and the comments also note // Similarity calculation method:

MetricType, // Similarity calculation method

Three common vector similarity calculation methods:

Metric Formula Intuition Use Case
COSINE Looks at the "angle" between two vectors; smaller angle means more similar. Text semantic similarity (recommended)
IP (Inner Product) Looks at the "projection length" of two vectors; larger value means more similar. Scenarios where vectors are already normalized
L2 (Euclidean Distance) Looks at the "straight-line distance" between two points in space; closer means more similar. Image feature matching

For text embeddings, cosine similarity is the most common choice because it only cares about direction, not the absolute length of the vectors — the vector directions for "hiking" and "mountaineering" should be very close, even if their respective vector lengths differ.


5. Collection: The "Table" in a Vector Database

5.1 Schema Design

In a traditional relational database, you must first CREATE TABLE to define the type and constraints of each column before inserting data. A Collection in Milvus is analogous to a MySQL Table. In index.mjs, we define the field structure of the diary Collection using an explicit Schema:

const COLLECTION_NAME = 'ai_diary';
const VECTOR_DIM = 1024;  // Vector dimension, determined by the Embedding model's output
await client.createCollection({
  collection_name: COLLECTION_NAME,
  fields: [
    // diary_01
    {
      name: 'id',
      data_type: DataType.VarChar,   // String type
      max_length: 50,                 // Max length limit
      is_primary_key: true,           // Set as primary key
      description: 'Unique diary identifier'
    },
    {
      name: 'vector',
      data_type: DataType.FloatVector, // Float vector type
      dim: VECTOR_DIM,                  // Vector dimension 1024
      description: 'Vector representation of diary content'
    },
    {
      name: 'content',
      data_type: DataType.VarChar,
      max_length: 5000,                // Diary text, allows longer strings
      description: 'Diary body content'
    },
    {
      name: 'date',
      data_type: DataType.VarChar,
      max_length: 50,
      description: 'Diary date'
    },
    {
      name: 'mood',
      data_type: DataType.VarChar,
      max_length: 50,
      description: 'Mood tag'
    },
    {
      name: 'tags',
      data_type: DataType.Array,        // Array type!
      element_type: DataType.VarChar,   // Type of each element in the array
      max_capacity: 10,                 // Max array capacity
      max_length: 50,                   // Max length of each element
      description: 'Diary tag list'
    },
  ]
});

5.2 Field Type Breakdown

Notice the DataType import corresponding to the // Field data type constraints comment. The Schema defines 6 fields, each carrying different responsibilities:

Field Type Role MySQL Analogy
id VarChar(50), PK Unique diary ID, e.g., diary_001 VARCHAR(50) PRIMARY KEY
vector FloatVector(1024) Embedding vector of the diary content No MySQL equivalent
content VarChar(5000) Diary body content TEXT
date VarChar(50) Diary date VARCHAR(50)
mood VarChar(50) Mood tag VARCHAR(50)
tags Array Tag array MySQL 8+ JSON array

The most interesting design is the tags field. In traditional SQL, a one-to-many tag relationship usually requires a separate association table. In Milvus, DataType.Array allows storing a list of tags directly within a single record (max_capacity: 10 limits it to a maximum of 10 tags), meaning you can get the complete data directly during a query without needing a JOIN. Of course, this also means filtering by tags (like "find all diaries containing the 'work' tag") needs to be done via Scalar Filtering, rather than a traditional SQL WHERE subquery.

5.3 Idempotent Creation

// Check and create Collection
const hasCollection = await client.hasCollection({ collection_name: COLLECTION_NAME });
if (!hasCollection.value) {
  // Create Collection and Index ...
} // end if !hasCollection

In SDK v3, hasCollection returns an object containing a .value property, rather than returning a boolean directly — this is a detail change in the v3 API. Using this check enables idempotent execution: the Collection is created on the first run, and subsequent runs skip creation and go straight to inserting data.

5.4 Loading into Memory

console.log('loading collection');
await client.loadCollection({
  collection_name: COLLECTION_NAME
});
console.log('collection loaded')

After creation, a Collection is in an unloaded state — data is on disk, but searching requires data to be in memory. loadCollection loads the Collection's index and data into memory, after which search operations can be performed. This is a key performance design: you can have hundreds of Collections but only load the ones you need to query, saving memory overhead.


6. Diary Data Vectorization and Insertion

6.1 Sample Data

const diaryContents = [
  {
    id: 'diary_001',
    content: 'The weather was great today. Went for a walk in the park and felt happy. 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. Completed an important project milestone. Team collaboration was pleasant, feeling 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. It\'s so nice to enjoy nature.',
    date: '2026-01-12',
    mood: 'relaxed',
    tags: ['outdoor', '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: 'Cooked a sumptuous dinner tonight and tried a new recipe. My family said it was delicious; feeling a great sense of accomplishment.',
    date: '2026-01-13',
    mood: 'proud',
    tags: ['food', 'family']
  }
];

5 simulated diary entries covering five themes: life, work, outdoor, study, and food, with moods ranging from happy to proud, providing sufficient diversity for the subsequent semantic search.

6.2 Batch Vectorization

console.log('Generating embeddings...');
const diaryData = await Promise.all(
  diaryContents.map(async (diary) => ({
    ...diary,
    vector: await getEmbedding(diary.content)
  }))
);

Promise.all + async map concurrently calls the Embedding API, generating vectors for 5 diaries in parallel, which is several times faster than doing it serially. The generated diaryData now has an extra vector field in each record — a 1024-dimensional number[].

A question to consider: Why must Promise.all be used here instead of forEach?

The callback for forEach executes synchronously; it won't wait for the internal await. If your Embedding interface has a rate limit of 10 QPS, 5 concurrent requests might all be sent within tens of milliseconds, instantly triggering the rate limit. In a real project, you might need to switch to p-limit or another concurrency control scheme to manage the number of concurrent requests.

6.3 Inserting Data

const insertResult = await client.insert({
  collection_name: COLLECTION_NAME,
  data: diaryData      // So simple, JSON, no need to write SQL
})
console.log(insertResult.insert_cnt, "records inserted successfully.");

Data is passed directly as an array of JSON objects — no need to concatenate INSERT INTO ... VALUES ... like in SQL. Milvus automatically validates whether each record's fields match the Schema; mismatched field names will cause an immediate error.


7. Semantic Search

7.1 Search Implementation in query.mjs

const query = 'I want to see diaries about outdoor activities';
console.log(`Query: ${query}`);
const queryVector = await getEmbedding(query);
const searchResult = await client.search({
  collection_name: COLLECTION_NAME,
  vector: queryVector,              // Query vector
  limit: 2,                          // Return Top-2
  metric_type: MetricType.COSINE,    // Cosine similarity
  output_fields: ['id', 'content', 'date', 'mood', 'tags']  // Scalar fields to return
});

This is the complete semantic search flow:

"Outdoor activities" 
  → Embedding Model → [0.023, -0.451, ..., 0.337] (1024-dim vector)
  → Milvus Search (COSINE) 
  → Returns the 2 most similar diaries

7.2 Another Search Method in main.mjs

main.mjs demonstrates a different search API call method — using the data parameter to pass multiple vectors directly:

const searchRes = await client.search({
    collection_name: COLLECTION_NAME,
    data: [[0.5, 0.5, 0.6, 0.8]],   // Directly pass vector array
    limit: 2,
    output_fields: ['content']        // Only return the content field
})
console.log(JSON.stringify(searchRes.results, null, 2));

Note the use of data (plural vectors) instead of vector (singular vector). This is a batch search capability provided by the SDK — a single request can search for multiple query vectors simultaneously, reducing network round trips.

7.3 Parsing Search Results

console.log(`Found ${searchResult.results.length} results.`);
searchResult.results.forEach((item, index) => {
  console.log(`Result ${index + 1}.[Score: ${item.score.toFixed(4)}]`);
  console.log(`
      ID: ${item.id}
      Content: ${item.content}
      Date: ${item.date}
      Mood: ${item.mood}
      Tags: ${item.tags?.join(",")}
  `)
});

The search for query: 'I want to see diaries about outdoor activities' returns — diary_003 (the hiking diary), with a score of about 0.66:

Result 1.[Score: 0.6575]
  ID: diary_003
  Content: Went hiking with friends over the weekend, the weather was great...
  Mood: relaxed
  Tags: outdoor,friends

The COSINE similarity range is [-1, 1]; 0.66 indicates the two vectors are highly consistent in direction. Core insight: The query term "outdoor activities" and this diary's content "hiking" are semantically related, even though they share no common vocabulary. This is the fundamental difference between vector search and keyword search — it doesn't match literal strings; it matches meaning.


8. RAG: Connecting Retrieval and Generation

8.1 What is RAG

The readme has a concise summary of the RAG flow:

Documents are vectorized and placed in a vector database. For each query, the vectorized query is used for similarity matching in the database to find relevant documents, which are then placed into a prompt for the large language model to generate an answer.

These four sentences precisely summarize the four stages of RAG:

flowchart TD
    Q["❓ User Question<br/>'What have I done recently that made me proud?'"] --> E["🧠 Embedding<br/>Question → Query Vector"]
    E --> S["🔍 Milvus Semantic Retrieval<br/>Return Top-K relevant diaries"]
    S --> P["📋 Assemble Prompt<br/>Context + Question → Complete Prompt"]
    P --> L["🤖 ChatOpenAI / LLM<br/>Generate answer based on diary content"]
    L --> A["✅ Return a warm, empathetic answer"]

rag.mjs implements this as two modular functions.

8.2 Retrieval Module

// r a g modularization
console.log('Retrieving relevant diaries');
async function retrieveRelevantDiaries(question, k = 2) {
  try {
    const queryVector = await getEmbedding(question);
    const searchResult = await client.search({
      collection_name: COLLECTION_NAME,
      vector: queryVector,
      limit: k,                                     // Return the most relevant k diaries
      metric_type: MetricType.COSINE,
      output_fields: ['id', 'content', 'date', 'mood', 'tags']
    });
    return searchResult.results;
  } catch (err) {
    console.error('Error retrieving diaries:', err.message);
    return [];  // Graceful degradation: return empty array on retrieval failure, not affecting the main flow
  }
}

Note the naming of retrieveRelevantDiaries — not simply search, but retrieve. In the context of RAG, "retrieval" is more precise than "search": given a question, find document fragments related to it. The k = 2 here is an important parameter — it determines the amount of context injected into the Prompt. Too small a k might lead to insufficient information; too large a k dilutes effective information and increases token consumption.

8.3 Context Assembly

const context = retrievedDiaries
  .map((diary, i) => `
    [Diary ${i + 1}]
    Date: ${diary.date}
    Mood: ${diary.mood}
    Tags: ${diary.tags?.join(",")}
    Content: ${diary.content}
  `).join('\n')

The retrieved diaries are formatted into structured text blocks, each containing the date, mood, tags, and body. The purpose of this formatting is to help the LLM quickly parse the information structure — clear delimiters ([Diary N]) and field names (Date:, Mood:) allow the model to accurately distinguish between different diaries and different dimensions of information.

8.4 Prompt Engineering

const prompt = `You are a warm and caring AI diary assistant. Answer questions based on the user's diary content,
using friendly and natural language. Please answer the question based on the following 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 content from 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:
`

There are several noteworthy details in the design of this Prompt:

8.5 LLM Call

const model = new ChatOpenAI({
  temperature: 0.1,                  // Low temperature → more deterministic, stable output
  model: process.env.MODEL_NAME,     // qwen-plus
  apiKey: process.env.OPENAI_API_KEY,
  configuration: {
    baseURL: process.env.OPENAI_BASE_URL  // DashScope compatible endpoint
  }
})

const response = await model.invoke(prompt);
console.log(response.content);

temperature: 0.1 is close to 0, meaning the model will tend to choose the highest probability token, making the output more stable and predictable. In a RAG scenario, we want the model to be faithful to the retrieved document content rather than improvising freely — so a low temperature is the correct choice.

8.6 Main Entry Point for the Complete RAG

async function main() {
  try {
    console.log('Connecting to Milvus ...');
    // SDK v3 auto-connects
    console.log('Connected');
    await answerQuestion('What have I done recently that made me proud?');
  } catch (err) {
    console.error('Query failed:', err.message);
  }
}
main().catch(console.error);

The question asked here is "What have I done recently that made me proud?" The word "proud" does not appear in any diary entry, but the vector search can hit diaries containing "sense of accomplishment" in their content — because "proud" and "sense of accomplishment" are close in semantic space. This is precisely the core advantage of embedding + vector search over keyword search.

The actual run results hit diary_002 (project milestone, sense of accomplishment) and diary_005 (family praise, sense of accomplishment), and the LLM generated a warm, summarizing answer based on this.


9. main.mjs: The Fastest Onboarding Path

main.mjs is the most streamlined example in the project, demonstrating the core operational loop of Milvus with minimal code. Let's walk through it completely:

import {
  MilvusClient,   // client connects to zilliz server
  IndexType,      // Index type
  MetricType      // Similarity calculation type
} from '@zilliz/milvus2-sdk-node'
import 'dotenv/config'

Connection:

const client = new MilvusClient({
  address: ADDRESS,
  token: TOKEN
});
const checkHealth = await client.checkHealth();

Search (using a pre-created test Collection):

const searchRes = await client.search({
  collection_name: COLLECTION_NAME,         // mysql table, collection
  data: [[0.5, 0.5, 0.6, 0.8]],           // 4-dim vector — because DIMENSION = 4 during creation
  limit: 2,
  output_fields: ['content']
})

A low-level detail about the dimension (DIMENSION) setting: DIMENSION = 4 in main.mjs is for demonstration convenience. In practical applications, too low a dimension leads to insufficient vector "expressiveness" (much semantic information is compressed and lost), while too high a dimension consumes more storage and computational resources. The output dimension of an Embedding model (like 1024) is an engineering sweet spot validated by extensive experimentation, striking a balance between semantic richness and computational overhead.


10. Full .env Configuration Panorama

MODEL_NAME=qwen-plus                  # LLM Model: Tongyi Qianwen Enhanced Edition
EMBEDDINGS_MODEL_NAME=text-embedding-v3  # Embedding Model
OPENAI_API_KEY=sk-xxx                 # API Key (DashScope)
OPENAI_BASE_URL=https://dashscope.aliyuncs.com/compatible-mode/v1  # Compatible endpoint
MILVUS_ADDRESS=https://xxx.zilliz.com.cn  # Zilliz Cloud Address
MILVUS_TOKEN=xxx                      # Zilliz Auth Token

Two AI services are involved here:

Service Purpose Key Config
Alibaba Cloud DashScope Embedding + LLM OPENAI_BASE_URL points to compatible endpoint
Zilliz Cloud Vector storage & retrieval Fully managed cloud service for Milvus

The architectural implication of using a custom baseURL is: you don't need to integrate multiple SDKs simultaneously. Alibaba Cloud, DeepSeek, Zhipu, and other vendors all provide OpenAI-compatible interfaces. The code only imports the @langchain/openai package, and by switching baseURL, you can use models from different vendors — this is a vendor decoupling solution based on interface unification.


11. Project File Structure Overview

demo/
├── .env                    # Environment variable configuration
├── package.json            # Dependency management
└── src/
    ├── main.mjs            # Simplest connection + search example
    ├── index.mjs           # Complete data flow: connect → create table → vectorize → insert
    ├── query.mjs           # Semantic search query
    └── rag.mjs             # Complete RAG flow: retrieval + generation

Following the note sequence in readme.md, the learning and development path is:

main.mjs (Understand Milvus basic connection and search)
  → index.mjs (Understand Schema design, vectorization, data insertion)
    → query.mjs (Understand the complete semantic search chain)
      → rag.mjs (Connect retrieval and LLM generation, completing the RAG loop)

12. Key Technical Decision Review

Decision Choice Reason
Vector Database Milvus (Zilliz Cloud) Open-source, active community, cloud-native architecture
Embedding Model text-embedding-v3 (DashScope) 1024 dimensions, good Chinese performance
LLM qwen-plus (DashScope) Strong Chinese capability, compatible with OpenAI protocol
Similarity Metric COSINE Optimal choice for text semantic scenarios
Index Type IVF_FLAT Suitable for medium-sized datasets, millisecond response
SDK Version @zilliz/milvus2-sdk-node v3 Latest API design, supports ESM
LLM Wrapper LangChain ChatOpenAI Unified interface, zero-cost vendor switching
Temperature 0.1 RAG scenarios require stable, document-faithful output

Final Words

This article walked you through a complete tech stack from scratch:

  1. Understanding the Problem — Why traditional databases can't do semantic search.
  2. Choosing Tools — What Milvus is and how to use Zilliz Cloud.
  3. Designing the Schema — How to design the field definitions for a Collection.
  4. Data Vectorization — How the Embedding model turns text into vectors.
  5. Building Indexes — Why IVF_FLAT can achieve millisecond search in massive data.
  6. Semantic Search — Using a natural language sentence to find semantically related documents.
  7. RAG Loop — Feeding retrieval results to an LLM to generate empathetic answers.

This AI Diary Assistant has less than 200 lines of code, but the architectural pattern behind it — Vector Database + Embedding + LLM — is the infrastructure foundation for almost all AI Agent products today.

When your Agent needs to "remember" things, "understand" semantics, or "retrieve" memories, this trio is the standard answer.