jfox-cli 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
jfox/vector_store.py ADDED
@@ -0,0 +1,200 @@
1
+ """ChromaDB 向量存储封装"""
2
+
3
+ import logging
4
+ from pathlib import Path
5
+ from typing import List, Optional, Dict, Any
6
+
7
+ import chromadb
8
+ from chromadb.config import Settings
9
+
10
+ from .models import Note
11
+ from .config import config
12
+
13
+ logger = logging.getLogger(__name__)
14
+
15
+
16
+ class VectorStore:
17
+ """向量存储封装"""
18
+
19
+ def __init__(self, persist_directory: Optional[Path] = None):
20
+ if persist_directory is None:
21
+ persist_directory = config.chroma_dir
22
+
23
+ self.persist_directory = persist_directory
24
+ self.client = None
25
+ self.collection = None
26
+
27
+ def init(self):
28
+ """初始化 ChromaDB"""
29
+ if self.client is not None:
30
+ return
31
+
32
+ # 确保目录存在
33
+ self.persist_directory.mkdir(parents=True, exist_ok=True)
34
+
35
+ # 创建客户端
36
+ self.client = chromadb.PersistentClient(
37
+ path=str(self.persist_directory),
38
+ settings=Settings(
39
+ anonymized_telemetry=False,
40
+ allow_reset=True,
41
+ )
42
+ )
43
+
44
+ # 获取或创建集合
45
+ self.collection = self.client.get_or_create_collection(
46
+ name="notes",
47
+ metadata={"hnsw:space": "cosine"}
48
+ )
49
+
50
+ logger.info(f"VectorStore initialized at {self.persist_directory}")
51
+
52
+ def add_note(self, note: Note) -> bool:
53
+ """添加笔记到向量存储"""
54
+ if self.collection is None:
55
+ self.init()
56
+
57
+ try:
58
+ # 准备文档内容
59
+ document = f"{note.title}\n{note.content}"
60
+
61
+ # 获取 embedding
62
+ from .embedding_backend import get_backend
63
+ backend = get_backend()
64
+ embedding = backend.encode_single(document).tolist()
65
+
66
+ # 添加到 ChromaDB
67
+ self.collection.add(
68
+ ids=[note.id],
69
+ documents=[document],
70
+ embeddings=[embedding],
71
+ metadatas=[{
72
+ "title": note.title,
73
+ "type": note.type.value,
74
+ "filepath": str(note.filepath),
75
+ "tags": ",".join(note.tags),
76
+ }]
77
+ )
78
+
79
+ logger.debug(f"Added note {note.id} to vector store")
80
+ return True
81
+
82
+ except Exception as e:
83
+ logger.error(f"Failed to add note {note.id}: {e}")
84
+ return False
85
+
86
+ def search(
87
+ self,
88
+ query: str,
89
+ top_k: int = 5,
90
+ note_type: Optional[str] = None
91
+ ) -> List[Dict[str, Any]]:
92
+ """语义搜索"""
93
+ if self.collection is None:
94
+ self.init()
95
+
96
+ try:
97
+ # 获取查询向量
98
+ from .embedding_backend import get_backend
99
+ backend = get_backend()
100
+ query_embedding = backend.encode_single(query).tolist()
101
+
102
+ # 构建过滤条件
103
+ where = {}
104
+ if note_type:
105
+ where["type"] = note_type
106
+
107
+ # 搜索
108
+ results = self.collection.query(
109
+ query_embeddings=[query_embedding],
110
+ n_results=top_k,
111
+ where=where if where else None,
112
+ include=["documents", "metadatas", "distances"]
113
+ )
114
+
115
+ # 格式化结果
116
+ formatted_results = []
117
+ for i in range(len(results["ids"][0])):
118
+ formatted_results.append({
119
+ "id": results["ids"][0][i],
120
+ "document": results["documents"][0][i],
121
+ "metadata": results["metadatas"][0][i],
122
+ "distance": results["distances"][0][i],
123
+ "score": 1 - results["distances"][0][i], # 转换为相似度
124
+ })
125
+
126
+ return formatted_results
127
+
128
+ except Exception as e:
129
+ logger.error(f"Search failed: {e}")
130
+ return []
131
+
132
+ def delete_note(self, note_id: str) -> bool:
133
+ """删除笔记"""
134
+ if self.collection is None:
135
+ self.init()
136
+
137
+ try:
138
+ self.collection.delete(ids=[note_id])
139
+ logger.debug(f"Deleted note {note_id} from vector store")
140
+ return True
141
+ except Exception as e:
142
+ logger.error(f"Failed to delete note {note_id}: {e}")
143
+ return False
144
+
145
+ def add_or_update_note(self, note: Note) -> bool:
146
+ """添加或更新笔记(如果已存在则更新)"""
147
+ # 先尝试删除旧的(如果存在)
148
+ try:
149
+ self.collection.delete(ids=[note.id])
150
+ except Exception:
151
+ pass # 可能不存在,忽略错误
152
+
153
+ # 添加新的
154
+ return self.add_note(note)
155
+
156
+ def get_all_ids(self) -> List[str]:
157
+ """获取所有索引的笔记 ID"""
158
+ if self.collection is None:
159
+ self.init()
160
+
161
+ try:
162
+ # 获取所有数据
163
+ result = self.collection.get(include=[])
164
+ return result.get("ids", [])
165
+ except Exception as e:
166
+ logger.error(f"Failed to get all IDs: {e}")
167
+ return []
168
+
169
+ def get_stats(self) -> Dict[str, Any]:
170
+ """获取统计信息"""
171
+ if self.collection is None:
172
+ self.init()
173
+
174
+ try:
175
+ count = self.collection.count()
176
+ return {
177
+ "total_notes": count,
178
+ "persist_directory": str(self.persist_directory),
179
+ }
180
+ except Exception as e:
181
+ logger.error(f"Failed to get stats: {e}")
182
+ return {"total_notes": 0, "error": str(e)}
183
+
184
+
185
+ # 全局向量存储实例
186
+ _vector_store: Optional[VectorStore] = None
187
+
188
+
189
+ def get_vector_store() -> VectorStore:
190
+ """获取全局向量存储实例"""
191
+ global _vector_store
192
+ if _vector_store is None:
193
+ _vector_store = VectorStore()
194
+ return _vector_store
195
+
196
+
197
+ def reset_vector_store():
198
+ """重置全局向量存储实例(用于切换知识库时)"""
199
+ global _vector_store
200
+ _vector_store = None