mcp-vector-search 0.15.7__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.
Potentially problematic release.
This version of mcp-vector-search might be problematic. Click here for more details.
- mcp_vector_search/__init__.py +10 -0
- mcp_vector_search/cli/__init__.py +1 -0
- mcp_vector_search/cli/commands/__init__.py +1 -0
- mcp_vector_search/cli/commands/auto_index.py +397 -0
- mcp_vector_search/cli/commands/chat.py +534 -0
- mcp_vector_search/cli/commands/config.py +393 -0
- mcp_vector_search/cli/commands/demo.py +358 -0
- mcp_vector_search/cli/commands/index.py +762 -0
- mcp_vector_search/cli/commands/init.py +658 -0
- mcp_vector_search/cli/commands/install.py +869 -0
- mcp_vector_search/cli/commands/install_old.py +700 -0
- mcp_vector_search/cli/commands/mcp.py +1254 -0
- mcp_vector_search/cli/commands/reset.py +393 -0
- mcp_vector_search/cli/commands/search.py +796 -0
- mcp_vector_search/cli/commands/setup.py +1133 -0
- mcp_vector_search/cli/commands/status.py +584 -0
- mcp_vector_search/cli/commands/uninstall.py +404 -0
- mcp_vector_search/cli/commands/visualize/__init__.py +39 -0
- mcp_vector_search/cli/commands/visualize/cli.py +265 -0
- mcp_vector_search/cli/commands/visualize/exporters/__init__.py +12 -0
- mcp_vector_search/cli/commands/visualize/exporters/html_exporter.py +33 -0
- mcp_vector_search/cli/commands/visualize/exporters/json_exporter.py +29 -0
- mcp_vector_search/cli/commands/visualize/graph_builder.py +709 -0
- mcp_vector_search/cli/commands/visualize/layout_engine.py +469 -0
- mcp_vector_search/cli/commands/visualize/server.py +201 -0
- mcp_vector_search/cli/commands/visualize/state_manager.py +428 -0
- mcp_vector_search/cli/commands/visualize/templates/__init__.py +16 -0
- mcp_vector_search/cli/commands/visualize/templates/base.py +218 -0
- mcp_vector_search/cli/commands/visualize/templates/scripts.py +3670 -0
- mcp_vector_search/cli/commands/visualize/templates/styles.py +779 -0
- mcp_vector_search/cli/commands/visualize.py.original +2536 -0
- mcp_vector_search/cli/commands/watch.py +287 -0
- mcp_vector_search/cli/didyoumean.py +520 -0
- mcp_vector_search/cli/export.py +320 -0
- mcp_vector_search/cli/history.py +295 -0
- mcp_vector_search/cli/interactive.py +342 -0
- mcp_vector_search/cli/main.py +484 -0
- mcp_vector_search/cli/output.py +414 -0
- mcp_vector_search/cli/suggestions.py +375 -0
- mcp_vector_search/config/__init__.py +1 -0
- mcp_vector_search/config/constants.py +24 -0
- mcp_vector_search/config/defaults.py +200 -0
- mcp_vector_search/config/settings.py +146 -0
- mcp_vector_search/core/__init__.py +1 -0
- mcp_vector_search/core/auto_indexer.py +298 -0
- mcp_vector_search/core/config_utils.py +394 -0
- mcp_vector_search/core/connection_pool.py +360 -0
- mcp_vector_search/core/database.py +1237 -0
- mcp_vector_search/core/directory_index.py +318 -0
- mcp_vector_search/core/embeddings.py +294 -0
- mcp_vector_search/core/exceptions.py +89 -0
- mcp_vector_search/core/factory.py +318 -0
- mcp_vector_search/core/git_hooks.py +345 -0
- mcp_vector_search/core/indexer.py +1002 -0
- mcp_vector_search/core/llm_client.py +453 -0
- mcp_vector_search/core/models.py +294 -0
- mcp_vector_search/core/project.py +350 -0
- mcp_vector_search/core/scheduler.py +330 -0
- mcp_vector_search/core/search.py +952 -0
- mcp_vector_search/core/watcher.py +322 -0
- mcp_vector_search/mcp/__init__.py +5 -0
- mcp_vector_search/mcp/__main__.py +25 -0
- mcp_vector_search/mcp/server.py +752 -0
- mcp_vector_search/parsers/__init__.py +8 -0
- mcp_vector_search/parsers/base.py +296 -0
- mcp_vector_search/parsers/dart.py +605 -0
- mcp_vector_search/parsers/html.py +413 -0
- mcp_vector_search/parsers/javascript.py +643 -0
- mcp_vector_search/parsers/php.py +694 -0
- mcp_vector_search/parsers/python.py +502 -0
- mcp_vector_search/parsers/registry.py +223 -0
- mcp_vector_search/parsers/ruby.py +678 -0
- mcp_vector_search/parsers/text.py +186 -0
- mcp_vector_search/parsers/utils.py +265 -0
- mcp_vector_search/py.typed +1 -0
- mcp_vector_search/utils/__init__.py +42 -0
- mcp_vector_search/utils/gitignore.py +250 -0
- mcp_vector_search/utils/gitignore_updater.py +212 -0
- mcp_vector_search/utils/monorepo.py +339 -0
- mcp_vector_search/utils/timing.py +338 -0
- mcp_vector_search/utils/version.py +47 -0
- mcp_vector_search-0.15.7.dist-info/METADATA +884 -0
- mcp_vector_search-0.15.7.dist-info/RECORD +86 -0
- mcp_vector_search-0.15.7.dist-info/WHEEL +4 -0
- mcp_vector_search-0.15.7.dist-info/entry_points.txt +3 -0
- mcp_vector_search-0.15.7.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,360 @@
|
|
|
1
|
+
"""Connection pooling for vector database operations."""
|
|
2
|
+
|
|
3
|
+
import asyncio
|
|
4
|
+
import time
|
|
5
|
+
from collections.abc import AsyncGenerator
|
|
6
|
+
from contextlib import asynccontextmanager
|
|
7
|
+
from dataclasses import dataclass
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
from typing import Any
|
|
10
|
+
|
|
11
|
+
from loguru import logger
|
|
12
|
+
|
|
13
|
+
from .exceptions import DatabaseError
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
@dataclass
|
|
17
|
+
class PooledConnection:
|
|
18
|
+
"""Represents a pooled database connection."""
|
|
19
|
+
|
|
20
|
+
client: Any
|
|
21
|
+
collection: Any
|
|
22
|
+
created_at: float
|
|
23
|
+
last_used: float
|
|
24
|
+
in_use: bool = False
|
|
25
|
+
use_count: int = 0
|
|
26
|
+
|
|
27
|
+
@property
|
|
28
|
+
def age(self) -> float:
|
|
29
|
+
"""Get the age of this connection in seconds."""
|
|
30
|
+
return time.time() - self.created_at
|
|
31
|
+
|
|
32
|
+
@property
|
|
33
|
+
def idle_time(self) -> float:
|
|
34
|
+
"""Get the idle time of this connection in seconds."""
|
|
35
|
+
return time.time() - self.last_used
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
class ChromaConnectionPool:
|
|
39
|
+
"""Connection pool for ChromaDB operations."""
|
|
40
|
+
|
|
41
|
+
def __init__(
|
|
42
|
+
self,
|
|
43
|
+
persist_directory: Path,
|
|
44
|
+
embedding_function: Any,
|
|
45
|
+
collection_name: str = "code_search",
|
|
46
|
+
max_connections: int = 10,
|
|
47
|
+
min_connections: int = 2,
|
|
48
|
+
max_idle_time: float = 300.0, # 5 minutes
|
|
49
|
+
max_connection_age: float = 3600.0, # 1 hour
|
|
50
|
+
):
|
|
51
|
+
"""Initialize connection pool.
|
|
52
|
+
|
|
53
|
+
Args:
|
|
54
|
+
persist_directory: Directory to persist database
|
|
55
|
+
embedding_function: Function to generate embeddings
|
|
56
|
+
collection_name: Name of the collection
|
|
57
|
+
max_connections: Maximum number of connections in pool
|
|
58
|
+
min_connections: Minimum number of connections to maintain
|
|
59
|
+
max_idle_time: Maximum time a connection can be idle (seconds)
|
|
60
|
+
max_connection_age: Maximum age of a connection (seconds)
|
|
61
|
+
"""
|
|
62
|
+
self.persist_directory = persist_directory
|
|
63
|
+
self.embedding_function = embedding_function
|
|
64
|
+
self.collection_name = collection_name
|
|
65
|
+
self.max_connections = max_connections
|
|
66
|
+
self.min_connections = min_connections
|
|
67
|
+
self.max_idle_time = max_idle_time
|
|
68
|
+
self.max_connection_age = max_connection_age
|
|
69
|
+
|
|
70
|
+
self._pool: list[PooledConnection] = []
|
|
71
|
+
self._lock = asyncio.Lock()
|
|
72
|
+
self._initialized = False
|
|
73
|
+
self._cleanup_task: asyncio.Task | None = None
|
|
74
|
+
|
|
75
|
+
# Statistics
|
|
76
|
+
self._stats = {
|
|
77
|
+
"connections_created": 0,
|
|
78
|
+
"connections_reused": 0,
|
|
79
|
+
"connections_expired": 0,
|
|
80
|
+
"pool_hits": 0,
|
|
81
|
+
"pool_misses": 0,
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
async def initialize(self) -> None:
|
|
85
|
+
"""Initialize the connection pool."""
|
|
86
|
+
if self._initialized:
|
|
87
|
+
return
|
|
88
|
+
|
|
89
|
+
async with self._lock:
|
|
90
|
+
if self._initialized:
|
|
91
|
+
return
|
|
92
|
+
|
|
93
|
+
# Create minimum number of connections
|
|
94
|
+
for _ in range(self.min_connections):
|
|
95
|
+
conn = await self._create_connection()
|
|
96
|
+
self._pool.append(conn)
|
|
97
|
+
|
|
98
|
+
# Start cleanup task
|
|
99
|
+
self._cleanup_task = asyncio.create_task(self._cleanup_loop())
|
|
100
|
+
self._initialized = True
|
|
101
|
+
|
|
102
|
+
logger.info(
|
|
103
|
+
f"Connection pool initialized with {len(self._pool)} connections"
|
|
104
|
+
)
|
|
105
|
+
|
|
106
|
+
async def close(self) -> None:
|
|
107
|
+
"""Close all connections and cleanup."""
|
|
108
|
+
if not self._initialized:
|
|
109
|
+
return
|
|
110
|
+
|
|
111
|
+
async with self._lock:
|
|
112
|
+
# Cancel cleanup task
|
|
113
|
+
if self._cleanup_task:
|
|
114
|
+
self._cleanup_task.cancel()
|
|
115
|
+
try:
|
|
116
|
+
await self._cleanup_task
|
|
117
|
+
except asyncio.CancelledError:
|
|
118
|
+
pass
|
|
119
|
+
|
|
120
|
+
# Close all connections
|
|
121
|
+
for conn in self._pool:
|
|
122
|
+
await self._close_connection(conn)
|
|
123
|
+
|
|
124
|
+
self._pool.clear()
|
|
125
|
+
self._initialized = False
|
|
126
|
+
|
|
127
|
+
logger.info("Connection pool closed")
|
|
128
|
+
|
|
129
|
+
async def _create_connection(self) -> PooledConnection:
|
|
130
|
+
"""Create a new database connection."""
|
|
131
|
+
try:
|
|
132
|
+
import chromadb
|
|
133
|
+
|
|
134
|
+
# Ensure directory exists
|
|
135
|
+
self.persist_directory.mkdir(parents=True, exist_ok=True)
|
|
136
|
+
|
|
137
|
+
# Create client
|
|
138
|
+
client = chromadb.PersistentClient(
|
|
139
|
+
path=str(self.persist_directory),
|
|
140
|
+
settings=chromadb.Settings(
|
|
141
|
+
anonymized_telemetry=False,
|
|
142
|
+
allow_reset=True,
|
|
143
|
+
),
|
|
144
|
+
)
|
|
145
|
+
|
|
146
|
+
# Create or get collection
|
|
147
|
+
collection = client.get_or_create_collection(
|
|
148
|
+
name=self.collection_name,
|
|
149
|
+
embedding_function=self.embedding_function,
|
|
150
|
+
metadata={
|
|
151
|
+
"description": "Semantic code search collection",
|
|
152
|
+
},
|
|
153
|
+
)
|
|
154
|
+
|
|
155
|
+
conn = PooledConnection(
|
|
156
|
+
client=client,
|
|
157
|
+
collection=collection,
|
|
158
|
+
created_at=time.time(),
|
|
159
|
+
last_used=time.time(),
|
|
160
|
+
)
|
|
161
|
+
|
|
162
|
+
self._stats["connections_created"] += 1
|
|
163
|
+
logger.debug(
|
|
164
|
+
f"Created new database connection (total: {self._stats['connections_created']})"
|
|
165
|
+
)
|
|
166
|
+
|
|
167
|
+
return conn
|
|
168
|
+
|
|
169
|
+
except Exception as e:
|
|
170
|
+
logger.error(f"Failed to create database connection: {e}")
|
|
171
|
+
raise DatabaseError(f"Connection creation failed: {e}") from e
|
|
172
|
+
|
|
173
|
+
async def _close_connection(self, conn: PooledConnection) -> None:
|
|
174
|
+
"""Close a database connection."""
|
|
175
|
+
try:
|
|
176
|
+
# ChromaDB doesn't require explicit closing
|
|
177
|
+
conn.client = None
|
|
178
|
+
conn.collection = None
|
|
179
|
+
logger.debug("Closed database connection")
|
|
180
|
+
except Exception as e:
|
|
181
|
+
logger.warning(f"Error closing connection: {e}")
|
|
182
|
+
|
|
183
|
+
@asynccontextmanager
|
|
184
|
+
async def get_connection(self) -> AsyncGenerator[PooledConnection, None]:
|
|
185
|
+
"""Get a connection from the pool."""
|
|
186
|
+
if not self._initialized:
|
|
187
|
+
await self.initialize()
|
|
188
|
+
|
|
189
|
+
conn = None
|
|
190
|
+
try:
|
|
191
|
+
# Get connection from pool
|
|
192
|
+
conn = await self._acquire_connection()
|
|
193
|
+
yield conn
|
|
194
|
+
finally:
|
|
195
|
+
# Return connection to pool
|
|
196
|
+
if conn:
|
|
197
|
+
await self._release_connection(conn)
|
|
198
|
+
|
|
199
|
+
async def _acquire_connection(self) -> PooledConnection:
|
|
200
|
+
"""Acquire a connection from the pool."""
|
|
201
|
+
async with self._lock:
|
|
202
|
+
# Try to find an available connection
|
|
203
|
+
for conn in self._pool:
|
|
204
|
+
if not conn.in_use and self._is_connection_valid(conn):
|
|
205
|
+
conn.in_use = True
|
|
206
|
+
conn.last_used = time.time()
|
|
207
|
+
conn.use_count += 1
|
|
208
|
+
self._stats["pool_hits"] += 1
|
|
209
|
+
self._stats["connections_reused"] += 1
|
|
210
|
+
logger.debug(f"Reused connection (use count: {conn.use_count})")
|
|
211
|
+
return conn
|
|
212
|
+
|
|
213
|
+
# No available connection, create new one if under limit
|
|
214
|
+
if len(self._pool) < self.max_connections:
|
|
215
|
+
conn = await self._create_connection()
|
|
216
|
+
conn.in_use = True
|
|
217
|
+
self._pool.append(conn)
|
|
218
|
+
self._stats["pool_misses"] += 1
|
|
219
|
+
logger.debug(f"Created new connection (pool size: {len(self._pool)})")
|
|
220
|
+
return conn
|
|
221
|
+
|
|
222
|
+
# Pool is full, wait for a connection to become available (outside lock)
|
|
223
|
+
self._stats["pool_misses"] += 1
|
|
224
|
+
logger.warning("Connection pool exhausted, waiting for available connection")
|
|
225
|
+
|
|
226
|
+
# Wait for a connection (with timeout) - release lock during wait
|
|
227
|
+
timeout = 30.0 # 30 seconds
|
|
228
|
+
start_time = time.time()
|
|
229
|
+
|
|
230
|
+
while time.time() - start_time < timeout:
|
|
231
|
+
await asyncio.sleep(0.1)
|
|
232
|
+
# Re-acquire lock to check for available connections
|
|
233
|
+
async with self._lock:
|
|
234
|
+
for conn in self._pool:
|
|
235
|
+
if not conn.in_use and self._is_connection_valid(conn):
|
|
236
|
+
conn.in_use = True
|
|
237
|
+
conn.last_used = time.time()
|
|
238
|
+
conn.use_count += 1
|
|
239
|
+
self._stats["connections_reused"] += 1
|
|
240
|
+
return conn
|
|
241
|
+
|
|
242
|
+
raise DatabaseError("Connection pool timeout: no connections available")
|
|
243
|
+
|
|
244
|
+
async def _release_connection(self, conn: PooledConnection) -> None:
|
|
245
|
+
"""Release a connection back to the pool."""
|
|
246
|
+
async with self._lock:
|
|
247
|
+
conn.in_use = False
|
|
248
|
+
conn.last_used = time.time()
|
|
249
|
+
logger.debug(f"Released connection (use count: {conn.use_count})")
|
|
250
|
+
|
|
251
|
+
def _is_connection_valid(self, conn: PooledConnection) -> bool:
|
|
252
|
+
"""Check if a connection is still valid."""
|
|
253
|
+
now = time.time()
|
|
254
|
+
|
|
255
|
+
# Check age
|
|
256
|
+
if now - conn.created_at > self.max_connection_age:
|
|
257
|
+
return False
|
|
258
|
+
|
|
259
|
+
# Check if idle too long
|
|
260
|
+
if now - conn.last_used > self.max_idle_time:
|
|
261
|
+
return False
|
|
262
|
+
|
|
263
|
+
# Check if client/collection are still valid
|
|
264
|
+
if not conn.client or not conn.collection:
|
|
265
|
+
return False
|
|
266
|
+
|
|
267
|
+
return True
|
|
268
|
+
|
|
269
|
+
async def _cleanup_loop(self) -> None:
|
|
270
|
+
"""Background task to cleanup expired connections."""
|
|
271
|
+
while True:
|
|
272
|
+
try:
|
|
273
|
+
await asyncio.sleep(60) # Check every minute
|
|
274
|
+
await self._cleanup_expired_connections()
|
|
275
|
+
except asyncio.CancelledError:
|
|
276
|
+
break
|
|
277
|
+
except Exception as e:
|
|
278
|
+
logger.error(f"Error in connection cleanup: {e}")
|
|
279
|
+
|
|
280
|
+
async def _cleanup_expired_connections(self) -> None:
|
|
281
|
+
"""Remove expired connections from the pool."""
|
|
282
|
+
async with self._lock:
|
|
283
|
+
expired_connections = []
|
|
284
|
+
|
|
285
|
+
for conn in self._pool:
|
|
286
|
+
if not conn.in_use and not self._is_connection_valid(conn):
|
|
287
|
+
expired_connections.append(conn)
|
|
288
|
+
|
|
289
|
+
# Remove expired connections
|
|
290
|
+
for conn in expired_connections:
|
|
291
|
+
self._pool.remove(conn)
|
|
292
|
+
await self._close_connection(conn)
|
|
293
|
+
self._stats["connections_expired"] += 1
|
|
294
|
+
|
|
295
|
+
if expired_connections:
|
|
296
|
+
logger.debug(
|
|
297
|
+
f"Cleaned up {len(expired_connections)} expired connections"
|
|
298
|
+
)
|
|
299
|
+
|
|
300
|
+
# Ensure minimum connections
|
|
301
|
+
while len(self._pool) < self.min_connections:
|
|
302
|
+
try:
|
|
303
|
+
conn = await self._create_connection()
|
|
304
|
+
self._pool.append(conn)
|
|
305
|
+
except Exception as e:
|
|
306
|
+
logger.error(f"Failed to create minimum connection: {e}")
|
|
307
|
+
break
|
|
308
|
+
|
|
309
|
+
def get_stats(self) -> dict[str, Any]:
|
|
310
|
+
"""Get connection pool statistics."""
|
|
311
|
+
active_connections = sum(1 for conn in self._pool if conn.in_use)
|
|
312
|
+
idle_connections = len(self._pool) - active_connections
|
|
313
|
+
|
|
314
|
+
return {
|
|
315
|
+
**self._stats,
|
|
316
|
+
"pool_size": len(self._pool),
|
|
317
|
+
"active_connections": active_connections,
|
|
318
|
+
"idle_connections": idle_connections,
|
|
319
|
+
"max_connections": self.max_connections,
|
|
320
|
+
"min_connections": self.min_connections,
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
async def health_check(self) -> bool:
|
|
324
|
+
"""Perform a health check on the connection pool."""
|
|
325
|
+
try:
|
|
326
|
+
async with self.get_connection() as conn:
|
|
327
|
+
# Try a simple operation
|
|
328
|
+
conn.collection.count()
|
|
329
|
+
return True
|
|
330
|
+
except Exception as e:
|
|
331
|
+
logger.error(f"Connection pool health check failed: {e}")
|
|
332
|
+
return False
|
|
333
|
+
|
|
334
|
+
# Backward compatibility aliases for old test API
|
|
335
|
+
async def cleanup(self) -> None:
|
|
336
|
+
"""Alias for close() method (backward compatibility)."""
|
|
337
|
+
await self.close()
|
|
338
|
+
|
|
339
|
+
def _validate_connection(self, conn: PooledConnection) -> bool:
|
|
340
|
+
"""Alias for _is_connection_valid() method (backward compatibility)."""
|
|
341
|
+
return self._is_connection_valid(conn)
|
|
342
|
+
|
|
343
|
+
async def _cleanup_idle_connections(self) -> None:
|
|
344
|
+
"""Alias for _cleanup_expired_connections() method (backward compatibility)."""
|
|
345
|
+
await self._cleanup_expired_connections()
|
|
346
|
+
|
|
347
|
+
@property
|
|
348
|
+
def _connections(self) -> list[PooledConnection]:
|
|
349
|
+
"""Alias for _pool attribute (backward compatibility)."""
|
|
350
|
+
return self._pool
|
|
351
|
+
|
|
352
|
+
@property
|
|
353
|
+
def _max_connections(self) -> int:
|
|
354
|
+
"""Alias for max_connections attribute (backward compatibility)."""
|
|
355
|
+
return self.max_connections
|
|
356
|
+
|
|
357
|
+
@property
|
|
358
|
+
def _min_connections(self) -> int:
|
|
359
|
+
"""Alias for min_connections attribute (backward compatibility)."""
|
|
360
|
+
return self.min_connections
|