| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384 |
- import json
- from datetime import datetime
- from typing import Dict, Any
- import os
- from dotenv import load_dotenv
- from minio import Minio
- 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 配置信息。
- 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
- class MinioEventHandler:
- """Minio事件监听器"""
-
- def __init__(self, bucket_name: str = None):
- """
- 初始化监听器
-
- Args:
- bucket_name (str): 要监听的桶名称,默认从环境变量获取
- """
- self.bucket_name = bucket_name or os.getenv("MINIO_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()
- logger.info("所有组件初始化完成!")
-
- def _extract_event_info(self, event_data: Dict[str, Any]) -> Dict[str, Any]:
- """
- 从事件数据中提取关键信息
-
- Args:
- event_data (Dict[str, Any]): 事件数据
-
- Returns:
- Dict[str, Any]: 提取的信息
- """
- try:
- #logger.info(f"event_data: {event_data}")
- logger.info(f"数据: {json.dumps(event_data, ensure_ascii=False, indent=2)}")
- record = event_data.get('Records', [{}])[0]
- s3_info = record.get('s3', {})
-
- return {
- 'event_name': record.get('eventName', ''),
- 'event_time': record.get('eventTime', ''),
- 'bucket_name': s3_info.get('bucket', {}).get('name', ''),
- 'object_name': s3_info.get('object', {}).get('key', ''),
- 'object_size': s3_info.get('object', {}).get('size', 0),
- 'content_type': s3_info.get('object', {}).get('contentType', ''),
- 'etag': s3_info.get('object', {}).get('eTag', ''),
- }
- except Exception as e:
- logger.error(f"提取事件信息失败: {e}")
- return {}
-
- def _handle_object_created(self, event_info: Dict[str, Any]):
- """
- 处理对象创建事件
-
- Args:
- event_info (Dict[str, Any]): 事件信息
- """
- try:
- bucket_name = event_info['bucket_name']
- object_name = event_info['object_name']
-
- logger.info(f"\n=== 处理新增对象: {bucket_name}/{object_name} ===")
- logger.info(f"对象大小: {event_info['object_size']} 字节")
- logger.info(f"内容类型: {event_info['content_type']}")
-
- # 直接调用_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: Dict[str, Any]):
- """
- 处理对象删除事件
-
- Args:
- event_info (Dict[str, Any]): 事件信息
- """
- try:
- bucket_name = event_info['bucket_name']
- object_name = event_info['object_name']
- 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_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 = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
-
- logger.info(f"\n[{timestamp}] 收到事件: {event_name}")
- logger.info(f"对象: {event_info['bucket_name']}/{event_info['object_name']}")
-
- # 根据事件类型进行处理
- if 'ObjectCreated' in event_name:
- self._handle_object_created(event_info)
- elif 'ObjectRemoved' in event_name:
- self._handle_object_deleted(event_info)
- else:
- logger.error(f"未处理的事件类型: {event_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:
- # 监听桶事件
- events = self.minio_client.listen_bucket_notification(
- bucket_name=self.bucket_name,
- events=['s3:ObjectCreated:*', 's3:ObjectRemoved:*']
- )
-
- for event in events:
- try:
- if event:
- # 解析事件数据
- if isinstance(event, bytes):
- event_data = json.loads(event.decode('utf-8'))
- elif isinstance(event, str):
- event_data = json.loads(event)
- elif isinstance(event, dict):
- event_data = event
- else:
- logger.error(f"未知的事件数据类型: {type(event)}")
- continue
-
- # 处理事件
- self._process_event(event_data)
-
- except json.JSONDecodeError as e:
- logger.error(f"解析事件数据失败: {e}")
- except Exception as e:
- logger.error(f"处理事件失败: {e}")
-
- except KeyboardInterrupt:
- logger.info("\n监听已停止")
- except Exception as e:
- logger.error(f"监听过程中出错: {e}")
-
|