Milvus
Zilliz
  • Home
  • Blog
  • RAG pratique avec Qwen3 Modèles d'intégration et de re-ranking avec Milvus

RAG pratique avec Qwen3 Modèles d'intégration et de re-ranking avec Milvus

  • Tutorials
June 30, 2025
Lumina

Si vous avez gardé un œil sur l'espace des modèles d'intégration, vous avez probablement remarqué qu'Alibaba vient de lancer sa série Qwen3 Embedding. Ils ont publié des modèles d'intégration et de reclassement en trois tailles (0.6B, 4B, 8B), tous construits sur les modèles de base Qwen3 et conçus spécifiquement pour les tâches de recherche.

La série Qwen3 présente quelques caractéristiques que j'ai trouvées intéressantes :

  • Intégration multilingue - ils revendiquent un espace sémantique unifié dans plus de 100 langues.

  • Instruction prompte - vous pouvez passer des instructions personnalisées pour modifier le comportement de l'intégration.

  • Dimensions variables - prend en charge différentes tailles d'intégration via l'apprentissage de la représentation Matryoshka

  • Longueur de contexte 32K - peut traiter des séquences d'entrée plus longues

  • Configuration standard à double encodeur et à encodeur croisé - le modèle d'intégration utilise un double encodeur, le reranker utilise un encodeur croisé.

Si l'on examine les points de référence, Qwen3-Embedding-8B a obtenu un score de 70,58 sur le tableau de classement multilingue MTEB, dépassant BGE, E5 et même Google Gemini. Qwen3-Reranker-8B a obtenu un score de 69,02 pour les tâches de classement multilingues. Il ne s'agit pas simplement d'une "bonne performance parmi les modèles à code source ouvert", mais d'une performance équivalente, voire supérieure, à celle des principales API commerciales. Dans les systèmes de recherche RAG, de recherche interlangues et de recherche de codes, en particulier dans les contextes chinois, ces modèles ont déjà des capacités prêtes à être produites.

En tant que personne ayant probablement eu affaire aux suspects habituels (embeddings d'OpenAI, BGE, E5), vous vous demandez peut-être si ces modèles valent la peine que vous y consacriez du temps. Spoiler : c'est le cas.

Ce que nous construisons

Ce tutoriel présente la construction d'un système RAG complet utilisant Qwen3-Embedding-0.6B et Qwen3-Reranker-0.6B avec Milvus. Nous mettrons en œuvre un pipeline de récupération en deux étapes :

  1. Récupération dense avec Qwen3 embeddings pour une sélection rapide des candidats

  2. Reranking avec l'encodeur croisé Qwen3 pour l'affinement de la précision

  3. Génération avec OpenAI's GPT-4 pour les réponses finales

À la fin, vous aurez un système fonctionnel qui gère les requêtes multilingues, utilise des instructions pour l'ajustement du domaine et équilibre la vitesse avec la précision grâce à un reclassement intelligent.

Configuration de l'environnement

Commençons par les dépendances. Notez les versions minimales requises - elles sont importantes pour la compatibilité :

pip install --upgrade pymilvus openai requests tqdm sentence-transformers transformers

Transformers>=4.51.0 et sentence-transformers>=2.7.0 sont requis.

Pour ce tutoriel, nous utiliserons OpenAI comme modèle de génération. Configurez votre clé API :

import os

os.environ[“OPENAI_API_KEY”] = “sk-***********”

Préparation des données

Nous utiliserons la documentation Milvus comme base de connaissances - c'est un bon mélange de contenu technique qui teste à la fois la qualité de l'extraction et de la génération.

Télécharger et extraire la documentation :

! wget https://github.com/milvus-io/milvus-docs/releases/download/v2.4.6-preview/milvus_docs_2.4.x_en.zip
! unzip -q milvus_docs_2.4.x_en.zip -d milvus_docs

Chargement et découpage des fichiers markdown. Nous utilisons ici une simple stratégie de découpage basée sur les en-têtes - pour les systèmes de production, envisagez des approches de découpage plus sophistiquées :

from glob import glob

text_lines = []

for file_path in glob(“milvus_docs/en/faq/*.md”, recursive=True): with open(file_path, “r”) as file: file_text = file.read()

text_lines += file_text.split(<span class="hljs-string">&quot;# &quot;</span>)

Configuration du modèle

Initialisons maintenant nos modèles. Nous utilisons les versions légères 0.6B, qui offrent un bon équilibre entre les performances et les besoins en ressources :

from openai import OpenAI
from sentence_transformers import SentenceTransformer
import torch
from transformers import AutoModel, AutoTokenizer, AutoModelForCausalLM

# Initialize OpenAI client for LLM generation openai_client = OpenAI()

# Load Qwen3-Embedding-0.6B model for text embeddings embedding_model = SentenceTransformer(“Qwen/Qwen3-Embedding-0.6B”)

# Load Qwen3-Reranker-0.6B model for reranking reranker_tokenizer = AutoTokenizer.from_pretrained(“Qwen/Qwen3-Reranker-0.6B”, padding_side=‘left’) reranker_model = AutoModelForCausalLM.from_pretrained(“Qwen/Qwen3-Reranker-0.6B”).eval()

# Reranker configuration token_false_id = reranker_tokenizer.convert_tokens_to_ids(“no”) token_true_id = reranker_tokenizer.convert_tokens_to_ids(“yes”) max_reranker_length = 8192

prefix = “<|im_start|>system\nJudge whether the Document meets the requirements based on the Query and the Instruct provided. Note that the answer can only be "yes" or "no".<|im_end|>\n<|im_start|>user\n” suffix = “<|im_end|>\n<|im_start|>assistant\n\n\n\n\n” prefix_tokens = reranker_tokenizer.encode(prefix, add_special_tokens=False) suffix_tokens = reranker_tokenizer.encode(suffix, add_special_tokens=False)

Le résultat attendu :

Fonction d'intégration

L'élément clé de l'intégration de Qwen3 est la possibilité d'utiliser des invites différentes pour les requêtes et les documents. Ce détail apparemment insignifiant peut améliorer de manière significative les performances de recherche :

def emb_text(text, is_query=False):
    """
    Generate text embeddings using Qwen3-Embedding-0.6B model.
Args:
    text: Input text to embed
    is_query: Whether this is a query (True) or document (False)

Returns:
    List of embedding values
&quot;&quot;&quot;</span>
<span class="hljs-keyword">if</span> is_query:
    <span class="hljs-comment"># For queries, use the &quot;query&quot; prompt for better retrieval performance</span>
    embeddings = embedding_model.encode([text], prompt_name=<span class="hljs-string">&quot;query&quot;</span>)
<span class="hljs-keyword">else</span>:
    <span class="hljs-comment"># For documents, use default encoding</span>
    embeddings = embedding_model.encode([text])

<span class="hljs-keyword">return</span> embeddings[<span class="hljs-number">0</span>].tolist()

Testons la fonction d'intégration et vérifions les dimensions du résultat :

test_embedding = emb_text("This is a test")
embedding_dim = len(test_embedding)
print(f"Embedding dimension: {embedding_dim}")
print(f"First 10 values: {test_embedding[:10]}")

Résultat attendu :

Embedding dimension: 1024
First 10 values: [-0.009923271834850311, -0.030248118564486504, -0.011494234204292297, ...]

Mise en œuvre du reclassement

Le reranker utilise une architecture d'encodeurs croisés pour évaluer les paires requête-document. Cette architecture est plus coûteuse en termes de calcul que le modèle d'intégration à double encodeur, mais elle permet d'obtenir une notation de la pertinence beaucoup plus nuancée.

Voici le pipeline complet de reranking :

def format_instruction(instruction, query, doc):
    """Format instruction for reranker input"""
    if instruction is None:
        instruction = 'Given a web search query, retrieve relevant passages that answer the query'
    output = "<Instruct>: {instruction}\n<Query>: {query}\n<Document>: {doc}".format(
        instruction=instruction, query=query, doc=doc
    )
    return output

def process_inputs(pairs): “""Process inputs for reranker""” inputs = reranker_tokenizer( pairs, padding=False, truncation=‘longest_first’, return_attention_mask=False, max_length=max_reranker_length - len(prefix_tokens) - len(suffix_tokens) ) for i, ele in enumerate(inputs[‘input_ids’]): inputs[‘input_ids’][i] = prefix_tokens + ele + suffix_tokens inputs = reranker_tokenizer.pad(inputs, padding=True, return_tensors=“pt”, max_length=max_reranker_length) for key in inputs: inputs[key] = inputs[key].to(reranker_model.device) return inputs

@torch.no_grad() def compute_logits(inputs, kwargs): “""Compute relevance scores using reranker""” batch_scores = reranker_model(inputs).logits[:, -1, :] true_vector = batch_scores[:, token_true_id] false_vector = batch_scores[:, token_false_id] batch_scores = torch.stack([false_vector, true_vector], dim=1) batch_scores = torch.nn.functional.log_softmax(batch_scores, dim=1) scores = batch_scores[:, 1].exp().tolist() return scores

def rerank_documents(query, documents, task_instruction=None): “"” Rerank documents based on query relevance using Qwen3-Reranker

Args:
    query: Search query
    documents: List of documents to rerank
    task_instruction: Task instruction for reranking

Returns:
    List of (document, score) tuples sorted by relevance score
&quot;&quot;&quot;</span>
<span class="hljs-keyword">if</span> task_instruction <span class="hljs-keyword">is</span> <span class="hljs-literal">None</span>:
    task_instruction = <span class="hljs-string">&#x27;Given a web search query, retrieve relevant passages that answer the query&#x27;</span>

<span class="hljs-comment"># Format inputs for reranker</span>
pairs = [format_instruction(task_instruction, query, doc) <span class="hljs-keyword">for</span> doc <span class="hljs-keyword">in</span> documents]

<span class="hljs-comment"># Process inputs and compute scores</span>
inputs = process_inputs(pairs)
scores = compute_logits(inputs)

<span class="hljs-comment"># Combine documents with scores and sort by score (descending)</span>
doc_scores = <span class="hljs-built_in">list</span>(<span class="hljs-built_in">zip</span>(documents, scores))
doc_scores.sort(key=<span class="hljs-keyword">lambda</span> x: x[<span class="hljs-number">1</span>], reverse=<span class="hljs-literal">True</span>)

<span class="hljs-keyword">return</span> doc_scores

Configuration de la base de données vectorielle Milvus

Configurons maintenant notre base de données vectorielle. Nous utilisons Milvus Lite pour plus de simplicité, mais le même code fonctionne avec les déploiements Milvus complets :

from pymilvus import MilvusClient

milvus_client = MilvusClient(uri=“./milvus_demo.db”)

collection_name = “my_rag_collection”

Options de déploiement :

  • Fichier local (comme ./milvus.db) : Utilise Milvus Lite, parfait pour le développement.

  • Docker/Kubernetes: Utilise l'URI du serveur comme http://localhost:19530 pour la production

  • Zilliz Cloud: Utiliser le point de terminaison du nuage et la clé API pour le service géré.

Nettoyer toute collection existante et en créer une nouvelle :

# Remove existing collection if it exists
if milvus_client.has_collection(collection_name):
    milvus_client.drop_collection(collection_name)

# Create new collection with our embedding dimensions milvus_client.create_collection( collection_name=collection_name, dimension=embedding_dim, # 1024 for Qwen3-Embedding-0.6B metric_type=“IP”, # Inner product for similarity consistency_level=“Strong”, # Ensure data consistency )

Chargement des données dans Milvus

Traitons maintenant nos documents et insérons-les dans la base de données vectorielle :

from tqdm import tqdm

data = []

for i, line in enumerate(tqdm(text_lines, desc=“Creating embeddings”)): data.append({“id”: i, “vector”: emb_text(line), “text”: line})

milvus_client.insert(collection_name=collection_name, data=data)

Résultat attendu :

Creating embeddings: 100%|████████████| 72/72 [00:08<00:00, 8.68it/s]
Inserted 72 documents

Améliorer le RAG avec la technologie de reclassement

Maintenant vient la partie la plus excitante - mettre tout cela ensemble dans un système complet de génération augmentée par la recherche.

Étape 1 : Requête et recherche initiale

Testons avec une question courante sur Milvus :

question = "How is data stored in milvus?"

# Perform initial dense retrieval to get top candidates search_res = milvus_client.search( collection_name=collection_name, data=[emb_text(question, is_query=True)], # Use query prompt limit=10, # Get top 10 candidates for reranking search_params={“metric_type”: “IP”, “params”: {}}, output_fields=[“text”], # Return the actual text content )

print(f"Found {len(search_res[0])} initial candidates")

Étape 2 : Reclassement pour plus de précision

Extraction des documents candidats et application du reclassement :

# Extract candidate documents
candidate_docs = [res["entity"]["text"] for res in search_res[0]]

# Rerank using Qwen3-Reranker print(“Reranking documents…”) reranked_docs = rerank_documents(question, candidate_docs)

# Select top 3 after reranking top_reranked_docs = reranked_docs[:3] print(f"Selected top {len(top_reranked_docs)} documents after reranking")

Étape 3 : Comparaison des résultats

Examinons comment le reclassement modifie les résultats :

Reranked results (top 3):
[
    [
        " Where does Milvus store data?\n\nMilvus deals with two types of data, inserted data and metadata. \n\nInserted data, including vector data, scalar data, and collection-specific schema, are stored in persistent storage as incremental log. Milvus supports multiple object storage backends, including [MinIO](https://min.io/), [AWS S3](https://aws.amazon.com/s3/?nc1=h_ls), [Google Cloud Storage](https://cloud.google.com/storage?hl=en#object-storage-for-companies-of-all-sizes) (GCS), [Azure Blob Storage](https://azure.microsoft.com/en-us/products/storage/blobs), [Alibaba Cloud OSS](https://www.alibabacloud.com/product/object-storage-service), and [Tencent Cloud Object Storage](https://www.tencentcloud.com/products/cos) (COS).\n\nMetadata are generated within Milvus. Each Milvus module has its own metadata that are stored in etcd.\n\n###",
        0.9997891783714294
    ],
    [
        "How does Milvus flush data?\n\nMilvus returns success when inserted data are loaded to the message queue. However, the data are not yet flushed to the disk. Then Milvus' data node writes the data in the message queue to persistent storage as incremental logs. If `flush()` is called, the data node is forced to write all data in the message queue to persistent storage immediately.\n\n###",
        0.9989748001098633
    ],
    [
        "Does the query perform in memory? What are incremental data and historical data?\n\nYes. When a query request comes, Milvus searches both incremental data and historical data by loading them into memory. Incremental data are in the growing segments, which are buffered in memory before they reach the threshold to be persisted in storage engine, while historical data are from the sealed segments that are stored in the object storage. Incremental data and historical data together constitute the whole dataset to search.\n\n###",
        0.9984032511711121
    ]
]

================================================================================ Original embedding-based results (top 3): [ [ " Where does Milvus store data?\n\nMilvus deals with two types of data, inserted data and metadata. \n\nInserted data, including vector data, scalar data, and collection-specific schema, are stored in persistent storage as incremental log. Milvus supports multiple object storage backends, including [MinIO](https://min.io/), [AWS S3](https://aws.amazon.com/s3/?nc1=h_ls), [Google Cloud Storage](https://cloud.google.com/storage?hl=en#object-storage-for-companies-of-all-sizes) (GCS), [Azure Blob Storage](https://azure.microsoft.com/en-us/products/storage/blobs), [Alibaba Cloud OSS](https://www.alibabacloud.com/product/object-storage-service), and [Tencent Cloud Object Storage](https://www.tencentcloud.com/products/cos) (COS).\n\nMetadata are generated within Milvus. Each Milvus module has its own metadata that are stored in etcd.\n\n###", 0.8306853175163269 ], [ "How does Milvus flush data?\n\nMilvus returns success when inserted data are loaded to the message queue. However, the data are not yet flushed to the disk. Then Milvus’ data node writes the data in the message queue to persistent storage as incremental logs. If <span class="hljs-title">flush</span>() is called, the data node is forced to write all data in the message queue to persistent storage immediately.\n\n###", 0.7302717566490173 ], [ "How does Milvus handle vector data types and precision?\n\nMilvus supports Binary, Float32, Float16, and BFloat16 vector types.\n\n- Binary vectors: Store binary data as sequences of 0s and 1s, used in image processing and information retrieval.\n- Float32 vectors: Default storage with a precision of about 7 decimal digits. Even Float64 values are stored with Float32 precision, leading to potential precision loss upon retrieval.\n- Float16 and BFloat16 vectors: Offer reduced precision and memory usage. Float16 is suitable for applications with limited bandwidth and storage, while BFloat16 balances range and efficiency, commonly used in deep learning to reduce computational requirements without significantly impacting accuracy.\n\n###", 0.7003671526908875 ] ]

Le reranking affiche généralement des scores discriminants beaucoup plus élevés (plus proches de 1,0 pour les documents pertinents) que les scores de similarité d'intégration.

Étape 4 : Génération de la réponse finale

Utilisons maintenant le contexte récupéré pour générer une réponse complète :

Premièrement : Convertir les documents extraits au format chaîne de caractères.

context = "\n".join(
    [line_with_distance[0] for line_with_distance in retrieved_lines_with_distances]
)

Fournir une invite système et une invite utilisateur pour le modèle linguistique étendu. Cette invite est générée à partir des documents extraits de Milvus.

SYSTEM_PROMPT = """
Human: You are an AI assistant. You are able to find answers to the questions from the contextual passage snippets provided.
"""
USER_PROMPT = f"""
Use the following pieces of information enclosed in <context> tags to provide an answer to the question enclosed in <question> tags.
<context>
{context}
</context>
<question>
{question}
</question>
"""

Utiliser GPT-4o pour générer une réponse basée sur les invites.

response = openai_client.chat.completions.create(
    model="gpt-4o",
    messages=[
        {"role": "system", "content": SYSTEM_PROMPT},
        {"role": "user", "content": USER_PROMPT},
    ],
)
print(response.choices[0].message.content)

Résultat attendu :

In Milvus, data is stored in two main forms: inserted data and metadata. 
Inserted data, which includes vector data, scalar data, and collection-specific 
schema, is stored in persistent storage as incremental logs. Milvus supports 
multiple object storage backends for this purpose, including MinIO, AWS S3, 
Google Cloud Storage, Azure Blob Storage, Alibaba Cloud OSS, and Tencent 
Cloud Object Storage. Metadata for Milvus is generated by its various modules 
and stored in etcd.

Récapitulation

Ce tutoriel a démontré une mise en œuvre complète de RAG à l'aide des modèles d'intégration et de reclassement de Qwen3. Les principaux points à retenir :

  1. Larécupération en deux étapes (dense + reranking) améliore systématiquement la précision par rapport aux approches basées uniquement sur l'intégration.

  2. L'incitation à l'instruction permet un réglage spécifique au domaine sans réentraînement.

  3. Les capacités multilingues fonctionnent naturellement sans complexité supplémentaire

  4. Ledéploiement local est possible avec les modèles 0.6B

La série Qwen3 offre de solides performances dans un ensemble léger et à code source ouvert. Sans être révolutionnaires, ils apportent des améliorations progressives et des fonctionnalités utiles, telles que les messages d'instruction, qui peuvent faire une réelle différence dans les systèmes de production.

Testez ces modèles avec vos données spécifiques et vos cas d'utilisation - ce qui fonctionne le mieux dépend toujours de votre contenu, de vos modèles de requête et de vos exigences en matière de performances.

    Try Managed Milvus for Free

    Zilliz Cloud is hassle-free, powered by Milvus and 10x faster.

    Get Started

    Like the article? Spread the word

    Continuer à Lire