oss_event_handler.py 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490
  1. import json
  2. from datetime import datetime
  3. import time
  4. from enum import Enum
  5. from typing import Dict, Any, Optional, Callable
  6. import os
  7. from dotenv import load_dotenv
  8. from dataclasses import dataclass
  9. from minio import Minio
  10. from mns.account import Account
  11. from parser.simple_object_parser import SimpleObjectParser
  12. from embedding import EmbeddingTool
  13. from milvus_api import MilvusAPI, MilvusDocument
  14. from logger.logging import setup_logging
  15. load_dotenv()
  16. logger = setup_logging()
  17. # Minio 配置,因为兼容阿里云 OSS,所以使用同一套MinIO客户端。
  18. MINIO_BUCKET = os.getenv('MINIO_BUCKET')
  19. MINIO_ENDPOINT = os.getenv('MINIO_ENDPOINT')
  20. MINIO_ACCESS_KEY = os.getenv('MINIO_ACCESS_KEY')
  21. MINIO_SECRET_KEY = os.getenv('MINIO_SECRET_KEY')
  22. MINIO_REGION = os.getenv('MINIO_REGION')
  23. MINIO_USE_VIRTUAL_HOST = os.getenv('MINIO_USE_VIRTUAL_HOST', 'false').lower() == 'true'
  24. MINIO_USE_SSL = os.getenv('MINIO_USE_SSL', 'false').lower() == 'true'
  25. ## 获取MinIO客户端
  26. def _get_minio_client() -> Minio:
  27. client = Minio(
  28. endpoint=MINIO_ENDPOINT,
  29. access_key=MINIO_ACCESS_KEY,
  30. secret_key=MINIO_SECRET_KEY,
  31. secure=MINIO_USE_SSL,
  32. region=MINIO_REGION,
  33. )
  34. if MINIO_USE_VIRTUAL_HOST:
  35. client.enable_virtual_style_endpoint()
  36. return client
  37. ## 配置阿里云消息队列
  38. MNS_ACCESS_KEY_ID = os.getenv("MNS_ACCESS_KEY_ID")
  39. MNS_ACCESS_KEY_SECRET = os.getenv("MNS_ACCESS_KEY_SECRET")
  40. MNS_ENDPOINT = os.getenv("MNS_ENDPOINT")
  41. MNS_QUEUE_NAME = os.getenv("MNS_QUEUE_NAME", "mildoc-oss-notify")
  42. class OSSEventType(Enum):
  43. """OSS 事件类型"""
  44. OBJECT_CREATED = "ObjectCreated" # 对象创建(上传)
  45. OBJECT_REMOVED = "ObjectRemoved" # 对象删除
  46. @dataclass
  47. class OSSEvent:
  48. """OSS 事件数据结构"""
  49. event_name: str # 事件名称,如 "ObjectCreated:PutObject"
  50. event_time: str # 事件时间
  51. bucket_name: str # Bucket 名称
  52. object_key: str # 对象键(路径)
  53. object_size: Optional[int] = None # 对象大小(字节)
  54. etag: Optional[str] = None # 对象 ETag
  55. class OSSEventNotifier:
  56. """OSS 事件通知管理器"""
  57. def __init__(self):
  58. """初始化 MNS 队列"""
  59. if not MNS_ENDPOINT or not MNS_ACCESS_KEY_ID or not MNS_ACCESS_KEY_SECRET:
  60. logger.error(f"✗ MNS 队列初始化失败: MNS_ENDPOINT or MNS_ACCESS_KEY_ID or MNS_ACCESS_KEY_SECRET is not set")
  61. raise Exception("MNS_ENDPOINT or MNS_ACCESS_KEY_ID or MNS_ACCESS_KEY_SECRET is not set")
  62. mns_account = Account(MNS_ENDPOINT, MNS_ACCESS_KEY_ID, MNS_ACCESS_KEY_SECRET)
  63. self.mildoc_queue = mns_account.get_queue(MNS_QUEUE_NAME)
  64. logger.info(f"✓ MNS 队列初始化成功 (endpoint: {MNS_ENDPOINT})")
  65. def listen_mns_queue(
  66. self,
  67. handler: Callable[[Dict], None],
  68. poll_interval: int = 5
  69. ):
  70. """
  71. 监听 MNS 队列,接收 OSS 事件通知。
  72. :param handler: 事件处理函数,接收 Dict 对象
  73. :param poll_interval: 轮询间隔(秒)
  74. """
  75. if not self.mildoc_queue:
  76. logger.error("✗ MNS 客户端未初始化,无法监听队列")
  77. return
  78. logger.info(f"开始监听 MNS 队列: {MNS_QUEUE_NAME}")
  79. logger.info("按 Ctrl+C 停止监听")
  80. try:
  81. while True:
  82. try:
  83. # 接收消息(长轮询,等待最多 10 秒)
  84. try:
  85. msg = self.mildoc_queue.receive_message_with_str_body(wait_seconds=10)
  86. except Exception as e:
  87. # 如果没有消息,会抛出异常,继续轮询
  88. if "MessageNotExist" in str(e) or "not found" in str(e).lower():
  89. time.sleep(poll_interval)
  90. continue
  91. else:
  92. logger.error(f"✗ 接收消息失败: {e}")
  93. time.sleep(poll_interval)
  94. continue
  95. if msg:
  96. try:
  97. # 解析 OSS 事件
  98. # MNS 消息体可能是字符串,需要先解析
  99. message_body = msg.message_body
  100. if isinstance(message_body, str):
  101. event_data = json.loads(message_body)
  102. else:
  103. event_data = message_body
  104. #logger.info(f"event_data: {event_data}")
  105. logger.info(f"数据: {json.dumps(event_data, ensure_ascii=False, indent=2)}")
  106. if event_data:
  107. # 调用处理函数
  108. if handler:
  109. handler(event_data)
  110. # 删除消息(确认已处理)
  111. self.mildoc_queue.delete_message(msg.receipt_handle)
  112. logger.info(" ✓ 消息已处理并删除")
  113. else:
  114. logger.warning(f"⚠ 无法解析事件数据: {message_body}")
  115. # 即使无法解析,也删除消息,避免重复处理
  116. self.mildoc_queue.delete_message(msg.receipt_handle)
  117. except json.JSONDecodeError as e:
  118. logger.error(f"✗ 解析消息失败: {e}, 消息内容: {msg.message_body}")
  119. # 删除无法解析的消息,避免阻塞队列
  120. try:
  121. self.mildoc_queue.delete_message(msg.receipt_handle)
  122. except:
  123. logger.error(f"✗ 删除消息失败: {e}")
  124. pass
  125. except Exception as e:
  126. logger.error(f"✗ 处理消息失败: {e}")
  127. time.sleep(poll_interval)
  128. except KeyboardInterrupt:
  129. logger.info("\n停止监听...")
  130. break
  131. except Exception as e:
  132. logger.error(f"✗ 接收消息失败: {e}")
  133. time.sleep(poll_interval)
  134. except Exception as e:
  135. logger.error(f"✗ 监听队列失败: {e}")
  136. class OSSEventHandler:
  137. """OSS 事件处理器"""
  138. def __init__(self, bucket_name: str = None):
  139. """
  140. 初始化监听器
  141. Args:
  142. bucket_name (str): 要监听的桶名称,默认从环境变量获取
  143. """
  144. self.bucket_name = bucket_name or os.getenv("OSS_BUCKET", "mildoc")
  145. # 初始化各个组件
  146. self.minio_client = _get_minio_client()
  147. # 初始化解析器
  148. logger.info("初始化解析器...")
  149. self.parser: SimpleObjectParser = SimpleObjectParser(minio_client=self.minio_client)
  150. # 初始化Milvus
  151. logger.info("初始化Milvus...")
  152. self.milvus_api: MilvusAPI = MilvusAPI()
  153. # 测试embedding工具
  154. logger.info("测试embedding工具...")
  155. self.embedding_tool: EmbeddingTool = EmbeddingTool()
  156. # OSS 事件监听
  157. logger.info("初始化OSS事件监听...")
  158. self.oss_event_notifier: OSSEventNotifier = OSSEventNotifier()
  159. logger.info("所有组件初始化完成!")
  160. def _process_event(self, event_data: Dict[str, Any]):
  161. """
  162. 处理单个事件
  163. Args:
  164. event_data (Dict[str, Any]): 事件数据
  165. """
  166. try:
  167. # 提取事件信息
  168. event_info = self._extract_event_info(event_data)
  169. if not event_info:
  170. logger.error("无法提取事件信息,跳过处理")
  171. return
  172. event_name = event_info.event_name
  173. timestamp = event_info.event_time
  174. logger.info(f"\n[{timestamp}] 收到事件: {event_name}")
  175. logger.info(f"对象: {event_info.bucket_name}/{event_info.object_key}")
  176. # 根据事件类型进行处理
  177. if OSSEventType.OBJECT_CREATED.value in event_name:
  178. self._handle_object_created(event_info)
  179. elif OSSEventType.OBJECT_REMOVED.value in event_name:
  180. self._handle_object_deleted(event_info)
  181. else:
  182. logger.error(f"未处理的事件类型: {event_name}")
  183. except Exception as e:
  184. logger.error(f"处理事件时出错: {e}")
  185. def _extract_event_info(self, event_data: Dict) -> Optional[OSSEvent]:
  186. """解析 OSS 事件数据"""
  187. try:
  188. # OSS 事件通知格式
  189. events = event_data.get("events", [])
  190. if not events:
  191. return None
  192. event = events[0]
  193. event_name = event.get("eventName", "")
  194. event_time = event.get("eventTime", "")
  195. # 获取对象信息
  196. oss_obj = event.get("oss", {}).get("object", {})
  197. bucket = event.get("oss", {}).get("bucket", {})
  198. return OSSEvent(
  199. event_name=event_name,
  200. event_time=event_time,
  201. bucket_name=bucket.get("name", ""),
  202. object_key=oss_obj.get("key", ""),
  203. object_size=oss_obj.get("size"),
  204. etag=oss_obj.get("etag"),
  205. )
  206. except Exception as e:
  207. logger.error(f"✗ 解析事件数据失败: {e}")
  208. return None
  209. def _handle_object_created(self, event_info: OSSEvent):
  210. """
  211. 处理对象创建事件
  212. Args:
  213. event_info (Dict[str, Any]): 事件信息
  214. """
  215. try:
  216. bucket_name = event_info.bucket_name
  217. object_name = event_info.object_key
  218. logger.info(f"\n=== 处理新增对象: {bucket_name}/{object_name} ===")
  219. logger.info(f"对象大小: {event_info.object_size} 字节")
  220. # 直接调用_process_single_object方法处理
  221. self._process_single_object(bucket_name, object_name, force_update=True)
  222. except Exception as e:
  223. logger.error(f"处理对象创建事件失败: {e}")
  224. def _handle_object_deleted(self, event_info: OSSEvent):
  225. """
  226. 处理对象删除事件
  227. Args:
  228. event_info (Dict[str, Any]): 事件信息
  229. """
  230. try:
  231. bucket_name = event_info.bucket_name
  232. object_name = event_info.object_key
  233. doc_path_name = object_name # 不再包含bucket_name前缀
  234. logger.info(f"\n=== 处理删除对象: {bucket_name}/{object_name} ===")
  235. # 从Milvus中删除相关记录
  236. logger.info("从Milvus中查找并删除相关记录...")
  237. # 使用MilvusAPI的删除方法
  238. if self.milvus_api.delete_existing_document(doc_path_name):
  239. logger.info(f"成功删除文档记录: {doc_path_name}")
  240. else:
  241. logger.error(f"删除文档记录失败: {doc_path_name}")
  242. except Exception as e:
  243. logger.error(f"处理对象删除事件失败: {e}")
  244. def _process_single_object(self, bucket_name: str, object_name: str, force_update: bool = False):
  245. """
  246. 处理单个对象(用于全量刷新和排查补漏)
  247. Args:
  248. bucket_name (str): 桶名称
  249. object_name (str): 对象名称
  250. force_update (bool): 是否强制更新(True=全量刷新,False=排查补漏)
  251. Returns:
  252. bool: 处理是否成功
  253. """
  254. try:
  255. doc_path_name = object_name # 不再包含bucket_name前缀
  256. # 如果是排查补漏模式,先检查是否已存在
  257. if not force_update:
  258. if self.milvus_api.check_document_exists(doc_path_name):
  259. logger.info(f" 文档已存在,跳过: {object_name}")
  260. return True
  261. logger.info(f" 处理文档: {object_name}")
  262. # 解析对象内容
  263. parse_result = self.parser.parse_object(bucket_name, object_name)
  264. if 'error' in parse_result:
  265. logger.error(f" 解析失败: {parse_result['error']}")
  266. return False
  267. if not parse_result['contents']:
  268. logger.error(f" 未提取到文本内容,跳过")
  269. return True
  270. logger.info(f" 解析成功,获得 {len(parse_result['contents'])} 个文本片段")
  271. # 如果是强制更新,先删除已存在的记录
  272. if force_update:
  273. self.milvus_api.delete_existing_document(doc_path_name)
  274. # 为每个文本片段生成embedding并存储到Milvus
  275. success_count = 0
  276. for i, content in enumerate(parse_result['contents']):
  277. try:
  278. # 生成embedding向量
  279. embedding_vector = self.embedding_tool.get_embedding(content)
  280. if not embedding_vector:
  281. logger.error(f" 片段 {i+1} embedding生成失败,跳过")
  282. continue
  283. # 准备文档数据
  284. doc_data = MilvusDocument(
  285. doc_name=parse_result['doc_name'],
  286. doc_path_name=parse_result['doc_path_name'],
  287. doc_type=parse_result['doc_type'],
  288. doc_md5=parse_result['doc_md5'],
  289. doc_length=parse_result['doc_length'],
  290. content=content,
  291. content_vector=embedding_vector,
  292. embedding_model=self.embedding_tool.model
  293. )
  294. # 存储到Milvus(允许重复,因为我们已经处理了去重逻辑)
  295. if self.milvus_api.insert_document(doc_data):
  296. success_count += 1
  297. else:
  298. logger.error(f" 片段 {i+1} 存储失败")
  299. except Exception as e:
  300. logger.error(f" 处理片段 {i+1} 时出错: {e}")
  301. continue
  302. logger.info(f" 完成!成功存储 {success_count}/{len(parse_result['contents'])} 个片段")
  303. return success_count > 0
  304. except Exception as e:
  305. logger.error(f" 处理对象失败: {e}")
  306. return False
  307. def full_update(self):
  308. """
  309. 模式1:全量刷新 - 遍历Minio桶中的所有数据并更新到Milvus
  310. """
  311. logger.info(f"\n=== 模式1:全量刷新 ===")
  312. logger.info(f"正在遍历桶 '{self.bucket_name}' 中的所有对象...")
  313. try:
  314. # 获取桶中的所有对象
  315. objects = self.minio_client.list_objects(self.bucket_name, recursive=True)
  316. total_objects = 0
  317. processed_objects = 0
  318. for obj in objects:
  319. object_name = obj.object_name
  320. # 跳过文件夹
  321. if object_name.endswith('/'):
  322. continue
  323. total_objects += 1
  324. logger.info(f"\n[{total_objects}] 处理对象: {object_name}")
  325. if self._process_single_object(self.bucket_name, object_name, force_update=True):
  326. processed_objects += 1
  327. self.milvus_api.flush_collection()
  328. logger.info(f"\n=== 全量刷新完成 ===")
  329. logger.info(f"总对象数: {total_objects}")
  330. logger.info(f"成功处理: {processed_objects}")
  331. logger.info(f"失败数量: {total_objects - processed_objects}")
  332. except Exception as e:
  333. logger.error(f"全量刷新失败: {e}")
  334. def backfill_update(self):
  335. """
  336. 模式2:排查补漏 - 检查Milvus中不存在的文档并新增
  337. """
  338. logger.info(f"\n=== 模式2:排查补漏 ===")
  339. logger.info(f"正在检查桶 '{self.bucket_name}' 中缺失的文档...")
  340. try:
  341. # 获取桶中的所有对象
  342. objects = self.minio_client.list_objects(self.bucket_name, recursive=True)
  343. total_objects = 0
  344. new_objects = 0
  345. existing_objects = 0
  346. for obj in objects:
  347. object_name = obj.object_name
  348. # 跳过文件夹
  349. if object_name.endswith('/'):
  350. continue
  351. total_objects += 1
  352. logger.info(f"\n[{total_objects}] 检查对象: {object_name}")
  353. # 检查是否已存在
  354. if self.milvus_api.check_document_exists(object_name):
  355. logger.info(f" 已存在,跳过")
  356. existing_objects += 1
  357. else:
  358. logger.info(f" 不存在,开始处理...")
  359. if self._process_single_object(self.bucket_name, object_name, force_update=False):
  360. new_objects += 1
  361. self.milvus_api.flush_collection()
  362. logger.info(f"\n=== 排查补漏完成 ===")
  363. logger.info(f"总对象数: {total_objects}")
  364. logger.info(f"已存在: {existing_objects}")
  365. logger.info(f"新增: {new_objects}")
  366. logger.info(f"失败数量: {total_objects - existing_objects - new_objects}")
  367. except Exception as e:
  368. logger.error(f"排查补漏失败: {e}")
  369. def start_listening(self):
  370. """
  371. 模式3:增量更新 - 根据消息通知进行增量更新
  372. """
  373. logger.info(f"\n=== 模式3:增量更新 ===")
  374. logger.info(f"开始监听桶 '{self.bucket_name}' 的事件...")
  375. logger.info("按 Ctrl+C 停止监听")
  376. try:
  377. # 监听桶事件
  378. self.oss_event_notifier.listen_mns_queue(self._process_event)
  379. except KeyboardInterrupt:
  380. logger.info("\n监听已停止")
  381. except Exception as e:
  382. logger.error(f"监听过程中出错: {e}")