Giới thiệu
Trong thế giới phát triển ứng dụng AI, việc tối ưu hóa hiệu suất là một yếu tố quan trọng. Một trong những kỹ thuật hữu ích đó là quantization (lượng tử hóa) vectơ. Bài viết này sẽ hướng dẫn bạn cách thực hiện lượng tử hóa vectơ trong ứng dụng Java bằng cách sử dụng MongoDB Atlas Vector Search.
Lượng tử hóa vectơ là quá trình giảm kích thước của các vectơ độ chính xác cao thành các biểu diễn nhỏ hơn, từ đó tiết kiệm bộ nhớ và tăng cường hiệu suất ứng dụng. Theo khuyến nghị của MongoDB, lượng tử hóa là cần thiết cho các ứng dụng có số lượng vectơ lớn hơn 100,000.
Lượng tử hóa vectơ là gì?
Lượng tử hóa vectơ là quá trình chuyển đổi các vectơ độ chính xác cao thành các dạng nhỏ hơn mà vẫn giữ được thông tin cần thiết. Quá trình này giúp giảm mức tiêu thụ tài nguyên mà không làm mất nhiều độ chính xác của dữ liệu.
Tại sao cần lượng tử hóa?
- Tiết kiệm bộ nhớ: Lượng tử hóa giúp giảm bộ nhớ cần thiết để lưu trữ mỗi vectơ.
- Tăng tốc độ: Việc giảm kích thước vectơ giúp tăng tốc độ truy vấn và xử lý.
- Hiệu suất tốt hơn cho ứng dụng: Ứng dụng của bạn sẽ hoạt động hiệu quả hơn với lượng dữ liệu thấp hơn.
Các phương pháp lượng tử hóa
Lượng tử hóa scalar
Lượng tử hóa scalar bắt đầu bằng việc xác định giá trị tối thiểu và tối đa cho mỗi chiều của các vectơ đã chỉ mục. Sau đó, khoảng giá trị này được chia thành các bin có kích thước bằng nhau. Mỗi giá trị float được ánh xạ vào một bin để chuyển đổi các giá trị float trước đó thành các số nguyên rời rạc. Trong MongoDB Atlas Vector Search, lượng tử hóa scalar giúp giảm chi phí RAM của vectơ xuống còn khoảng 25% của chi phí trước khi lượng tử hóa.
Lượng tử hóa nhị phân
Khi một vectơ được chuẩn hóa về chiều dài 1, chẳng hạn như với mô hình text-embedding-3-large
của OpenAI, có thể sử dụng lượng tử hóa nhị phân. Phương pháp này giả định một điểm giữa là 0 cho mỗi chiều trong vectơ. Với mỗi giá trị trong vectơ, chúng ta sẽ gán giá trị nhị phân 1 nếu giá trị lớn hơn điểm giữa và 0 nếu giá trị nhỏ hơn hoặc bằng.
Trong MongoDB Atlas Vector Search, lượng tử hóa nhị phân giúp giảm mức sử dụng RAM của vectơ xuống chỉ còn hơn 4% so với chi phí trước khi lượng tử hóa.
Yêu cầu cho lượng tử hóa
Để tự động lượng tử hóa vectơ hoặc nhập vectơ đã lượng tử hóa, có một số yêu cầu cần được đáp ứng. Bảng dưới đây từ tài liệu của MongoDB về lượng tử hóa vectơ tóm tắt các yêu cầu này:
Yêu cầu | Nhập int1 |
Nhập int8 |
Lượng tử hóa scalar tự động | Lượng tử hóa nhị phân tự động |
---|---|---|---|---|
Cần định nghĩa chỉ mục | Không | Không | Có | Có |
Cần định dạng BSON binData |
Có | Có | Không | Không |
Lưu trữ trên mongod | binData(int1) |
binData(int8) |
binData(float32) array(double) |
binData(float32) array(double) |
Phương pháp tương tự được hỗ trợ | euclidean |
cosine euclidean dotProduct |
cosine euclidean dotProduct |
cosine euclidean dotProduct |
Số chiều hỗ trợ | Bội số của 8 | Từ 1 đến 8192 | Từ 1 đến 8192 | Từ 1 đến 8192 |
Hỗ trợ tìm kiếm ANN và ENN | Có | Có | Có | Có |
Lưu ý: MongoDB Atlas lưu trữ tất cả các giá trị điểm động dưới dạng kiểu dữ liệu double
nội bộ. Do đó, cả vectơ 32-bit và 64-bit đều tương thích với lượng tử hóa tự động mà không cần chuyển đổi.
Ứng dụng Java của chúng ta
Trong hướng dẫn này, chúng ta sẽ thiết lập ứng dụng Java và cơ sở dữ liệu MongoDB để nhúng một số dữ liệu mẫu, và thực hiện truy vấn tìm kiếm vectơ trên đó. Chúng ta sẽ thực hiện hai cách tiếp cận chính: tự động lượng tử hóa các vectơ và nhập các vectơ đã lượng tử hóa trước.
Các yêu cầu trước
- Tài khoản MongoDB Atlas với một cụm đã được thiết lập.
- Java trên máy của bạn, được cài đặt và sẵn sàng sử dụng (Tôi sử dụng phiên bản Java 21).
- Maven (Tôi sử dụng phiên bản 3.9.10).
- Mã thông báo Voyage AI API.
- Các mô hình nhúng khác cũng hoạt động—đảm bảo nó hỗ trợ lượng tử hóa vectơ. Kiểm tra danh sách các mô hình được hỗ trợ trong tài liệu của chúng tôi.
Thiết lập ứng dụng Java
Đầu tiên, hãy tạo ứng dụng Maven mới. Sau khi thiết lập dự án, mở tệp application.properties
và thêm các giá trị cấu hình cần thiết để kết nối với MongoDB và Voyage AI, hoặc thêm chúng dưới dạng biến môi trường.
properties
VOYAGE_API_KEY=YOUR_VOYAGE_AI_API_KEY
MONGODB_URI=YOUR_MONGODB_CONNECTION_STRING
Chúng ta cần thêm các phụ thuộc vào tệp pom.xml
:
xml
<dependencies>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.13.2</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>2.19.2</version>
</dependency>
<dependency>
<groupId>org.mongodb</groupId>
<artifactId>mongodb-driver-sync</artifactId>
<version>5.3.1</version>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
<version>2.0.16</version>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-simple</artifactId>
<version>2.0.16</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.json</groupId>
<artifactId>json</artifactId>
<version>20250517</version>
</dependency>
<dependency>
<groupId>com.squareup.okhttp3</groupId>
<artifactId>okhttp</artifactId>
<version>4.12.0</version>
</dependency>
</dependencies>
Thiết lập lớp chính
Để đơn giản hóa, chúng ta sẽ giữ logic cho ứng dụng này trong một lớp Main
. Lớp Main
này thiết lập mọi thứ mà ứng dụng cần trước khi chúng ta viết bất kỳ logic nào:
- Cấu hình dựa trên môi trường:
VOYAGE_API_KEY
vàMONGODB_URI
được đọc từ các biến môi trường để bạn không phải mã hóa bí mật. Nếu bất kỳ biến nào bị thiếu, ứng dụng sẽ gặp lỗi rõ ràng. - Hằng số dự án: Tên cơ sở dữ liệu/collection/index (
test
,demo
,vector_index
) sẽ được sử dụng để kết nối với cơ sở dữ liệu MongoDB và cấu hình chỉ mục tìm kiếm vectơ của chúng ta. - Chi tiết HTTP:
VOYAGE_API_URL
trỏ đến điểm cuối nhúng của Voyage AI. Thời gian kết nối/đọc giữ cho các cuộc gọi HTTP của chúng ta có thể dự đoán. - Tập dữ liệu mẫu + truy vấn:
DATA
là danh sách nhỏ các tài liệu mà chúng ta sẽ nhúng và lưu trữ;QUERY_TEXT
là những gì chúng ta sẽ sử dụng để chạy một truy vấn tìm kiếm vectơ sau này.
java
public class Main {
// Cấu hình
private static final String VOYAGE_API_KEY = System.getenv("VOYAGE_API_KEY");
private static final String MONGODB_URI = System.getenv("MONGODB_URI");
private static final String DB_NAME = "test";
private static final String COLLECTION_NAME = "demo";
private static final String INDEX_NAME = "vector_index";
private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper();
// Điểm cuối Voyage AI
private static final String VOYAGE_API_URL = "https://api.voyageai.com/v1/embeddings";
// Thời gian timeout cho các yêu cầu API
private static final int CONNECTION_TIMEOUT = 30;
private static final int READ_TIMEOUT = 60;
// Dữ liệu mẫu
private static final List<String> DATA = List.of(
"Vạn lý trường thành có thể nhìn thấy từ không gian.",
"Tháp Eiffel được hoàn thành ở Paris vào năm 1889.",
"Núi Everest là đỉnh cao nhất trên Trái đất với độ cao 8,848m.",
"Shakespeare đã viết 37 vở kịch và 154 bài sonnet trong suốt cuộc đời của ông.",
"Bức tranh Mona Lisa được vẽ bởi Leonardo da Vinci."
);
private static final String QUERY_TEXT = "Các danh lam thắng cảnh của đất nước";
public static void main(String[] args) {
if (VOYAGE_API_KEY == null || VOYAGE_API_KEY.isEmpty()) {
throw new RuntimeException("Không tìm thấy API key. Đặt VOYAGE_API_KEY trong môi trường của bạn.");
}
if (MONGODB_URI == null || MONGODB_URI.isEmpty()) {
throw new RuntimeException("Không tìm thấy MongoDB URI. Đặt MONGODB_URI trong môi trường của bạn.");
}
// thêm mã ở đây
}
}
Nhận phản hồi
Chúng ta sẽ tạo hai bản ghi để xử lý phản hồi từ API Voyage AI. Bản ghi đầu tiên được gọi là ResponseDouble
.
Khi chúng ta sử dụng lượng tử hóa tự động trong MongoDB Atlas, chúng ta gửi và lưu trữ các vectơ float (được nhận dưới dạng double trong Java). Bản ghi này phản ánh hình dạng JSON Voyage AI cho các nhúng float:
data[i].embedding
làList<Double>
, ánh xạ rõ ràng đếnarray<double>
của MongoDB (điều mà Atlas yêu cầu khi nó sẽ tự động lượng tử hóa cho chúng ta).- Đường dẫn này giữ cho các vectơ đầy đủ vẫn có sẵn để tái đánh giá trong khi chỉ mục lưu trữ dạng đã lượng tử hóa trong RAM.
java
package com.timkelly;
import java.util.List;
public record ResponseDouble (
String object,
List<DataItem> data,
String model,
Usage usage
) {
public record DataItem(
String object,
List<Double> embedding, // lưu trữ dưới dạng double trong MongoDB
int index
) {}
public record Usage(
int total_tokens
) {}
}
Cách để kích hoạt lượng tử hóa tự động các vectơ
Chúng ta có thể cấu hình MongoDB Atlas Vector Search để tự động lượng tử hóa các nhúng vectơ float trong collection của chúng ta thành các loại biểu diễn giảm, chẳng hạn như int8
(scalar) và binary
trong các chỉ mục vectơ của chúng ta.
json
{
"fields":[
{
"type": "vector",
"path": "<field-to-index>",
"numDimensions": <number-of-dimensions>,
"similarity": "euclidean | cosine | dotProduct",
"quantization": "none | scalar | binary",
"hnswOptions": {
"maxEdges": <number-of-connected-neighbors>,
"numEdgeCandidates": <number-of-nearest-neighbors>
}
},
{
"type": "filter",
"path": "<field-to-index>"
},
...
]
}
Nhúng dữ liệu của chúng ta
Dưới phương thức chính, chúng ta sẽ tạo một phương thức embedDataAndCreateDocument
sẽ nhận vào một danh sách các chuỗi (dữ liệu của chúng ta để nhúng) và trả về một danh sách các tài liệu BSON với tất cả các nhúng của chúng ta đã sẵn sàng để truy vấn bằng các vectơ đã lượng tử hóa scalar hoặc binary tự động.
java
private static List<Document> embedDataAndCreateDocument(List<String> data) {
OkHttpClient client = new OkHttpClient.Builder()
.connectTimeout(CONNECTION_TIMEOUT, TimeUnit.SECONDS)
.readTimeout(READ_TIMEOUT, TimeUnit.SECONDS)
.build();
try {
ResponseDouble baselineValues = embedBaselineDoubles(client, data);
// Tạo một danh sách các tài liệu BSON chứa tất cả các nhúng
return createCombinedEmbeddingsDocument(
data,
baselineValues);
} catch (IOException e) {
throw new RuntimeException("Lỗi khi lấy nhúng: ", e);
}
}
Kết luận
Với thiết lập này, chúng ta đã thấy cách nhúng dữ liệu, lưu trữ nó trong MongoDB và truy vấn nó với Atlas Vector Search qua nhiều chiến lược lượng tử hóa. Lượng tử hóa tự động giúp dễ dàng giảm bộ nhớ trong khi vẫn giữ được độ chính xác cao, trong khi vectơ đã lượng tử hóa trước đem lại kích thước payload nhỏ nhất và truy vấn nhanh nhất khi mô hình nhúng của bạn hỗ trợ chúng. Sự đánh đổi giữa độ chính xác, mức sử dụng tài nguyên và tốc độ trở nên rõ ràng khi bạn chạy cùng một truy vấn qua từng chỉ mục.
Nếu bạn thấy hướng dẫn này hữu ích và muốn tìm hiểu thêm về những gì bạn có thể làm với MongoDB trong Java, hãy kiểm tra cách tôi biến kho Obsidian của mình thành một wiki có thể tìm kiếm hoặc Tìm kiếm RAG cục bộ an toàn với quyền truy cập dựa trên vai trò: Spring AI, Ollama & MongoDB.