import json import os import numpy as np from pymilvus import connections, MilvusClient, FieldSchema, CollectionSchema, DataType, Collection, AnnSearchRequest, RRFRanker from pymilvus.model.hybrid import BGEM3EmbeddingFunction # 1. 初始化设置 COLLECTION_NAME = "dragon_hybrid_demo" MILVUS_URI = "http://localhost:19530" # 服务器模式 DATA_PATH = "../../data/C4/metadata/dragon.json" # 相对路径 BATCH_SIZE = 50 # 2. 连接 Milvus 并初始化嵌入模型 print(f"--> 正在连接到 Milvus: {MILVUS_URI}") connections.connect(uri=MILVUS_URI) print("--> 正在初始化 BGE-M3 嵌入模型...") ef = BGEM3EmbeddingFunction(use_fp16=False, device="cpu") print(f"--> 嵌入模型初始化完成。密集向量维度: {ef.dim['dense']}") # 3. 创建 Collection milvus_client = MilvusClient(uri=MILVUS_URI) if milvus_client.has_collection(COLLECTION_NAME): print(f"--> 正在删除已存在的 Collection '{COLLECTION_NAME}'...") milvus_client.drop_collection(COLLECTION_NAME) fields = [ FieldSchema(name="pk", dtype=DataType.VARCHAR, is_primary=True, auto_id=True, max_length=100), FieldSchema(name="img_id", dtype=DataType.VARCHAR, max_length=100), FieldSchema(name="path", dtype=DataType.VARCHAR, max_length=256), FieldSchema(name="title", dtype=DataType.VARCHAR, max_length=256), FieldSchema(name="description", dtype=DataType.VARCHAR, max_length=4096), FieldSchema(name="category", dtype=DataType.VARCHAR, max_length=64), FieldSchema(name="location", dtype=DataType.VARCHAR, max_length=128), FieldSchema(name="environment", dtype=DataType.VARCHAR, max_length=64), FieldSchema(name="sparse_vector", dtype=DataType.SPARSE_FLOAT_VECTOR), FieldSchema(name="dense_vector", dtype=DataType.FLOAT_VECTOR, dim=ef.dim["dense"]) ] # 如果集合不存在,则创建它及索引 if not milvus_client.has_collection(COLLECTION_NAME): print(f"--> 正在创建 Collection '{COLLECTION_NAME}'...") schema = CollectionSchema(fields, description="关于龙的混合检索示例") # 创建集合 collection = Collection(name=COLLECTION_NAME, schema=schema, consistency_level="Strong") print("--> Collection 创建成功。") # 4. 创建索引 print("--> 正在为新集合创建索引...") sparse_index = {"index_type": "SPARSE_INVERTED_INDEX", "metric_type": "IP"} collection.create_index("sparse_vector", sparse_index) print("稀疏向量索引创建成功。") dense_index = {"index_type": "AUTOINDEX", "metric_type": "IP"} collection.create_index("dense_vector", dense_index) print("密集向量索引创建成功。") collection = Collection(COLLECTION_NAME) # 5. 加载数据并插入 collection.load() print(f"--> Collection '{COLLECTION_NAME}' 已加载到内存。") if collection.is_empty: print(f"--> Collection 为空,开始插入数据...") if not os.path.exists(DATA_PATH): raise FileNotFoundError(f"数据文件未找到: {DATA_PATH}") with open(DATA_PATH, 'r', encoding='utf-8') as f: dataset = json.load(f) docs, metadata = [], [] for item in dataset: parts = [ item.get('title', ''), item.get('description', ''), item.get('location', ''), item.get('environment', ''), # *item.get('combat_details', {}).get('combat_style', []), # *item.get('combat_details', {}).get('abilities_used', []), # item.get('scene_info', {}).get('time_of_day', '') ] docs.append(' '.join(filter(None, parts))) metadata.append(item) print(f"--> 数据加载完成,共 {len(docs)} 条。") print("--> 正在生成向量嵌入...") embeddings = ef(docs) print("--> 向量生成完成。") print("--> 正在分批插入数据...") # 为每个字段准备批量数据 img_ids = [doc["img_id"] for doc in metadata] paths = [doc["path"] for doc in metadata] titles = [doc["title"] for doc in metadata] descriptions = [doc["description"] for doc in metadata] categories = [doc["category"] for doc in metadata] locations = [doc["location"] for doc in metadata] environments = [doc["environment"] for doc in metadata] # 获取向量 sparse_vectors = embeddings["sparse"] dense_vectors = embeddings["dense"] # 插入数据 collection.insert([ img_ids, paths, titles, descriptions, categories, locations, environments, sparse_vectors, dense_vectors ]) collection.flush() print(f"--> 数据插入完成,总数: {collection.num_entities}") else: print(f"--> Collection 中已有 {collection.num_entities} 条数据,跳过插入。") # 6. 执行搜索 search_query = "悬崖上的巨龙" search_filter = 'category in ["western_dragon", "chinese_dragon", "movie_character"]' top_k = 5 print(f"\n{'='*20} 开始混合搜索 {'='*20}") print(f"查询: '{search_query}'") print(f"过滤器: '{search_filter}'") query_embeddings = ef([search_query]) dense_vec = query_embeddings["dense"][0] sparse_vec = query_embeddings["sparse"]._getrow(0) # 打印向量信息 print("\n=== 向量信息 ===") print(f"密集向量维度: {len(dense_vec)}") print(f"密集向量前5个元素: {dense_vec[:5]}") print(f"密集向量范数: {np.linalg.norm(dense_vec):.4f}") print(f"\n稀疏向量维度: {sparse_vec.shape[1]}") print(f"稀疏向量非零元素数量: {sparse_vec.nnz}") print("稀疏向量前5个非零元素:") for i in range(min(5, sparse_vec.nnz)): print(f" - 索引: {sparse_vec.indices[i]}, 值: {sparse_vec.data[i]:.4f}") density = (sparse_vec.nnz / sparse_vec.shape[1] * 100) print(f"\n稀疏向量密度: {density:.8f}%") # 定义搜索参数 search_params = {"metric_type": "IP", "params": {}} # 先执行单独的搜索 print("\n--- [单独] 密集向量搜索结果 ---") dense_results = collection.search( [dense_vec], anns_field="dense_vector", param=search_params, limit=top_k, expr=search_filter, output_fields=["title", "path", "description", "category", "location", "environment"] )[0] for i, hit in enumerate(dense_results): print(f"{i+1}. {hit.entity.get('title')} (Score: {hit.distance:.4f})") print(f" 路径: {hit.entity.get('path')}") print(f" 描述: {hit.entity.get('description')[:100]}...") print("\n--- [单独] 稀疏向量搜索结果 ---") sparse_results = collection.search( [sparse_vec], anns_field="sparse_vector", param=search_params, limit=top_k, expr=search_filter, output_fields=["title", "path", "description", "category", "location", "environment"] )[0] for i, hit in enumerate(sparse_results): print(f"{i+1}. {hit.entity.get('title')} (Score: {hit.distance:.4f})") print(f" 路径: {hit.entity.get('path')}") print(f" 描述: {hit.entity.get('description')[:100]}...") print("\n--- [混合] 稀疏+密集向量搜索结果 ---") # 创建 RRF 融合器 rerank = RRFRanker(k=60) # 创建搜索请求 dense_req = AnnSearchRequest([dense_vec], "dense_vector", search_params, limit=top_k) sparse_req = AnnSearchRequest([sparse_vec], "sparse_vector", search_params, limit=top_k) # 执行混合搜索 results = collection.hybrid_search( [sparse_req, dense_req], rerank=rerank, limit=top_k, output_fields=["title", "path", "description", "category", "location", "environment"] )[0] # 打印最终结果 for i, hit in enumerate(results): print(f"{i+1}. {hit.entity.get('title')} (Score: {hit.distance:.4f})") print(f" 路径: {hit.entity.get('path')}") print(f" 描述: {hit.entity.get('description')[:100]}...") # 7. 清理资源 milvus_client.release_collection(collection_name=COLLECTION_NAME) print(f"已从内存中释放 Collection: '{COLLECTION_NAME}'") milvus_client.drop_collection(COLLECTION_NAME) print(f"已删除 Collection: '{COLLECTION_NAME}'")