milvus_api.py 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349
  1. from enum import Enum
  2. from pymilvus import MilvusClient, DataType
  3. import os
  4. from dotenv import load_dotenv
  5. from logger.logging import setup_logging
  6. from pprint import pprint
  7. from dataclasses import dataclass, asdict
  8. load_dotenv()
  9. logger = setup_logging()
  10. @dataclass
  11. class MilvusDocument:
  12. doc_name: str # 文档名称
  13. doc_path_name: str # 文档路径(含名字)
  14. doc_type: str # 文档类型
  15. doc_md5: str # 文档MD5
  16. doc_length: int # 文档字节数
  17. content: str # 文档分段内容
  18. content_vector: list # 分段内容向量
  19. embedding_model: str # embedding模型名称
  20. class MilvusDocumentField(str, Enum):
  21. ID = "id" # 主键ID
  22. DOC_NAME = "doc_name" # 文档名称
  23. DOC_PATH_NAME = "doc_path_name" # 文档路径(含名字)
  24. DOC_TYPE = "doc_type" # 文档类型
  25. DOC_MD5 = "doc_md5" # 文档MD5
  26. DOC_LENGTH = "doc_length" # 文档字节数
  27. CONTENT = "content" # 文档分段内容
  28. CONTENT_VECTOR = "content_vector" # 分段内容向量
  29. EMBEDDING_MODEL = "embedding_model" # embedding模型名称
  30. class MilvusAPI:
  31. def __init__(self):
  32. """初始化Milvus客户端连接"""
  33. self.database_name = os.getenv("MILVUS_DATABASE")
  34. self.collection_name = os.getenv("MILVUS_COLLECTION")
  35. self.index_name = os.getenv("MILVUS_INDEX_NAME")
  36. self.vector_dim = int(os.getenv("MILVUS_VECTOR_DIM"))
  37. if not self.database_name or not self.collection_name or not self.index_name or not self.vector_dim:
  38. logger.error("Milvus配置错误")
  39. raise ValueError("Milvus配置错误")
  40. # 创建客户端连接,指定数据库
  41. self.client = MilvusClient(
  42. uri=f"http://{os.getenv('MILVUS_HOST')}:{os.getenv('MILVUS_PORT')}",
  43. user=os.getenv('MILVUS_USER'),
  44. password=os.getenv('MILVUS_PASSWORD'),
  45. db_name=self.database_name
  46. )
  47. init_result = self._initialize()
  48. if not init_result:
  49. logger.error("Milvus初始化失败")
  50. raise ValueError("Milvus初始化失败")
  51. def _create_collection_if_not_exists(self) -> bool:
  52. """创建集合(如果不存在则创建)"""
  53. try:
  54. # 检查集合是否存在
  55. if self.client.has_collection(collection_name=self.collection_name):
  56. logger.info(f"集合 '{self.collection_name}' 已存在")
  57. return True
  58. # 定义schema
  59. schema = self.client.create_schema(
  60. auto_id=True, # 自动生成ID
  61. enable_dynamic_field=False
  62. )
  63. # 添加字段
  64. # 主键ID字段(自动生成)
  65. schema.add_field(
  66. field_name=MilvusDocumentField.ID.value,
  67. datatype=DataType.INT64,
  68. is_primary=True,
  69. auto_id=True
  70. )
  71. # 文档名称
  72. schema.add_field(
  73. field_name=MilvusDocumentField.DOC_NAME.value,
  74. datatype=DataType.VARCHAR,
  75. max_length=500
  76. )
  77. # 文档路径(含名字)
  78. schema.add_field(
  79. field_name=MilvusDocumentField.DOC_PATH_NAME.value,
  80. datatype=DataType.VARCHAR,
  81. max_length=1000
  82. )
  83. # 文档类型
  84. schema.add_field(
  85. field_name=MilvusDocumentField.DOC_TYPE.value,
  86. datatype=DataType.VARCHAR,
  87. max_length=50
  88. )
  89. # 文档MD5
  90. schema.add_field(
  91. field_name=MilvusDocumentField.DOC_MD5.value,
  92. datatype=DataType.VARCHAR,
  93. max_length=32
  94. )
  95. # 文档字节数
  96. schema.add_field(
  97. field_name=MilvusDocumentField.DOC_LENGTH.value,
  98. datatype=DataType.INT64
  99. )
  100. # 文档内容
  101. schema.add_field(
  102. field_name=MilvusDocumentField.CONTENT.value,
  103. datatype=DataType.VARCHAR,
  104. max_length=65535 # 最大长度
  105. )
  106. # 内容向量(text-embedding-v4的维度是1536)
  107. schema.add_field(
  108. field_name=MilvusDocumentField.CONTENT_VECTOR.value,
  109. datatype=DataType.FLOAT_VECTOR,
  110. dim=self.vector_dim
  111. )
  112. # embedding模型名称
  113. schema.add_field(
  114. field_name=MilvusDocumentField.EMBEDDING_MODEL.value,
  115. datatype=DataType.VARCHAR,
  116. max_length=100
  117. )
  118. # 创建集合
  119. self.client.create_collection(
  120. collection_name=self.collection_name,
  121. schema=schema
  122. )
  123. logger.info(f"集合 '{self.collection_name}' 创建成功")
  124. return True
  125. except Exception as e:
  126. logger.error(f"创建集合失败: {e}")
  127. return False
  128. def _create_index_if_not_exists(self) -> bool:
  129. """创建索引"""
  130. try:
  131. # 检查索引是否已存在
  132. indexes = self.client.list_indexes(collection_name=self.collection_name)
  133. if self.index_name in indexes:
  134. logger.info(f"索引 '{self.index_name}' 已存在")
  135. return True
  136. # 创建向量索引
  137. index_params = self.client.prepare_index_params()
  138. index_params.add_index(
  139. field_name=MilvusDocumentField.CONTENT_VECTOR.value,
  140. index_type="IVF_FLAT",
  141. metric_type="COSINE",
  142. params={"nlist": 1024}
  143. )
  144. self.client.create_index(
  145. collection_name=self.collection_name,
  146. index_params=index_params
  147. )
  148. logger.info(f"索引 '{self.index_name}' 创建成功")
  149. return True
  150. except Exception as e:
  151. logger.error(f"创建索引失败: {e}")
  152. return False
  153. def _load_collection(self) -> bool:
  154. """加载集合到内存"""
  155. try:
  156. self.client.load_collection(collection_name=self.collection_name)
  157. logger.info(f"集合 '{self.collection_name}' 加载成功")
  158. return True
  159. except Exception as e:
  160. logger.error(f"加载集合失败: {e}")
  161. return False
  162. def _initialize(self) -> bool:
  163. """初始化数据库、集合和索引"""
  164. logger.info("开始初始化Milvus...")
  165. # 创建集合
  166. if not self._create_collection_if_not_exists():
  167. return False
  168. # 创建索引
  169. if not self._create_index_if_not_exists():
  170. return False
  171. # 加载集合到内存
  172. if not self._load_collection():
  173. return False
  174. logger.info("Milvus初始化完成!")
  175. return True
  176. def check_document_exists(self, doc_path_name: str) -> bool:
  177. """
  178. 检查文档是否已存在
  179. Args:
  180. doc_path_name (str): 文档路径
  181. Returns:
  182. bool: 文档是否已存在
  183. """
  184. try:
  185. # 先确保集合已加载
  186. self._load_collection()
  187. # 根据路径查询
  188. filter_expr = f'doc_path_name == "{doc_path_name}"'
  189. results = self.client.query(
  190. collection_name=self.collection_name,
  191. filter=filter_expr,
  192. output_fields=[MilvusDocumentField.ID.value],
  193. limit=1
  194. )
  195. return len(results) > 0
  196. except Exception as e:
  197. logger.error(f"检查文档是否存在失败: {e}")
  198. raise e
  199. def delete_existing_document(self, doc_path_name: str) -> bool:
  200. """
  201. 删除已存在的文档记录
  202. Args:
  203. doc_path_name (str): 文档路径
  204. Returns:
  205. bool: 删除是否成功
  206. """
  207. try:
  208. # 安全检查:确保doc_path_name不为空,避免删除所有文档
  209. if not doc_path_name or not doc_path_name.strip():
  210. logger.error("错误: 文档路径名不能为空,拒绝执行删除操作")
  211. return False
  212. # 构建删除表达式
  213. delete_expr = f'doc_path_name == "{doc_path_name}"'
  214. # 执行删除操作
  215. result = self.client.delete(
  216. collection_name=self.collection_name,
  217. filter=delete_expr
  218. )
  219. logger.info(f"删除已存在的文档记录: {doc_path_name}")
  220. return True
  221. except Exception as e:
  222. logger.error(f"删除已存在文档失败: {e}")
  223. raise e
  224. def insert_document(self, doc_data: MilvusDocument) -> bool:
  225. """插入文档数据"""
  226. try:
  227. self.client.insert(
  228. collection_name=self.collection_name,
  229. data= asdict(doc_data)
  230. )
  231. logger.info(f"文档 '{doc_data.doc_name}' 插入成功")
  232. return True
  233. except Exception as e:
  234. logger.error(f"插入文档失败: {e}")
  235. return False
  236. def flush_collection(self) -> bool:
  237. """刷新集合"""
  238. try:
  239. self.client.flush(collection_name=self.collection_name)
  240. logger.info(f"集合 '{self.collection_name}' 刷新成功")
  241. return True
  242. except Exception as e:
  243. logger.error(f"刷新集合失败: {e}")
  244. return False
  245. def search_similar_documents(self, query_vector, limit=10):
  246. """搜索相似文档
  247. Args:
  248. query_vector (list): 查询向量
  249. limit (int): 返回结果数量限制
  250. Returns:
  251. list: 搜索结果
  252. """
  253. try:
  254. search_params = {
  255. "metric_type": "COSINE",
  256. "params": {"nprobe": 64}
  257. }
  258. results = self.client.search(
  259. collection_name=self.collection_name,
  260. data=[query_vector],
  261. anns_field=MilvusDocumentField.CONTENT_VECTOR.value,
  262. search_params=search_params,
  263. limit=limit,
  264. output_fields=[MilvusDocumentField.DOC_NAME.value, MilvusDocumentField.DOC_PATH_NAME.value, MilvusDocumentField.DOC_TYPE.value, MilvusDocumentField.CONTENT.value, MilvusDocumentField.EMBEDDING_MODEL.value]
  265. )
  266. return results[0] if results else []
  267. except Exception as e:
  268. logger.error(f"搜索失败: {e}")
  269. return []
  270. def get_collection_info(self):
  271. """获取集合信息"""
  272. try:
  273. info = self.client.describe_collection(collection_name=self.collection_name)
  274. return info
  275. except Exception as e:
  276. logger.error(f"获取集合信息失败: {e}")
  277. return None
  278. # 使用示例
  279. if __name__ == "__main__":
  280. # 创建MilvusAPI实例
  281. milvus_api = MilvusAPI()
  282. # 获取集合信息
  283. info = milvus_api.get_collection_info()
  284. if info:
  285. pprint("集合信息:")
  286. pprint(info)