向量搜索升级指南:FAISS 到 Qdrant 迁移方案与代码实现

简介: FAISS 适合实验,但生产环境痛点诸多:无元数据支持、非服务化、难持久化。迁移到 Qdrant 后,实现开箱即用的向量数据库能力,支持混合搜索、过滤、持久化与高效 API,大幅提升系统稳定性与开发效率,真正打通从研究到生产的闭环。

FAISS 在实验阶段确实好用,速度快、上手容易,notebook 里跑起来很顺手。但把它搬到生产环境还是有很多问题:

首先是元数据的问题,FAISS 索引只认向量,如果想按日期或其他条件筛选还需要自己另外搞一套查找系统。

其次它本质上是个库而不是服务,让如果想对外提供接口还得自己用 Flask 或 FastAPI 包一层。

最后最麻烦的是持久化,pod 一旦挂掉索引就没了,除非提前手动存盘。

Qdrant 的出现解决了这些痛点,它更像是个真正的数据库,提供开箱即用的 API、数据重启后依然在、原生支持元数据过滤。更关键的是混合搜索(Dense + Sparse)和量化这些高级功能都是内置的。

MS MARCO Passages 数据集

数据集地址:

MS MARCO 官方页面:https://microsoft.github.io/msmarco/

这次用的是 MS MARCO Passage Ranking 数据集,信息检索领域的标准测试集。

数据是从网页抓取的约880万条短文本段落,选它的原因很简单:段落短(平均50词),不用处理复杂的文本分块,可以把精力放在迁移工程本身。

实际测试时用了10万条数据的子集,这样速度会很快

嵌入模型用的是 sentence-transformers/all-MiniLM-L6-v2,输出384维的稠密向量。

SentenceTransformers 模型地址:https://huggingface.co/sentence-transformers/all-MiniLM-L6-v2

FAISS 阶段的初始配置

生成嵌入向量

加载原始数据,批量生成嵌入向量。这里关键的一步是把结果存成 .npy 文件,避免后续重复计算。

 import pandas as pd  
from sentence_transformers import SentenceTransformer  
import numpy as np  
import os  
import csv  

DATA_PATH = '../data'  
TSV_FILE = f'{DATA_PATH}/collection.tsv'  
SAMPLE_SIZE = 100000  
MODEL_ID = 'all-MiniLM-L6-v2'  

def prepare_data():  
   print(f"Loading Model '{MODEL_ID}'...")  
   model = SentenceTransformer(MODEL_ID)  
   print(f"Reading first {SAMPLE_SIZE} lines from {TSV_FILE}...")  
   ids = []  
   passages = []  
   # Efficiently read line-by-line without loading entire 8GB file to RAM
   try:  
       with open(TSV_FILE, 'r', encoding='utf8') as f:  
           reader = csv.reader(f, delimiter='\t')  
           for i, row in enumerate(reader):  
               if i >= SAMPLE_SIZE:  
                   break  
               # MS MARCO format is: [pid, text]
               if len(row) >= 2:  
                   ids.append(int(row[0]))  
                   passages.append(row[1])         
   except FileNotFoundError:  
       print(f"Error: Could not find {TSV_FILE}")  
       return  

   print(f"Loaded {len(passages)} passages.")  

   # Save text metadata (for Qdrant payload)
   print("Saving metadata to CSV...")  
   df = pd.DataFrame({'id': ids, 'text': passages})  
   df.to_csv(f'{DATA_PATH}/passages.csv', index=False)  
   # Generate Embeddings
   print("Encoding Embeddings (this may take a moment)...")  
   embeddings = model.encode(passages, show_progress_bar=True)  
   # Save binary files (for FAISS and Qdrant)
   print("5. Saving numpy arrays...")  
   np.save(f'{DATA_PATH}/embeddings.npy', embeddings)  
   np.save(f'{DATA_PATH}/ids.npy', np.array(ids))  
   print(f"Success! Saved {embeddings.shape} embeddings to {DATA_PATH}")  

if __name__ == "__main__":  
   os.makedirs(DATA_PATH, exist_ok=True)  
    prepare_data()

构建索引

用 IndexFlatL2 做精确搜索,对于百万级别的数据量来说足够了。

 import faiss  
import numpy as np  
import os  

DATA_PATH = '../data'  
INDEX_OUTPUT_PATH = './my_index.faiss'  

def build_index():  
   print("Loading embeddings...")  
   # Load the vectors
   if not os.path.exists(f'{DATA_PATH}/embeddings.npy'):  
       print(f"Error: {DATA_PATH}/embeddings.npy not found.")  
       return  
   embeddings = np.load(f'{DATA_PATH}/embeddings.npy')  
   d = embeddings.shape[1]  # Dimension (should be 384 for MiniLM)
   print(f"Building Index (Dimension={d})...")  
   # We use IndexFlatL2 for exact search (Simple & Accurate for <1M vectors).
   index = faiss.IndexFlatL2(d)  
   index.add(embeddings)  
   print(f"Saving index to {INDEX_OUTPUT_PATH}..")  
   faiss.write_index(index, INDEX_OUTPUT_PATH)  
   print(f"Success! Index contains {index.ntotal} vectors.")  

if __name__ == "__main__":  
   os.makedirs(os.path.dirname(INDEX_OUTPUT_PATH), exist_ok=True)  
    build_index()

语义搜索测试

随便跑一个查询就能看出问题了。返回的是 [42, 105] 这种 ID,如果想拿到实际文本还得写一堆代码去 CSV 里查,这种割裂感是迁移的主要原因。

 import faiss  
import numpy as np  
import pandas as pd  
from sentence_transformers import SentenceTransformer  

INDEX_PATH = './my_index.faiss'  
DATA_PATH = '../data'  
MODEL_NAME = 'all-MiniLM-L6-v2'  

def search_faiss():  
   print("Loading Index and Metadata...")  
   index = faiss.read_index(INDEX_PATH)  
   # LIMITATION: We must manually load the CSV to get text back.
   # FAISS only stores vectors, not the text itself.
   df = pd.read_csv(f'{DATA_PATH}/passages.csv')  
   model = SentenceTransformer(MODEL_NAME)  
   # userquery
   query_text = "What is the capital of France?"  
   print(f"\nQuery: '{query_text}'")  
   # Encode and Search
   query_vector = model.encode([query_text])  
   D, I = index.search(query_vector, k=3) # Search for top 3 results

   print("\n--- Results ---")  
   for rank, idx in enumerate(I[0]):  
       # LIMITATION: If we wanted to filter by "text_length > 50",
       # we would have to fetch ALL results first, then filter in Python.
       # FAISS cannot filter during search.
       text = df.iloc[idx]['text'] # Manual lookup
       score = D[0][rank]  
       print(f"[{rank+1}] ID: {idx} | Score: {score:.4f}")  
       print(f"     Text: {text[:100]}...")  

if __name__ == "__main__":  
    search_faiss()

迁移步骤

从 FAISS 导出向量

前面步骤已经有 embeddings.npy 了,直接加载 numpy 数组就行,省去了导出环节。

本地启动 Qdrant 很简单:

 docker run -p6333:6333 qdrant/qdrant

Collection 配置文档:https://qdrant.tech/documentation/concepts/collections/

 from qdrant_client import QdrantClient  
from qdrant_client.models import VectorParams, Distance, HnswConfigDiff  

QDRANT_URL = "http://localhost:6333"  
COLLECTION_NAME = "ms_marco_passages"  

def create_collection():  
   client = QdrantClient(url=QDRANT_URL)  
   print(f"Creating collection '{COLLECTION_NAME}'...")  

   client.recreate_collection(  
       collection_name=COLLECTION_NAME,  
       vectors_config=VectorParams(  
           size=384,# Dimension (MiniLM)- we should follow the existing dimension from FAISS
           distance=Distance.COSINE  
       ),  
       hnsw_config=HnswConfigDiff(  
           m=16,                 # Links per node (default is 16)
           ef_construct=100      # Search depth during build (default is 100)
       )  
   )  

   print(f"Collection '{COLLECTION_NAME}' created with HNSW config.")  

if __name__ == "__main__":  
    create_collection()

批量上传数据

Qdrant Python 客户端文档:https://qdrant.tech/documentation/clients/python/

 import pandas as pd  
import numpy as np  
from qdrant_client import QdrantClient  
from qdrant_client.models import PointStruct  

QDRANT_URL = "http://localhost:6333"  
COLLECTION_NAME = "ms_marco_passages"  
DATA_PATH = '../data'  
BATCH_SIZE = 500  

def upload_data():  
   client = QdrantClient(url=QDRANT_URL)  
   print("Loading local data...")  
   embeddings = np.load(f'{DATA_PATH}/embeddings.npy')  
   df_meta = pd.read_csv(f'{DATA_PATH}/passages.csv')  
   total = len(df_meta)  
   print(f"Starting upload of {total} vectors...")  
   points_batch = []  

   for i, row in df_meta.iterrows():  
       # Metadata to attach
       payload = {  
           "passage_id": int(row['id']),  
           "text": row['text'],  
           "text_length": len(str(row['text'])),  
           "dataset_source": "msmarco_passages"  
       }  
       points_batch.append(PointStruct(  
           id=int(row['id']),  
           vector=embeddings[i].tolist(),  
           payload=payload  
       ))  
       # Upload batch
       if len(points_batch) >= BATCH_SIZE or i == total - 1:  
           client.upsert(  
               collection_name=COLLECTION_NAME,  
               points=points_batch  
           )  
           points_batch = []  
           if i % 1000 == 0:  
               print(f"  Processed {i}/{total}...")     
   print("Upload Complete.")  

if __name__ == "__main__":  
    upload_data()

验证迁移结果

 from qdrant_client import QdrantClient  
from qdrant_client.models import Filter, FieldCondition, Range, MatchValue  
from sentence_transformers import SentenceTransformer  

QDRANT_URL = "http://localhost:6333"  
COLLECTION_NAME = "ms_marco_passages"  
MODEL_NAME = 'all-MiniLM-L6-v2'  

def validate_migration():  
   client = QdrantClient(url=QDRANT_URL)  
   model = SentenceTransformer(MODEL_NAME)  
   # Verify total count
   count_result = client.count(COLLECTION_NAME)  
   print(f"Total Vectors in Qdrant: {count_result.count}")  

   # Query example
   query_text = "What is a GPU?"  
   print(f"\n--- Query: '{query_text}' ---")  
   query_vector = model.encode(query_text).tolist()  

   # Filter Definition
   print("Applying filters (Length < 200 AND Source == msmarco)...")  
   search_filter = Filter(  
       must=[  
           FieldCondition(  
               key="text_length",  
               range=Range(lt=200)  # can be changed as per the requirement
           ),  
           FieldCondition(  
               key="dataset_source",  
               match=MatchValue(value="msmarco_passages")  
           )  
       ]  
   )  

   results = client.query_points(  
       collection_name=COLLECTION_NAME,  
       query=query_vector,        
       query_filter=search_filter,  
       limit=3  
   ).points  

   for hit in results:  
       print(f"\nID: {hit.id} (Score: {hit.score:.3f})")  
       print(f"Text: {hit.payload['text']}")  
       print(f"Metadata: {hit.payload}")  

if __name__ == "__main__":  
    validate_migration()

性能对比

针对10个常见查询做了对比测试。

FAISS(本地 CPU):约 0.5ms,纯数学计算的速度

Qdrant(Docker):约 3ms,包含了网络传输的开销

对 Web 服务来说3ms 的延迟完全可以接受,何况换来的是一堆新功能。

 import time  
import faiss  
import numpy as np  
from qdrant_client import QdrantClient  
from sentence_transformers import SentenceTransformer  

FAISS_INDEX_PATH = './faiss_index/my_index.faiss'  
QDRANT_URL = "http://localhost:6333"  
COLLECTION_NAME = "ms_marco_passages"  
MODEL_NAME = 'all-MiniLM-L6-v2'  

QUERIES = [  
   "What is a GPU?",  
   "Who is the president of France?",  
   "How to bake a cake?",  
   "Symptoms of the flu",  
   "Python programming language",  
   "Best places to visit in Italy",  
   "Define quantum mechanics",  
   "History of the Roman Empire",  
   "What is machine learning?",  
   "Healthy breakfast ideas"  
]  

def run_comparison():  
   print("---Loading Resources ---")  
   # Load Model
   model = SentenceTransformer(MODEL_NAME)  
   # Load FAISS (The "Old Way")
   print("Loading FAISS index...")  
   faiss_index = faiss.read_index(FAISS_INDEX_PATH)  
   # Connect to Qdrant (The "New Way")
   print("Connecting to Qdrant...")  
   client = QdrantClient(url=QDRANT_URL)  
   print(f"\n---Running Race ({len(QUERIES)} queries) ---")  
   print(f"{'Query':<30} | {'FAISS (ms)':<10} | {'Qdrant (ms)':<10}")  
   print("-" * 60)  

   faiss_times = []  
   qdrant_times = []  

   for query_text in QUERIES:  
       # Encode once
       query_vector = model.encode(query_text).tolist()  
       # --- MEASURE FAISS ---
       start_f = time.perf_counter()  
       # FAISS expects a numpy array of shape (1, d)
       faiss_input = np.array([query_vector], dtype='float32')  
       _, _ = faiss_index.search(faiss_input, k=3)  
       end_f = time.perf_counter()  
       faiss_ms = (end_f - start_f) * 1000  
       faiss_times.append(faiss_ms)  
       # --- MEASURE QDRANT ---
       start_q = time.perf_counter()  
       _ = client.query_points(  
           collection_name=COLLECTION_NAME,  
           query=query_vector,  
           limit=3  
       )  
       end_q = time.perf_counter()  
       qdrant_ms = (end_q - start_q) * 1000  
       qdrant_times.append(qdrant_ms)  
       print(f"{query_text[:30]:<30} | {faiss_ms:>10.2f} | {qdrant_ms:>10.2f}")  

   print("-" * 60)  
   print(f"{'AVERAGE':<30} | {np.mean(faiss_times):>10.2f} | {np.mean(qdrant_times):>10.2f}")  

if __name__ == "__main__":  
    run_comparison()

测试结果:

最大的差异不在速度,在于省心。

用 FAISS 时有次跑了个索引脚本处理大批数据,耗时40分钟,占了12GB内存。快完成时 SSH 连接突然断了,进程被杀,因为 FAISS 只是个跑在内存里的库一切都白费了。

换成 Qdrant 就不一样了:它像真正的数据库,数据推送后会持久化保存,即便突然断开 docker 连接重启后数据还在。

用过 FAISS 就知道为了把向量 ID 映射回文本,还需要额外维护一个 CSV 文件。迁移到 Qdrant 后这些查找逻辑都删掉了,文本和向量存在一起,直接查询 API 就能拿到完整结果,不再需要管理各种文件,就是在用一个微服务。

迁移总结

这次迁移断断续续做了一周但收获很大。最爽的不是写 Qdrant 脚本,是删掉旧代码——提交的 PR 几乎全是红色删除行。CSV 加载工具、手动 ID 映射、各种"代码"全删了,代码量减少了30%,可读性明显提升。

只用 FAISS 时,搜索有时像在碰运气——语义上相似但事实错误的结果时常出现。迁移到 Qdrant拿到的不只是数据库,更是对系统的掌控力。稠密向量配合关键词过滤(混合搜索),终于能回答"显示 GPU 相关的技术文档,但只要官方手册里的"这种精确查询,这在之前根本做不到。

信心的变化最明显,以前不敢加载完整的880万数据怕内存撑不住。现在架构解耦了可以把全部数据推给 Qdrant,它会在磁盘上处理存储和索引,应用层保持轻量。终于有了个在生产环境和 notebook 里都能跑得一样好的系统。

总结

FAISS 适合离线研究和快速实验,但要在生产环境跑起来Qdrant 提供了必需的基础设施。如果还在用额外的 CSV 文件来理解向量含义该考虑迁移了。

https://avoid.overfit.cn/post/ce7c45d8373741f6b8af465bb06bc398

作者:Sai Bhargav Rallapalli

目录
相关文章
|
26天前
|
存储 人工智能 缓存
阿里云对象存储OSS按量使用达标返券活动,消费达标报名即返等额代金券,用50元返50元
阿里云对象存储OSS推出专属代金券了,开通按量付费,消费达标报名即返等额代金券,用多少返多少。按小时使用对象存储 OSS 达到门槛,返对应代金券,满10元返10元,满50元返50元,灵活省钱又高效。适合互联网多媒体、大数据分析、AI 应用、数据归档等场景。
|
26天前
|
存储 弹性计算 人工智能
阿里云免费云服务器领取教程及阿里云免费云产品全解析:从资源配置到实用指南
在云计算普及的当下,阿里云作为国内领先的云服务提供商,长期推出免费云产品试用体系,覆盖从基础设施到上层应用的全场景需求,为个人开发者、学生及初创企业降低了上云门槛。然而,免费资源背后往往存在配置限制、合规要求等细节问题,用户需结合自身场景理性选择。本文基于阿里云官方规则与真实使用反馈,系统梳理免费云产品的资源矩阵、使用体验、常见风险及适配场景,为不同需求的用户提供全面参考。
|
23天前
|
人工智能 自然语言处理 安全
Lux 上手指南:让 AI 直接操作你的电脑
Lux 是一款能直接操作计算机的AI基础模型,通过视觉理解与动作预测,实现自然语言指令下的自动化任务。它无需依赖API,可像真人一样点击、输入、滚动,完成浏览器操作等复杂工作,准确率超越主流模型,是迈向“意图即执行”的重要突破。(238字)
239 13
Lux 上手指南:让 AI 直接操作你的电脑
|
12天前
|
前端开发 算法
深度研究Agent架构解析:4种Agent架构介绍及实用Prompt模板
本文系统梳理了深度搜索Agent的主流架构演进:从基础的Planner-Only,到引入评估反馈的双模块设计,再到支持层次化分解的递归式ROMA方案。重点解析了问题拆解与终止判断两大核心挑战,并提供了实用的Prompt模板与优化策略,为构建高效搜索Agent提供清晰路径。
312 10
深度研究Agent架构解析:4种Agent架构介绍及实用Prompt模板
|
26天前
|
缓存 监控 调度
阿里云 CDN 计费体系全解析:基础服务、增值服务与选型指南
在内容分发网络(CDN)领域,阿里云凭借全球节点覆盖与灵活的计费模式,成为个人开发者与企业用户的重要选择。其 CDN 服务计费分为基础服务与增值服务两部分,基础服务提供按流量计费、按带宽峰值计费、月结 95 带宽峰值计费三种核心模式,增值服务则根据功能使用情况单独计费。本文基于 今年最新官方文档与实测数据,从计费规则、价格详情、场景适配、成本优化等维度展开解析,为用户提供客观选型参考。
|
26天前
|
弹性计算 人工智能 安全
阿里云ECS服务器Alibaba cloud linux镜像系统版本有什么区别?操作系统选择方法
Alibaba Cloud Linux是阿里云推出的云原生操作系统,兼容CentOS生态,专为ECS实例优化。当前主要版本有2、3、4代,其中Alibaba Cloud Linux 3和4基于Anolis OS,分别使用5.10和6.6内核,支持dnf、Podman及OS Copilot智能助手。版本类型包括LTS(长期支持)、Pro(商业付费版,提供13年维护)、AI增强版、容器优化版、等保合规版、UEFI启动版及ARM架构版。建议优先选用Alibaba Cloud Linux 3或4,根据应用需求选择对应版本以获得更优性能与稳定性支持。
|
26天前
|
存储 人工智能 数据可视化
阿里云万小智 AI 建站系统全解析:版本功能、价格体系与场景适配
在企业数字化转型与个人建站需求增长的背景下,阿里云基于通义大模型推出的万小智 AI 建站系统,以 “低门槛、高效率、全集成” 为核心优势,成为无需专业开发能力用户的重要选择。该系统分为基础版、标准版、企业版三个层级,覆盖从个人博客到中大型企业官网的全场景需求,且购买即赠 CN 域名,进一步降低建站成本。本文结合今年最新官方文档与实测体验,从核心特性、版本差异、价格规则、场景适配等维度展开解析,为用户提供客观选型参考。
|
1月前
|
监控 Java C语言
揭开 Java 容器“消失的内存”之谜:云监控 2.0 SysOM 诊断实践
本文介绍云原生环境下Java应用内存超限问题的诊断与治理,聚焦容器化后常见的JVM堆外内存、JNI内存泄漏、LIBC分配器特性及Linux透明大页等导致OOM的根源,结合阿里云SysOM系统诊断工具,通过真实案例详解如何实现从应用到系统的全链路内存分析,精准定位“消失的内存”,提升资源利用率与稳定性。
150 19
|
8天前
|
存储 人工智能 数据库
Agentic Memory 实践:用 agents.md 实现 LLM 持续学习
利用 agents.md 文件实现LLM持续学习,让AI Agent记住你的编程习惯、偏好和常用信息,避免重复指令,显著提升效率。每次交互后自动归纳经验,减少冷启动成本,跨工具通用,是高效工程师的必备技能。
101 17
Agentic Memory 实践:用 agents.md 实现 LLM 持续学习