| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490 |
- import json
- from datetime import datetime
- import time
- from enum import Enum
- from typing import Dict, Any, Optional, Callable
- import os
- from dotenv import load_dotenv
- from dataclasses import dataclass
- from minio import Minio
- from mns.account import Account
- from parser.simple_object_parser import SimpleObjectParser
- from embedding import EmbeddingTool
- from milvus_api import MilvusAPI, MilvusDocument
- from logger.logging import setup_logging
- load_dotenv()
- logger = setup_logging()
- # Minio 配置,因为兼容阿里云 OSS,所以使用同一套MinIO客户端。
- MINIO_BUCKET = os.getenv('MINIO_BUCKET')
- MINIO_ENDPOINT = os.getenv('MINIO_ENDPOINT')
- MINIO_ACCESS_KEY = os.getenv('MINIO_ACCESS_KEY')
- MINIO_SECRET_KEY = os.getenv('MINIO_SECRET_KEY')
- MINIO_REGION = os.getenv('MINIO_REGION')
- MINIO_USE_VIRTUAL_HOST = os.getenv('MINIO_USE_VIRTUAL_HOST', 'false').lower() == 'true'
- MINIO_USE_SSL = os.getenv('MINIO_USE_SSL', 'false').lower() == 'true'
- ## 获取MinIO客户端
- def _get_minio_client() -> Minio:
- client = Minio(
- endpoint=MINIO_ENDPOINT,
- access_key=MINIO_ACCESS_KEY,
- secret_key=MINIO_SECRET_KEY,
- secure=MINIO_USE_SSL,
- region=MINIO_REGION,
- )
- if MINIO_USE_VIRTUAL_HOST:
- client.enable_virtual_style_endpoint()
-
- return client
- ## 配置阿里云消息队列
- MNS_ACCESS_KEY_ID = os.getenv("MNS_ACCESS_KEY_ID")
- MNS_ACCESS_KEY_SECRET = os.getenv("MNS_ACCESS_KEY_SECRET")
- MNS_ENDPOINT = os.getenv("MNS_ENDPOINT")
- MNS_QUEUE_NAME = os.getenv("MNS_QUEUE_NAME", "mildoc-oss-notify")
- class OSSEventType(Enum):
- """OSS 事件类型"""
- OBJECT_CREATED = "ObjectCreated" # 对象创建(上传)
- OBJECT_REMOVED = "ObjectRemoved" # 对象删除
- @dataclass
- class OSSEvent:
- """OSS 事件数据结构"""
- event_name: str # 事件名称,如 "ObjectCreated:PutObject"
- event_time: str # 事件时间
- bucket_name: str # Bucket 名称
- object_key: str # 对象键(路径)
- object_size: Optional[int] = None # 对象大小(字节)
- etag: Optional[str] = None # 对象 ETag
- class OSSEventNotifier:
- """OSS 事件通知管理器"""
-
- def __init__(self):
- """初始化 MNS 队列"""
- if not MNS_ENDPOINT or not MNS_ACCESS_KEY_ID or not MNS_ACCESS_KEY_SECRET:
- logger.error(f"✗ MNS 队列初始化失败: MNS_ENDPOINT or MNS_ACCESS_KEY_ID or MNS_ACCESS_KEY_SECRET is not set")
- raise Exception("MNS_ENDPOINT or MNS_ACCESS_KEY_ID or MNS_ACCESS_KEY_SECRET is not set")
-
-
- mns_account = Account(MNS_ENDPOINT, MNS_ACCESS_KEY_ID, MNS_ACCESS_KEY_SECRET)
- self.mildoc_queue = mns_account.get_queue(MNS_QUEUE_NAME)
- logger.info(f"✓ MNS 队列初始化成功 (endpoint: {MNS_ENDPOINT})")
-
- def listen_mns_queue(
- self,
- handler: Callable[[Dict], None],
- poll_interval: int = 5
- ):
- """
- 监听 MNS 队列,接收 OSS 事件通知。
-
- :param handler: 事件处理函数,接收 Dict 对象
- :param poll_interval: 轮询间隔(秒)
- """
- if not self.mildoc_queue:
- logger.error("✗ MNS 客户端未初始化,无法监听队列")
- return
-
- logger.info(f"开始监听 MNS 队列: {MNS_QUEUE_NAME}")
- logger.info("按 Ctrl+C 停止监听")
-
- try:
-
- while True:
- try:
- # 接收消息(长轮询,等待最多 10 秒)
- try:
- msg = self.mildoc_queue.receive_message_with_str_body(wait_seconds=10)
- except Exception as e:
- # 如果没有消息,会抛出异常,继续轮询
- if "MessageNotExist" in str(e) or "not found" in str(e).lower():
- time.sleep(poll_interval)
- continue
- else:
- logger.error(f"✗ 接收消息失败: {e}")
- time.sleep(poll_interval)
- continue
-
- if msg:
- try:
- # 解析 OSS 事件
- # MNS 消息体可能是字符串,需要先解析
- message_body = msg.message_body
- if isinstance(message_body, str):
- event_data = json.loads(message_body)
- else:
- event_data = message_body
-
- #logger.info(f"event_data: {event_data}")
- logger.info(f"数据: {json.dumps(event_data, ensure_ascii=False, indent=2)}")
-
- if event_data:
-
- # 调用处理函数
- if handler:
- handler(event_data)
-
- # 删除消息(确认已处理)
- self.mildoc_queue.delete_message(msg.receipt_handle)
- logger.info(" ✓ 消息已处理并删除")
- else:
- logger.warning(f"⚠ 无法解析事件数据: {message_body}")
- # 即使无法解析,也删除消息,避免重复处理
- self.mildoc_queue.delete_message(msg.receipt_handle)
-
- except json.JSONDecodeError as e:
- logger.error(f"✗ 解析消息失败: {e}, 消息内容: {msg.message_body}")
- # 删除无法解析的消息,避免阻塞队列
- try:
- self.mildoc_queue.delete_message(msg.receipt_handle)
- except:
- logger.error(f"✗ 删除消息失败: {e}")
- pass
- except Exception as e:
- logger.error(f"✗ 处理消息失败: {e}")
-
- time.sleep(poll_interval)
-
- except KeyboardInterrupt:
- logger.info("\n停止监听...")
- break
- except Exception as e:
- logger.error(f"✗ 接收消息失败: {e}")
- time.sleep(poll_interval)
-
- except Exception as e:
- logger.error(f"✗ 监听队列失败: {e}")
-
- class OSSEventHandler:
- """OSS 事件处理器"""
-
- def __init__(self, bucket_name: str = None):
- """
- 初始化监听器
-
- Args:
- bucket_name (str): 要监听的桶名称,默认从环境变量获取
- """
- self.bucket_name = bucket_name or os.getenv("OSS_BUCKET", "mildoc")
-
- # 初始化各个组件
- self.minio_client = _get_minio_client()
-
- # 初始化解析器
- logger.info("初始化解析器...")
- self.parser: SimpleObjectParser = SimpleObjectParser(minio_client=self.minio_client)
-
- # 初始化Milvus
- logger.info("初始化Milvus...")
- self.milvus_api: MilvusAPI = MilvusAPI()
-
- # 测试embedding工具
- logger.info("测试embedding工具...")
- self.embedding_tool: EmbeddingTool = EmbeddingTool()
- # OSS 事件监听
- logger.info("初始化OSS事件监听...")
- self.oss_event_notifier: OSSEventNotifier = OSSEventNotifier()
- logger.info("所有组件初始化完成!")
- def _process_event(self, event_data: Dict[str, Any]):
- """
- 处理单个事件
-
- Args:
- event_data (Dict[str, Any]): 事件数据
- """
- try:
- # 提取事件信息
- event_info = self._extract_event_info(event_data)
- if not event_info:
- logger.error("无法提取事件信息,跳过处理")
- return
-
- event_name = event_info.event_name
- timestamp = event_info.event_time
-
- logger.info(f"\n[{timestamp}] 收到事件: {event_name}")
- logger.info(f"对象: {event_info.bucket_name}/{event_info.object_key}")
-
- # 根据事件类型进行处理
- if OSSEventType.OBJECT_CREATED.value in event_name:
- self._handle_object_created(event_info)
- elif OSSEventType.OBJECT_REMOVED.value in event_name:
- self._handle_object_deleted(event_info)
- else:
- logger.error(f"未处理的事件类型: {event_name}")
-
- except Exception as e:
- logger.error(f"处理事件时出错: {e}")
-
- def _extract_event_info(self, event_data: Dict) -> Optional[OSSEvent]:
- """解析 OSS 事件数据"""
- try:
- # OSS 事件通知格式
- events = event_data.get("events", [])
- if not events:
- return None
-
- event = events[0]
- event_name = event.get("eventName", "")
- event_time = event.get("eventTime", "")
-
- # 获取对象信息
- oss_obj = event.get("oss", {}).get("object", {})
- bucket = event.get("oss", {}).get("bucket", {})
-
- return OSSEvent(
- event_name=event_name,
- event_time=event_time,
- bucket_name=bucket.get("name", ""),
- object_key=oss_obj.get("key", ""),
- object_size=oss_obj.get("size"),
- etag=oss_obj.get("etag"),
- )
- except Exception as e:
- logger.error(f"✗ 解析事件数据失败: {e}")
- return None
-
- def _handle_object_created(self, event_info: OSSEvent):
- """
- 处理对象创建事件
-
- Args:
- event_info (Dict[str, Any]): 事件信息
- """
- try:
- bucket_name = event_info.bucket_name
- object_name = event_info.object_key
-
- logger.info(f"\n=== 处理新增对象: {bucket_name}/{object_name} ===")
- logger.info(f"对象大小: {event_info.object_size} 字节")
-
- # 直接调用_process_single_object方法处理
- self._process_single_object(bucket_name, object_name, force_update=True)
-
- except Exception as e:
- logger.error(f"处理对象创建事件失败: {e}")
-
- def _handle_object_deleted(self, event_info: OSSEvent):
- """
- 处理对象删除事件
-
- Args:
- event_info (Dict[str, Any]): 事件信息
- """
- try:
- bucket_name = event_info.bucket_name
- object_name = event_info.object_key
- doc_path_name = object_name # 不再包含bucket_name前缀
-
- logger.info(f"\n=== 处理删除对象: {bucket_name}/{object_name} ===")
-
- # 从Milvus中删除相关记录
- logger.info("从Milvus中查找并删除相关记录...")
-
- # 使用MilvusAPI的删除方法
- if self.milvus_api.delete_existing_document(doc_path_name):
- logger.info(f"成功删除文档记录: {doc_path_name}")
- else:
- logger.error(f"删除文档记录失败: {doc_path_name}")
-
- except Exception as e:
- logger.error(f"处理对象删除事件失败: {e}")
-
-
- def _process_single_object(self, bucket_name: str, object_name: str, force_update: bool = False):
- """
- 处理单个对象(用于全量刷新和排查补漏)
-
- Args:
- bucket_name (str): 桶名称
- object_name (str): 对象名称
- force_update (bool): 是否强制更新(True=全量刷新,False=排查补漏)
-
- Returns:
- bool: 处理是否成功
- """
- try:
- doc_path_name = object_name # 不再包含bucket_name前缀
-
- # 如果是排查补漏模式,先检查是否已存在
- if not force_update:
- if self.milvus_api.check_document_exists(doc_path_name):
- logger.info(f" 文档已存在,跳过: {object_name}")
- return True
-
- logger.info(f" 处理文档: {object_name}")
-
- # 解析对象内容
- parse_result = self.parser.parse_object(bucket_name, object_name)
-
- if 'error' in parse_result:
- logger.error(f" 解析失败: {parse_result['error']}")
- return False
-
- if not parse_result['contents']:
- logger.error(f" 未提取到文本内容,跳过")
- return True
-
- logger.info(f" 解析成功,获得 {len(parse_result['contents'])} 个文本片段")
-
- # 如果是强制更新,先删除已存在的记录
- if force_update:
- self.milvus_api.delete_existing_document(doc_path_name)
-
- # 为每个文本片段生成embedding并存储到Milvus
- success_count = 0
- for i, content in enumerate(parse_result['contents']):
- try:
- # 生成embedding向量
- embedding_vector = self.embedding_tool.get_embedding(content)
- if not embedding_vector:
- logger.error(f" 片段 {i+1} embedding生成失败,跳过")
- continue
-
- # 准备文档数据
- doc_data = MilvusDocument(
- doc_name=parse_result['doc_name'],
- doc_path_name=parse_result['doc_path_name'],
- doc_type=parse_result['doc_type'],
- doc_md5=parse_result['doc_md5'],
- doc_length=parse_result['doc_length'],
- content=content,
- content_vector=embedding_vector,
- embedding_model=self.embedding_tool.model
- )
-
- # 存储到Milvus(允许重复,因为我们已经处理了去重逻辑)
- if self.milvus_api.insert_document(doc_data):
- success_count += 1
- else:
- logger.error(f" 片段 {i+1} 存储失败")
-
- except Exception as e:
- logger.error(f" 处理片段 {i+1} 时出错: {e}")
- continue
-
- logger.info(f" 完成!成功存储 {success_count}/{len(parse_result['contents'])} 个片段")
- return success_count > 0
-
- except Exception as e:
- logger.error(f" 处理对象失败: {e}")
- return False
-
- def full_update(self):
- """
- 模式1:全量刷新 - 遍历Minio桶中的所有数据并更新到Milvus
- """
- logger.info(f"\n=== 模式1:全量刷新 ===")
- logger.info(f"正在遍历桶 '{self.bucket_name}' 中的所有对象...")
-
- try:
- # 获取桶中的所有对象
- objects = self.minio_client.list_objects(self.bucket_name, recursive=True)
-
- total_objects = 0
- processed_objects = 0
-
- for obj in objects:
- object_name = obj.object_name
-
- # 跳过文件夹
- if object_name.endswith('/'):
- continue
-
- total_objects += 1
- logger.info(f"\n[{total_objects}] 处理对象: {object_name}")
-
- if self._process_single_object(self.bucket_name, object_name, force_update=True):
- processed_objects += 1
-
- self.milvus_api.flush_collection()
-
- logger.info(f"\n=== 全量刷新完成 ===")
- logger.info(f"总对象数: {total_objects}")
- logger.info(f"成功处理: {processed_objects}")
- logger.info(f"失败数量: {total_objects - processed_objects}")
-
- except Exception as e:
- logger.error(f"全量刷新失败: {e}")
-
- def backfill_update(self):
- """
- 模式2:排查补漏 - 检查Milvus中不存在的文档并新增
- """
- logger.info(f"\n=== 模式2:排查补漏 ===")
- logger.info(f"正在检查桶 '{self.bucket_name}' 中缺失的文档...")
-
- try:
- # 获取桶中的所有对象
- objects = self.minio_client.list_objects(self.bucket_name, recursive=True)
-
- total_objects = 0
- new_objects = 0
- existing_objects = 0
-
- for obj in objects:
- object_name = obj.object_name
-
- # 跳过文件夹
- if object_name.endswith('/'):
- continue
-
- total_objects += 1
- logger.info(f"\n[{total_objects}] 检查对象: {object_name}")
-
- # 检查是否已存在
- if self.milvus_api.check_document_exists(object_name):
- logger.info(f" 已存在,跳过")
- existing_objects += 1
- else:
- logger.info(f" 不存在,开始处理...")
- if self._process_single_object(self.bucket_name, object_name, force_update=False):
- new_objects += 1
-
- self.milvus_api.flush_collection()
-
- logger.info(f"\n=== 排查补漏完成 ===")
- logger.info(f"总对象数: {total_objects}")
- logger.info(f"已存在: {existing_objects}")
- logger.info(f"新增: {new_objects}")
- logger.info(f"失败数量: {total_objects - existing_objects - new_objects}")
-
- except Exception as e:
- logger.error(f"排查补漏失败: {e}")
-
- def start_listening(self):
- """
- 模式3:增量更新 - 根据消息通知进行增量更新
- """
- logger.info(f"\n=== 模式3:增量更新 ===")
- logger.info(f"开始监听桶 '{self.bucket_name}' 的事件...")
- logger.info("按 Ctrl+C 停止监听")
-
- try:
- # 监听桶事件
- self.oss_event_notifier.listen_mns_queue(self._process_event)
-
- except KeyboardInterrupt:
- logger.info("\n监听已停止")
- except Exception as e:
- logger.error(f"监听过程中出错: {e}")
-
|