minio_event_handler.py 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384
  1. import json
  2. from datetime import datetime
  3. from typing import Dict, Any
  4. import os
  5. from dotenv import load_dotenv
  6. from minio import Minio
  7. from parser.simple_object_parser import SimpleObjectParser
  8. from embedding import EmbeddingTool
  9. from milvus_api import MilvusAPI, MilvusDocument
  10. from logger.logging import setup_logging
  11. load_dotenv()
  12. logger = setup_logging()
  13. # Minio 配置信息。
  14. MINIO_BUCKET = os.getenv('MINIO_BUCKET')
  15. MINIO_ENDPOINT = os.getenv('MINIO_ENDPOINT')
  16. MINIO_ACCESS_KEY = os.getenv('MINIO_ACCESS_KEY')
  17. MINIO_SECRET_KEY = os.getenv('MINIO_SECRET_KEY')
  18. MINIO_REGION = os.getenv('MINIO_REGION')
  19. MINIO_USE_VIRTUAL_HOST = os.getenv('MINIO_USE_VIRTUAL_HOST', 'false').lower() == 'true'
  20. MINIO_USE_SSL = os.getenv('MINIO_USE_SSL', 'false').lower() == 'true'
  21. ## 获取MinIO客户端
  22. def _get_minio_client() -> Minio:
  23. client = Minio(
  24. endpoint=MINIO_ENDPOINT,
  25. access_key=MINIO_ACCESS_KEY,
  26. secret_key=MINIO_SECRET_KEY,
  27. secure=MINIO_USE_SSL,
  28. region=MINIO_REGION,
  29. )
  30. if MINIO_USE_VIRTUAL_HOST:
  31. client.enable_virtual_style_endpoint()
  32. return client
  33. class MinioEventHandler:
  34. """Minio事件监听器"""
  35. def __init__(self, bucket_name: str = None):
  36. """
  37. 初始化监听器
  38. Args:
  39. bucket_name (str): 要监听的桶名称,默认从环境变量获取
  40. """
  41. self.bucket_name = bucket_name or os.getenv("MINIO_BUCKET", "mildoc")
  42. # 初始化各个组件
  43. self.minio_client = _get_minio_client()
  44. # 初始化解析器
  45. logger.info("初始化解析器...")
  46. self.parser: SimpleObjectParser = SimpleObjectParser(minio_client=self.minio_client)
  47. # 初始化Milvus
  48. logger.info("初始化Milvus...")
  49. self.milvus_api: MilvusAPI = MilvusAPI()
  50. # 测试embedding工具
  51. logger.info("测试embedding工具...")
  52. self.embedding_tool: EmbeddingTool = EmbeddingTool()
  53. logger.info("所有组件初始化完成!")
  54. def _extract_event_info(self, event_data: Dict[str, Any]) -> Dict[str, Any]:
  55. """
  56. 从事件数据中提取关键信息
  57. Args:
  58. event_data (Dict[str, Any]): 事件数据
  59. Returns:
  60. Dict[str, Any]: 提取的信息
  61. """
  62. try:
  63. #logger.info(f"event_data: {event_data}")
  64. logger.info(f"数据: {json.dumps(event_data, ensure_ascii=False, indent=2)}")
  65. record = event_data.get('Records', [{}])[0]
  66. s3_info = record.get('s3', {})
  67. return {
  68. 'event_name': record.get('eventName', ''),
  69. 'event_time': record.get('eventTime', ''),
  70. 'bucket_name': s3_info.get('bucket', {}).get('name', ''),
  71. 'object_name': s3_info.get('object', {}).get('key', ''),
  72. 'object_size': s3_info.get('object', {}).get('size', 0),
  73. 'content_type': s3_info.get('object', {}).get('contentType', ''),
  74. 'etag': s3_info.get('object', {}).get('eTag', ''),
  75. }
  76. except Exception as e:
  77. logger.error(f"提取事件信息失败: {e}")
  78. return {}
  79. def _handle_object_created(self, event_info: Dict[str, Any]):
  80. """
  81. 处理对象创建事件
  82. Args:
  83. event_info (Dict[str, Any]): 事件信息
  84. """
  85. try:
  86. bucket_name = event_info['bucket_name']
  87. object_name = event_info['object_name']
  88. logger.info(f"\n=== 处理新增对象: {bucket_name}/{object_name} ===")
  89. logger.info(f"对象大小: {event_info['object_size']} 字节")
  90. logger.info(f"内容类型: {event_info['content_type']}")
  91. # 直接调用_process_single_object方法处理
  92. self._process_single_object(bucket_name, object_name, force_update=True)
  93. except Exception as e:
  94. logger.error(f"处理对象创建事件失败: {e}")
  95. def _handle_object_deleted(self, event_info: Dict[str, Any]):
  96. """
  97. 处理对象删除事件
  98. Args:
  99. event_info (Dict[str, Any]): 事件信息
  100. """
  101. try:
  102. bucket_name = event_info['bucket_name']
  103. object_name = event_info['object_name']
  104. doc_path_name = object_name # 不再包含bucket_name前缀
  105. logger.info(f"\n=== 处理删除对象: {bucket_name}/{object_name} ===")
  106. # 从Milvus中删除相关记录
  107. logger.info("从Milvus中查找并删除相关记录...")
  108. # 使用MilvusAPI的删除方法
  109. if self.milvus_api.delete_existing_document(doc_path_name):
  110. logger.info(f"成功删除文档记录: {doc_path_name}")
  111. else:
  112. logger.error(f"删除文档记录失败: {doc_path_name}")
  113. except Exception as e:
  114. logger.error(f"处理对象删除事件失败: {e}")
  115. def _process_event(self, event_data: Dict[str, Any]):
  116. """
  117. 处理单个事件
  118. Args:
  119. event_data (Dict[str, Any]): 事件数据
  120. """
  121. try:
  122. # 提取事件信息
  123. event_info = self._extract_event_info(event_data)
  124. if not event_info:
  125. logger.error("无法提取事件信息,跳过处理")
  126. return
  127. event_name = event_info['event_name']
  128. timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
  129. logger.info(f"\n[{timestamp}] 收到事件: {event_name}")
  130. logger.info(f"对象: {event_info['bucket_name']}/{event_info['object_name']}")
  131. # 根据事件类型进行处理
  132. if 'ObjectCreated' in event_name:
  133. self._handle_object_created(event_info)
  134. elif 'ObjectRemoved' in event_name:
  135. self._handle_object_deleted(event_info)
  136. else:
  137. logger.error(f"未处理的事件类型: {event_name}")
  138. except Exception as e:
  139. logger.error(f"处理事件时出错: {e}")
  140. def _process_single_object(self, bucket_name: str, object_name: str, force_update: bool = False):
  141. """
  142. 处理单个对象(用于全量刷新和排查补漏)
  143. Args:
  144. bucket_name (str): 桶名称
  145. object_name (str): 对象名称
  146. force_update (bool): 是否强制更新(True=全量刷新,False=排查补漏)
  147. Returns:
  148. bool: 处理是否成功
  149. """
  150. try:
  151. doc_path_name = object_name # 不再包含bucket_name前缀
  152. # 如果是排查补漏模式,先检查是否已存在
  153. if not force_update:
  154. if self.milvus_api.check_document_exists(doc_path_name):
  155. logger.info(f" 文档已存在,跳过: {object_name}")
  156. return True
  157. logger.info(f" 处理文档: {object_name}")
  158. # 解析对象内容
  159. parse_result = self.parser.parse_object(bucket_name, object_name)
  160. if 'error' in parse_result:
  161. logger.error(f" 解析失败: {parse_result['error']}")
  162. return False
  163. if not parse_result['contents']:
  164. logger.error(f" 未提取到文本内容,跳过")
  165. return True
  166. logger.info(f" 解析成功,获得 {len(parse_result['contents'])} 个文本片段")
  167. # 如果是强制更新,先删除已存在的记录
  168. if force_update:
  169. self.milvus_api.delete_existing_document(doc_path_name)
  170. # 为每个文本片段生成embedding并存储到Milvus
  171. success_count = 0
  172. for i, content in enumerate(parse_result['contents']):
  173. try:
  174. # 生成embedding向量
  175. embedding_vector = self.embedding_tool.get_embedding(content)
  176. if not embedding_vector:
  177. logger.error(f" 片段 {i+1} embedding生成失败,跳过")
  178. continue
  179. # 准备文档数据
  180. doc_data = MilvusDocument(
  181. doc_name=parse_result['doc_name'],
  182. doc_path_name=parse_result['doc_path_name'],
  183. doc_type=parse_result['doc_type'],
  184. doc_md5=parse_result['doc_md5'],
  185. doc_length=parse_result['doc_length'],
  186. content=content,
  187. content_vector=embedding_vector,
  188. embedding_model=self.embedding_tool.model
  189. )
  190. # 存储到Milvus(允许重复,因为我们已经处理了去重逻辑)
  191. if self.milvus_api.insert_document(doc_data):
  192. success_count += 1
  193. else:
  194. logger.error(f" 片段 {i+1} 存储失败")
  195. except Exception as e:
  196. logger.error(f" 处理片段 {i+1} 时出错: {e}")
  197. continue
  198. logger.info(f" 完成!成功存储 {success_count}/{len(parse_result['contents'])} 个片段")
  199. return success_count > 0
  200. except Exception as e:
  201. logger.error(f" 处理对象失败: {e}")
  202. return False
  203. def full_update(self):
  204. """
  205. 模式1:全量刷新 - 遍历Minio桶中的所有数据并更新到Milvus
  206. """
  207. logger.info(f"\n=== 模式1:全量刷新 ===")
  208. logger.info(f"正在遍历桶 '{self.bucket_name}' 中的所有对象...")
  209. try:
  210. # 获取桶中的所有对象
  211. objects = self.minio_client.list_objects(self.bucket_name, recursive=True)
  212. total_objects = 0
  213. processed_objects = 0
  214. for obj in objects:
  215. object_name = obj.object_name
  216. # 跳过文件夹
  217. if object_name.endswith('/'):
  218. continue
  219. total_objects += 1
  220. logger.info(f"\n[{total_objects}] 处理对象: {object_name}")
  221. if self._process_single_object(self.bucket_name, object_name, force_update=True):
  222. processed_objects += 1
  223. self.milvus_api.flush_collection()
  224. logger.info(f"\n=== 全量刷新完成 ===")
  225. logger.info(f"总对象数: {total_objects}")
  226. logger.info(f"成功处理: {processed_objects}")
  227. logger.info(f"失败数量: {total_objects - processed_objects}")
  228. except Exception as e:
  229. logger.error(f"全量刷新失败: {e}")
  230. def backfill_update(self):
  231. """
  232. 模式2:排查补漏 - 检查Milvus中不存在的文档并新增
  233. """
  234. logger.info(f"\n=== 模式2:排查补漏 ===")
  235. logger.info(f"正在检查桶 '{self.bucket_name}' 中缺失的文档...")
  236. try:
  237. # 获取桶中的所有对象
  238. objects = self.minio_client.list_objects(self.bucket_name, recursive=True)
  239. total_objects = 0
  240. new_objects = 0
  241. existing_objects = 0
  242. for obj in objects:
  243. object_name = obj.object_name
  244. # 跳过文件夹
  245. if object_name.endswith('/'):
  246. continue
  247. total_objects += 1
  248. logger.info(f"\n[{total_objects}] 检查对象: {object_name}")
  249. # 检查是否已存在
  250. if self.milvus_api.check_document_exists(object_name):
  251. logger.info(f" 已存在,跳过")
  252. existing_objects += 1
  253. else:
  254. logger.info(f" 不存在,开始处理...")
  255. if self._process_single_object(self.bucket_name, object_name, force_update=False):
  256. new_objects += 1
  257. self.milvus_api.flush_collection()
  258. logger.info(f"\n=== 排查补漏完成 ===")
  259. logger.info(f"总对象数: {total_objects}")
  260. logger.info(f"已存在: {existing_objects}")
  261. logger.info(f"新增: {new_objects}")
  262. logger.info(f"失败数量: {total_objects - existing_objects - new_objects}")
  263. except Exception as e:
  264. logger.error(f"排查补漏失败: {e}")
  265. def start_listening(self):
  266. """
  267. 模式3:增量更新 - 根据消息通知进行增量更新
  268. """
  269. logger.info(f"\n=== 模式3:增量更新 ===")
  270. logger.info(f"开始监听桶 '{self.bucket_name}' 的事件...")
  271. logger.info("按 Ctrl+C 停止监听")
  272. try:
  273. # 监听桶事件
  274. events = self.minio_client.listen_bucket_notification(
  275. bucket_name=self.bucket_name,
  276. events=['s3:ObjectCreated:*', 's3:ObjectRemoved:*']
  277. )
  278. for event in events:
  279. try:
  280. if event:
  281. # 解析事件数据
  282. if isinstance(event, bytes):
  283. event_data = json.loads(event.decode('utf-8'))
  284. elif isinstance(event, str):
  285. event_data = json.loads(event)
  286. elif isinstance(event, dict):
  287. event_data = event
  288. else:
  289. logger.error(f"未知的事件数据类型: {type(event)}")
  290. continue
  291. # 处理事件
  292. self._process_event(event_data)
  293. except json.JSONDecodeError as e:
  294. logger.error(f"解析事件数据失败: {e}")
  295. except Exception as e:
  296. logger.error(f"处理事件失败: {e}")
  297. except KeyboardInterrupt:
  298. logger.info("\n监听已停止")
  299. except Exception as e:
  300. logger.error(f"监听过程中出错: {e}")