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/__init__.py +5 -0
- jfox/__main__.py +6 -0
- jfox/bm25_index.py +388 -0
- jfox/cli.py +1895 -0
- jfox/config.py +180 -0
- jfox/embedding_backend.py +65 -0
- jfox/formatters.py +305 -0
- jfox/global_config.py +281 -0
- jfox/graph.py +331 -0
- jfox/indexer.py +366 -0
- jfox/kb_manager.py +316 -0
- jfox/models.py +144 -0
- jfox/note.py +464 -0
- jfox/performance.py +408 -0
- jfox/search_engine.py +237 -0
- jfox/template.py +301 -0
- jfox/template_cli.py +327 -0
- jfox/vector_store.py +200 -0
- jfox_cli-0.1.0.dist-info/METADATA +637 -0
- jfox_cli-0.1.0.dist-info/RECORD +22 -0
- jfox_cli-0.1.0.dist-info/WHEEL +4 -0
- jfox_cli-0.1.0.dist-info/entry_points.txt +2 -0
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
|