dbs-vector 0.5.1__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.
- dbs_vector/__init__.py +6 -0
- dbs_vector/api/__init__.py +0 -0
- dbs_vector/api/main.py +137 -0
- dbs_vector/api/mcp_server.py +100 -0
- dbs_vector/api/state.py +18 -0
- dbs_vector/cli.py +264 -0
- dbs_vector/config.py +110 -0
- dbs_vector/core/__init__.py +0 -0
- dbs_vector/core/models.py +107 -0
- dbs_vector/core/ports.py +89 -0
- dbs_vector/core/registry.py +36 -0
- dbs_vector/infrastructure/__init__.py +0 -0
- dbs_vector/infrastructure/chunking/__init__.py +0 -0
- dbs_vector/infrastructure/chunking/api.py +139 -0
- dbs_vector/infrastructure/chunking/document.py +100 -0
- dbs_vector/infrastructure/chunking/duckdb.py +119 -0
- dbs_vector/infrastructure/chunking/sql.py +65 -0
- dbs_vector/infrastructure/embeddings/__init__.py +0 -0
- dbs_vector/infrastructure/embeddings/mlx_engine.py +106 -0
- dbs_vector/infrastructure/storage/__init__.py +0 -0
- dbs_vector/infrastructure/storage/lancedb_engine.py +145 -0
- dbs_vector/infrastructure/storage/mappers.py +174 -0
- dbs_vector/logger.py +43 -0
- dbs_vector/py.typed +0 -0
- dbs_vector/services/__init__.py +0 -0
- dbs_vector/services/ingestion.py +127 -0
- dbs_vector/services/search.py +76 -0
- dbs_vector-0.5.1.dist-info/METADATA +178 -0
- dbs_vector-0.5.1.dist-info/RECORD +32 -0
- dbs_vector-0.5.1.dist-info/WHEEL +4 -0
- dbs_vector-0.5.1.dist-info/entry_points.txt +2 -0
- dbs_vector-0.5.1.dist-info/licenses/LICENSE.md +10 -0
dbs_vector/__init__.py
ADDED
|
File without changes
|
dbs_vector/api/main.py
ADDED
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
from collections.abc import AsyncGenerator
|
|
3
|
+
from contextlib import asynccontextmanager
|
|
4
|
+
|
|
5
|
+
from fastapi import FastAPI, HTTPException
|
|
6
|
+
from fastapi.middleware.cors import CORSMiddleware
|
|
7
|
+
from loguru import logger
|
|
8
|
+
from pydantic import BaseModel, Field
|
|
9
|
+
|
|
10
|
+
from dbs_vector.api.mcp_server import mcp
|
|
11
|
+
from dbs_vector.api.state import _services, initialize_services
|
|
12
|
+
from dbs_vector.config import settings
|
|
13
|
+
from dbs_vector.core.models import SearchResult, SqlSearchResult
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
@asynccontextmanager
|
|
17
|
+
async def lifespan(app: FastAPI) -> AsyncGenerator[None, None]:
|
|
18
|
+
"""Startup and shutdown events for the API."""
|
|
19
|
+
logger.info("Initializing MLX Embedders and LanceDB connections")
|
|
20
|
+
|
|
21
|
+
try:
|
|
22
|
+
initialize_services()
|
|
23
|
+
logger.success("API is ready to accept concurrent requests")
|
|
24
|
+
except Exception as e:
|
|
25
|
+
logger.error("Failed to initialize search services: {}", e)
|
|
26
|
+
raise
|
|
27
|
+
|
|
28
|
+
async with mcp.session_manager.run():
|
|
29
|
+
yield
|
|
30
|
+
|
|
31
|
+
logger.info("Cleaning up resources")
|
|
32
|
+
_services.clear()
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
app = FastAPI(
|
|
36
|
+
title="dbs-vector Search API",
|
|
37
|
+
description="Async API for high-performance Arrow-native local codebase search.",
|
|
38
|
+
version="0.1.0",
|
|
39
|
+
lifespan=lifespan,
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
app.add_middleware(
|
|
43
|
+
CORSMiddleware,
|
|
44
|
+
allow_origins=["https://claude.ai"],
|
|
45
|
+
allow_methods=["GET", "POST", "OPTIONS"],
|
|
46
|
+
allow_headers=["*"],
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
app.mount("/mcp", mcp.streamable_http_app())
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
class SearchRequest(BaseModel):
|
|
53
|
+
"""Schema for a standard document search request."""
|
|
54
|
+
|
|
55
|
+
query: str = Field(..., description="The semantic search query.")
|
|
56
|
+
limit: int = Field(5, ge=1, le=100, description="Maximum number of results to return.")
|
|
57
|
+
source_filter: str | None = Field(None, description="Optional path/file to filter the search.")
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
class SqlSearchRequest(BaseModel):
|
|
61
|
+
"""Schema for an SQL search request."""
|
|
62
|
+
|
|
63
|
+
query: str = Field(..., description="The semantic SQL search query.")
|
|
64
|
+
limit: int = Field(5, ge=1, le=100, description="Maximum number of results to return.")
|
|
65
|
+
source_filter: str | None = Field(None, description="Optional database to filter the search.")
|
|
66
|
+
min_time: float | None = Field(None, description="Minimum execution time in ms.")
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
class SearchResponse(BaseModel):
|
|
70
|
+
"""Schema for returning standard search results."""
|
|
71
|
+
|
|
72
|
+
query: str
|
|
73
|
+
results: list[SearchResult]
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
class SqlSearchResponse(BaseModel):
|
|
77
|
+
"""Schema for returning SQL search results."""
|
|
78
|
+
|
|
79
|
+
query: str
|
|
80
|
+
results: list[SqlSearchResult]
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
@app.get("/health")
|
|
84
|
+
async def health_check() -> dict[str, str]:
|
|
85
|
+
"""Basic health check endpoint."""
|
|
86
|
+
if not _services:
|
|
87
|
+
raise HTTPException(status_code=503, detail="Search service initializing or failed")
|
|
88
|
+
|
|
89
|
+
status_dict = {"status": "healthy"}
|
|
90
|
+
for engine_name, config in settings.engines.items():
|
|
91
|
+
status_dict[f"{engine_name}_model"] = config.model_name
|
|
92
|
+
|
|
93
|
+
return status_dict
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
@app.post("/search/md", response_model=SearchResponse)
|
|
97
|
+
async def search_md(request: SearchRequest) -> SearchResponse:
|
|
98
|
+
"""Executes a hybrid search asynchronously for documents."""
|
|
99
|
+
service = _services.get("md")
|
|
100
|
+
if not service:
|
|
101
|
+
raise HTTPException(status_code=503, detail="Document search service is not initialized.")
|
|
102
|
+
|
|
103
|
+
try:
|
|
104
|
+
results = await asyncio.to_thread(
|
|
105
|
+
service.execute_query,
|
|
106
|
+
request.query,
|
|
107
|
+
request.source_filter,
|
|
108
|
+
request.limit,
|
|
109
|
+
extra_filters={},
|
|
110
|
+
)
|
|
111
|
+
return SearchResponse(query=request.query, results=results) # type: ignore
|
|
112
|
+
except Exception as e:
|
|
113
|
+
raise HTTPException(status_code=500, detail=f"Search execution failed: {e}") from e
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
@app.post("/search/sql", response_model=SqlSearchResponse)
|
|
117
|
+
async def search_sql(request: SqlSearchRequest) -> SqlSearchResponse:
|
|
118
|
+
"""Executes a hybrid search asynchronously for SQL queries."""
|
|
119
|
+
service = _services.get("sql")
|
|
120
|
+
if not service:
|
|
121
|
+
raise HTTPException(status_code=503, detail="SQL search service is not initialized.")
|
|
122
|
+
|
|
123
|
+
extra_filters = {}
|
|
124
|
+
if request.min_time is not None:
|
|
125
|
+
extra_filters["min_time"] = request.min_time
|
|
126
|
+
|
|
127
|
+
try:
|
|
128
|
+
results = await asyncio.to_thread(
|
|
129
|
+
service.execute_query,
|
|
130
|
+
request.query,
|
|
131
|
+
request.source_filter,
|
|
132
|
+
request.limit,
|
|
133
|
+
extra_filters=extra_filters,
|
|
134
|
+
)
|
|
135
|
+
return SqlSearchResponse(query=request.query, results=results) # type: ignore
|
|
136
|
+
except Exception as e:
|
|
137
|
+
raise HTTPException(status_code=500, detail=f"Search execution failed: {e}") from e
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
from mcp.server.fastmcp import FastMCP
|
|
2
|
+
|
|
3
|
+
from dbs_vector.api.state import _services
|
|
4
|
+
|
|
5
|
+
mcp = FastMCP(
|
|
6
|
+
"dbs-vector",
|
|
7
|
+
stateless_http=True,
|
|
8
|
+
streamable_http_path="/",
|
|
9
|
+
)
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
@mcp.tool()
|
|
13
|
+
async def search_documents(query: str, limit: int = 5, source_filter: str | None = None) -> str:
|
|
14
|
+
"""
|
|
15
|
+
Search indexed codebase documents (Markdown, Python, etc.) via semantic vector search.
|
|
16
|
+
|
|
17
|
+
Args:
|
|
18
|
+
query: The semantic search query or concept you are looking for.
|
|
19
|
+
limit: Maximum number of results to return.
|
|
20
|
+
source_filter: Optional file path or pattern to restrict the search.
|
|
21
|
+
"""
|
|
22
|
+
service = _services.get("md")
|
|
23
|
+
if not service:
|
|
24
|
+
return "Error: Document search service ('md' engine) is not initialized."
|
|
25
|
+
|
|
26
|
+
try:
|
|
27
|
+
# execute_query is synchronous, but we can call it directly since MCP runs locally
|
|
28
|
+
results = service.execute_query(
|
|
29
|
+
query=query,
|
|
30
|
+
source_filter=source_filter,
|
|
31
|
+
limit=limit,
|
|
32
|
+
extra_filters={},
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
if not results:
|
|
36
|
+
return f"No results found for query: '{query}'"
|
|
37
|
+
|
|
38
|
+
output = [f"Found {len(results)} results for '{query}':\n"]
|
|
39
|
+
for res in results:
|
|
40
|
+
dist_str = f"{res.distance:.4f}" if res.distance is not None else "N/A (FTS)"
|
|
41
|
+
chunk = res.chunk
|
|
42
|
+
output.append(
|
|
43
|
+
f"--- Result (Score: {dist_str}) ---\n"
|
|
44
|
+
f"Source: {chunk.source}\n"
|
|
45
|
+
f"Content:\n{chunk.text}\n"
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
return "\n".join(output)
|
|
49
|
+
|
|
50
|
+
except Exception as e:
|
|
51
|
+
return f"Search execution failed: {e}"
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
@mcp.tool()
|
|
55
|
+
async def search_sql_logs(
|
|
56
|
+
query: str, limit: int = 5, source_filter: str | None = None, min_time: float | None = None
|
|
57
|
+
) -> str:
|
|
58
|
+
"""
|
|
59
|
+
Search indexed SQL query logs via semantic vector search.
|
|
60
|
+
|
|
61
|
+
Args:
|
|
62
|
+
query: The semantic search query, e.g. 'find user by email' or partial SQL.
|
|
63
|
+
limit: Maximum number of results to return.
|
|
64
|
+
source_filter: Optional database name to restrict the search.
|
|
65
|
+
min_time: Minimum execution time in milliseconds.
|
|
66
|
+
"""
|
|
67
|
+
service = _services.get("sql")
|
|
68
|
+
if not service:
|
|
69
|
+
return "Error: SQL search service ('sql' engine) is not initialized."
|
|
70
|
+
|
|
71
|
+
extra_filters = {}
|
|
72
|
+
if min_time is not None:
|
|
73
|
+
extra_filters["min_time"] = min_time
|
|
74
|
+
|
|
75
|
+
try:
|
|
76
|
+
results = service.execute_query(
|
|
77
|
+
query=query,
|
|
78
|
+
source_filter=source_filter,
|
|
79
|
+
limit=limit,
|
|
80
|
+
extra_filters=extra_filters,
|
|
81
|
+
)
|
|
82
|
+
|
|
83
|
+
if not results:
|
|
84
|
+
return f"No results found for query: '{query}'"
|
|
85
|
+
|
|
86
|
+
output = [f"Found {len(results)} results for '{query}':\n"]
|
|
87
|
+
for res in results:
|
|
88
|
+
dist_str = f"{res.distance:.4f}" if res.distance is not None else "N/A (FTS)"
|
|
89
|
+
chunk = res.chunk
|
|
90
|
+
output.append(
|
|
91
|
+
f"--- Result (Score: {dist_str}) ---\n"
|
|
92
|
+
f"Source Database: {chunk.source}\n"
|
|
93
|
+
f"Execution Time: {chunk.execution_time_ms}ms (Calls: {chunk.calls})\n"
|
|
94
|
+
f"SQL Query:\n{chunk.raw_query}\n"
|
|
95
|
+
)
|
|
96
|
+
|
|
97
|
+
return "\n".join(output)
|
|
98
|
+
|
|
99
|
+
except Exception as e:
|
|
100
|
+
return f"Search execution failed: {e}"
|
dbs_vector/api/state.py
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
from loguru import logger
|
|
2
|
+
|
|
3
|
+
from dbs_vector.cli import _build_dependencies
|
|
4
|
+
from dbs_vector.config import settings
|
|
5
|
+
from dbs_vector.services.search import SearchService
|
|
6
|
+
|
|
7
|
+
# Global service instances holding the initialized models and databases
|
|
8
|
+
_services: dict[str, SearchService] = {}
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def initialize_services() -> dict[str, SearchService]:
|
|
12
|
+
"""Initialize configured search services and return the service map."""
|
|
13
|
+
_services.clear()
|
|
14
|
+
for engine_name in settings.engines.keys():
|
|
15
|
+
logger.info("Loading engine: {}", engine_name)
|
|
16
|
+
deps = _build_dependencies(engine_name)
|
|
17
|
+
_services[engine_name] = SearchService(deps.embedder, deps.store)
|
|
18
|
+
return _services
|
dbs_vector/cli.py
ADDED
|
@@ -0,0 +1,264 @@
|
|
|
1
|
+
import os
|
|
2
|
+
|
|
3
|
+
# Suppress Hugging Face progress bars BEFORE any imports that might use huggingface_hub
|
|
4
|
+
os.environ["HF_HUB_DISABLE_PROGRESS_BARS"] = "1"
|
|
5
|
+
os.environ["HF_HUB_DISABLE_TELEMETRY"] = "1"
|
|
6
|
+
os.environ["HF_HUB_ENABLE_HF_TRANSFER"] = "0" # Disable hf-transfer to use standard downloads
|
|
7
|
+
os.environ["TRANSFORMERS_VERBOSITY"] = "error"
|
|
8
|
+
|
|
9
|
+
from typing import Annotated, Any, NamedTuple
|
|
10
|
+
|
|
11
|
+
import typer
|
|
12
|
+
from loguru import logger
|
|
13
|
+
|
|
14
|
+
from dbs_vector.config import settings
|
|
15
|
+
from dbs_vector.core.registry import ComponentRegistry
|
|
16
|
+
from dbs_vector.infrastructure.embeddings.mlx_engine import MLXEmbedder
|
|
17
|
+
from dbs_vector.infrastructure.storage.lancedb_engine import LanceDBStore
|
|
18
|
+
from dbs_vector.logger import configure_logger
|
|
19
|
+
from dbs_vector.services.ingestion import IngestionService
|
|
20
|
+
from dbs_vector.services.search import SearchService
|
|
21
|
+
|
|
22
|
+
app = typer.Typer(
|
|
23
|
+
help="dbs-vector: Local Arrow-Native Codebase Search Engine",
|
|
24
|
+
no_args_is_help=True,
|
|
25
|
+
rich_markup_mode=None,
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class EngineDeps(NamedTuple):
|
|
30
|
+
"""Container for resolved engine dependencies."""
|
|
31
|
+
|
|
32
|
+
embedder: Any
|
|
33
|
+
store: Any
|
|
34
|
+
chunker: Any
|
|
35
|
+
workflow: str
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def version_callback(value: bool) -> None:
|
|
39
|
+
if value:
|
|
40
|
+
from dbs_vector import __version__
|
|
41
|
+
|
|
42
|
+
typer.echo(f"dbs-vector version: {__version__}")
|
|
43
|
+
raise typer.Exit()
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
@app.callback()
|
|
47
|
+
def main(
|
|
48
|
+
ctx: typer.Context,
|
|
49
|
+
config_file: Annotated[
|
|
50
|
+
str, typer.Option("--config-file", "-c", help="Path to config.yaml file.")
|
|
51
|
+
] = "config.yaml",
|
|
52
|
+
version: Annotated[
|
|
53
|
+
bool | None,
|
|
54
|
+
typer.Option(
|
|
55
|
+
"--version",
|
|
56
|
+
"-v",
|
|
57
|
+
help="Show the version and exit.",
|
|
58
|
+
callback=version_callback,
|
|
59
|
+
is_eager=True,
|
|
60
|
+
),
|
|
61
|
+
] = None,
|
|
62
|
+
) -> None:
|
|
63
|
+
"""dbs-vector: Configurable Arrow-Native Search Engine."""
|
|
64
|
+
# Skip config loading when just showing help or version (no subcommand invoked)
|
|
65
|
+
if ctx.invoked_subcommand is None:
|
|
66
|
+
return
|
|
67
|
+
|
|
68
|
+
import os
|
|
69
|
+
|
|
70
|
+
from dbs_vector.config import load_settings, settings
|
|
71
|
+
|
|
72
|
+
# Export to environment so uvicorn subprocesses (in API mode) inherit it
|
|
73
|
+
os.environ["DBS_CONFIG_FILE"] = config_file
|
|
74
|
+
|
|
75
|
+
# Dynamically update the current process global settings singleton
|
|
76
|
+
new_settings = load_settings(config_file)
|
|
77
|
+
settings.db_path = new_settings.db_path
|
|
78
|
+
settings.batch_size = new_settings.batch_size
|
|
79
|
+
settings.nprobes = new_settings.nprobes
|
|
80
|
+
settings.engines = new_settings.engines
|
|
81
|
+
settings.log_level = new_settings.log_level
|
|
82
|
+
settings.log_serialize = new_settings.log_serialize
|
|
83
|
+
|
|
84
|
+
# Configure logger based on settings
|
|
85
|
+
configure_logger(level=settings.log_level, serialize=settings.log_serialize)
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def _build_dependencies(
|
|
89
|
+
engine_name: str,
|
|
90
|
+
query_override: str | None = None,
|
|
91
|
+
url_override: str | None = None,
|
|
92
|
+
) -> EngineDeps:
|
|
93
|
+
"""Dependency Injection Factory driven by config.yaml configuration."""
|
|
94
|
+
if engine_name not in settings.engines:
|
|
95
|
+
raise ValueError(
|
|
96
|
+
f"Unknown engine: '{engine_name}'. Check {os.environ.get('DBS_CONFIG_FILE', 'config.yaml')}."
|
|
97
|
+
)
|
|
98
|
+
|
|
99
|
+
config = settings.engines[engine_name]
|
|
100
|
+
|
|
101
|
+
# Initialize Embedder
|
|
102
|
+
embedder = MLXEmbedder(
|
|
103
|
+
model_name=config.model_name,
|
|
104
|
+
max_token_length=config.max_token_length,
|
|
105
|
+
dimension=config.vector_dimension,
|
|
106
|
+
passage_prefix=config.passage_prefix,
|
|
107
|
+
query_prefix=config.query_prefix,
|
|
108
|
+
)
|
|
109
|
+
|
|
110
|
+
# Resolve components via Registry
|
|
111
|
+
MapperClass = ComponentRegistry.get_mapper(config.mapper_type)
|
|
112
|
+
ChunkerClass = ComponentRegistry.get_chunker(config.chunker_type)
|
|
113
|
+
|
|
114
|
+
mapper = MapperClass(vector_dimension=config.vector_dimension)
|
|
115
|
+
|
|
116
|
+
chunker = ChunkerClass(
|
|
117
|
+
**config.chunker_kwargs(query_override=query_override, url_override=url_override)
|
|
118
|
+
)
|
|
119
|
+
|
|
120
|
+
try:
|
|
121
|
+
store = LanceDBStore(
|
|
122
|
+
db_path=settings.db_path,
|
|
123
|
+
table_name=config.table_name,
|
|
124
|
+
vector_dimension=config.vector_dimension,
|
|
125
|
+
mapper=mapper,
|
|
126
|
+
nprobes=settings.nprobes,
|
|
127
|
+
)
|
|
128
|
+
except ValueError as e:
|
|
129
|
+
if "Schema mismatch" in str(e):
|
|
130
|
+
typer.echo(f"\n[!] Database Error: {e}", err=True)
|
|
131
|
+
raise typer.Exit(code=1) from e
|
|
132
|
+
raise
|
|
133
|
+
|
|
134
|
+
return EngineDeps(embedder=embedder, store=store, chunker=chunker, workflow=config.workflow)
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
@app.command()
|
|
138
|
+
def ingest(
|
|
139
|
+
path: Annotated[
|
|
140
|
+
str, typer.Argument(help="Directory path, glob pattern, or JSON file to ingest.")
|
|
141
|
+
],
|
|
142
|
+
engine_name: Annotated[
|
|
143
|
+
str, typer.Option("--type", "-t", help="The type of data to ingest (md, sql, etc).")
|
|
144
|
+
] = "md",
|
|
145
|
+
rebuild: Annotated[
|
|
146
|
+
bool,
|
|
147
|
+
typer.Option(
|
|
148
|
+
"--rebuild", "-r", help="Drop the existing vector store and recreate it from scratch."
|
|
149
|
+
),
|
|
150
|
+
] = False,
|
|
151
|
+
force: Annotated[
|
|
152
|
+
bool,
|
|
153
|
+
typer.Option("--force", "-f", help="Bypass confirmation prompt when rebuilding."),
|
|
154
|
+
] = False,
|
|
155
|
+
query: Annotated[
|
|
156
|
+
str | None,
|
|
157
|
+
typer.Option("--query", "-q", help="Custom SQL query for DuckDB extraction."),
|
|
158
|
+
] = None,
|
|
159
|
+
) -> None:
|
|
160
|
+
"""Ingests documents or SQL query logs into the Arrow-native vector store."""
|
|
161
|
+
if engine_name not in settings.engines:
|
|
162
|
+
typer.echo(
|
|
163
|
+
f"Error: Unknown engine type '{engine_name}'. Available: {list(settings.engines.keys())}"
|
|
164
|
+
)
|
|
165
|
+
raise typer.Exit(code=1)
|
|
166
|
+
|
|
167
|
+
if rebuild and not force:
|
|
168
|
+
typer.confirm(
|
|
169
|
+
f"Are you sure you want to completely rebuild the '{engine_name}' vector store? This will erase all existing data.",
|
|
170
|
+
abort=True,
|
|
171
|
+
)
|
|
172
|
+
|
|
173
|
+
url_override = path if path.startswith(("http://", "https://")) else None
|
|
174
|
+
deps = _build_dependencies(engine_name, query_override=query, url_override=url_override)
|
|
175
|
+
service = IngestionService(deps.chunker, deps.embedder, deps.store, deps.workflow)
|
|
176
|
+
service.ingest_directory(path, rebuild=rebuild)
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
@app.command()
|
|
180
|
+
def search(
|
|
181
|
+
query: Annotated[
|
|
182
|
+
str, typer.Argument(help="The text or SQL to search for within the indexed data.")
|
|
183
|
+
],
|
|
184
|
+
engine_name: Annotated[
|
|
185
|
+
str, typer.Option("--type", "-t", help="The type of data to search (md, sql, etc).")
|
|
186
|
+
] = "md",
|
|
187
|
+
filter_source: Annotated[
|
|
188
|
+
str | None,
|
|
189
|
+
typer.Option("--source", "-s", help="Filter results to a specific file or database."),
|
|
190
|
+
] = None,
|
|
191
|
+
limit: Annotated[
|
|
192
|
+
int, typer.Option("--limit", "-l", help="Maximum number of search results to return.")
|
|
193
|
+
] = 5,
|
|
194
|
+
# SQL specific filters
|
|
195
|
+
min_time: Annotated[
|
|
196
|
+
float | None, typer.Option("--min-time", help="(SQL Only) Minimum execution time in ms.")
|
|
197
|
+
] = None,
|
|
198
|
+
) -> None:
|
|
199
|
+
"""Searches the vector store using hybrid retrieval (Vector + Full-Text)."""
|
|
200
|
+
if engine_name not in settings.engines:
|
|
201
|
+
typer.echo(
|
|
202
|
+
f"Error: Unknown engine type '{engine_name}'. Available: {list(settings.engines.keys())}"
|
|
203
|
+
)
|
|
204
|
+
raise typer.Exit(code=1)
|
|
205
|
+
|
|
206
|
+
deps = _build_dependencies(engine_name)
|
|
207
|
+
service = SearchService(deps.embedder, deps.store)
|
|
208
|
+
|
|
209
|
+
extra_filters = {}
|
|
210
|
+
if min_time is not None and engine_name == "sql":
|
|
211
|
+
extra_filters["min_time"] = min_time
|
|
212
|
+
|
|
213
|
+
results = service.execute_query(
|
|
214
|
+
query, source_filter=filter_source, limit=limit, extra_filters=extra_filters
|
|
215
|
+
)
|
|
216
|
+
service.print_results(results)
|
|
217
|
+
|
|
218
|
+
|
|
219
|
+
@app.command()
|
|
220
|
+
def serve(
|
|
221
|
+
host: Annotated[
|
|
222
|
+
str, typer.Option("--host", "-h", help="Host to bind the API server to.")
|
|
223
|
+
] = "127.0.0.1",
|
|
224
|
+
port: Annotated[
|
|
225
|
+
int, typer.Option("--port", "-p", help="Port to bind the API server to.")
|
|
226
|
+
] = 8000,
|
|
227
|
+
reload: Annotated[
|
|
228
|
+
bool, typer.Option("--reload", help="Enable auto-reload for development.")
|
|
229
|
+
] = False,
|
|
230
|
+
) -> None:
|
|
231
|
+
"""Starts the asynchronous FastAPI search server."""
|
|
232
|
+
import uvicorn
|
|
233
|
+
|
|
234
|
+
logger.info("Starting dbs-vector API server at http://{}:{}", host, port)
|
|
235
|
+
uvicorn.run("dbs_vector.api.main:app", host=host, port=port, reload=reload)
|
|
236
|
+
|
|
237
|
+
|
|
238
|
+
@app.command()
|
|
239
|
+
def mcp(
|
|
240
|
+
config_file: Annotated[
|
|
241
|
+
str, typer.Option("--config-file", "-c", help="Path to config.yaml file.")
|
|
242
|
+
] = "config.yaml",
|
|
243
|
+
) -> None:
|
|
244
|
+
"""Starts the FastMCP standard input/output (stdio) server for integrations."""
|
|
245
|
+
import os
|
|
246
|
+
|
|
247
|
+
from dbs_vector.api.mcp_server import mcp as mcp_server
|
|
248
|
+
from dbs_vector.api.state import initialize_services
|
|
249
|
+
|
|
250
|
+
# Export to environment so the MCP subprocess inherits it
|
|
251
|
+
os.environ["DBS_CONFIG_FILE"] = config_file
|
|
252
|
+
|
|
253
|
+
logger.info("Initializing MLX Embedders and LanceDB connections")
|
|
254
|
+
try:
|
|
255
|
+
initialize_services()
|
|
256
|
+
except Exception as e:
|
|
257
|
+
logger.error("Failed to initialize search services: {}", e)
|
|
258
|
+
raise
|
|
259
|
+
|
|
260
|
+
mcp_server.run()
|
|
261
|
+
|
|
262
|
+
|
|
263
|
+
if __name__ == "__main__":
|
|
264
|
+
app()
|
dbs_vector/config.py
ADDED
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import os
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
|
|
4
|
+
import yaml
|
|
5
|
+
from loguru import logger
|
|
6
|
+
from pydantic import BaseModel
|
|
7
|
+
from pydantic_settings import BaseSettings, SettingsConfigDict
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class EngineConfig(BaseModel):
|
|
11
|
+
"""Configuration specific to a single AI engine/data source."""
|
|
12
|
+
|
|
13
|
+
description: str
|
|
14
|
+
model_name: str
|
|
15
|
+
vector_dimension: int
|
|
16
|
+
max_token_length: int
|
|
17
|
+
table_name: str
|
|
18
|
+
mapper_type: str
|
|
19
|
+
chunker_type: str
|
|
20
|
+
chunk_max_chars: int
|
|
21
|
+
|
|
22
|
+
# Task Prefixes for models like embeddinggemma
|
|
23
|
+
query_prefix: str = ""
|
|
24
|
+
passage_prefix: str = ""
|
|
25
|
+
workflow: str = "default"
|
|
26
|
+
duckdb_query: str | None = None
|
|
27
|
+
|
|
28
|
+
# API chunker fields
|
|
29
|
+
api_base_url: str = ""
|
|
30
|
+
api_key: str = ""
|
|
31
|
+
api_page_size: int = 200
|
|
32
|
+
api_since_days: int = 15
|
|
33
|
+
api_timeout_sec: int = 30
|
|
34
|
+
api_min_execution_ms: float = 0.0
|
|
35
|
+
api_database: str = ""
|
|
36
|
+
|
|
37
|
+
def chunker_kwargs(
|
|
38
|
+
self, query_override: str | None = None, url_override: str | None = None
|
|
39
|
+
) -> dict[str, object]:
|
|
40
|
+
"""Resolve chunker initialization kwargs from engine config."""
|
|
41
|
+
if self.chunker_type == "duckdb":
|
|
42
|
+
return {"query": query_override or self.duckdb_query}
|
|
43
|
+
if self.chunker_type == "api":
|
|
44
|
+
kwargs: dict[str, object] = {
|
|
45
|
+
"base_url": url_override or self.api_base_url,
|
|
46
|
+
"api_key": self.api_key,
|
|
47
|
+
"page_size": self.api_page_size,
|
|
48
|
+
"since_days": self.api_since_days,
|
|
49
|
+
"timeout_sec": self.api_timeout_sec,
|
|
50
|
+
"min_execution_ms": self.api_min_execution_ms,
|
|
51
|
+
}
|
|
52
|
+
if self.api_database:
|
|
53
|
+
kwargs["database"] = self.api_database
|
|
54
|
+
if query_override:
|
|
55
|
+
kwargs["custom_query"] = query_override
|
|
56
|
+
return kwargs
|
|
57
|
+
if self.chunk_max_chars > 0:
|
|
58
|
+
return {"max_chars": self.chunk_max_chars}
|
|
59
|
+
return {}
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
class Settings(BaseSettings):
|
|
63
|
+
"""Global configuration for the dbs-vector application."""
|
|
64
|
+
|
|
65
|
+
# General System
|
|
66
|
+
db_path: str = "./lancedb_dbs_vector"
|
|
67
|
+
batch_size: int = 64
|
|
68
|
+
nprobes: int = 20
|
|
69
|
+
log_level: str = "INFO"
|
|
70
|
+
log_serialize: bool = False
|
|
71
|
+
|
|
72
|
+
# Engines dictionary
|
|
73
|
+
engines: dict[str, EngineConfig] = {}
|
|
74
|
+
|
|
75
|
+
model_config = SettingsConfigDict(env_prefix="DBS_", env_file=".env")
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def load_settings(config_file: str | None = None) -> Settings:
|
|
79
|
+
"""Loads base settings and overrides them from config.yaml."""
|
|
80
|
+
base_settings = Settings()
|
|
81
|
+
|
|
82
|
+
if config_file is None:
|
|
83
|
+
config_file = os.getenv("DBS_CONFIG_FILE", "config.yaml")
|
|
84
|
+
|
|
85
|
+
yaml_path = Path(config_file)
|
|
86
|
+
if yaml_path.exists():
|
|
87
|
+
with open(yaml_path, encoding="utf-8") as f:
|
|
88
|
+
data = yaml.safe_load(f)
|
|
89
|
+
|
|
90
|
+
if not data:
|
|
91
|
+
return base_settings
|
|
92
|
+
|
|
93
|
+
# Override System configuration
|
|
94
|
+
if "system" in data and isinstance(data["system"], dict):
|
|
95
|
+
for key, value in data["system"].items():
|
|
96
|
+
if hasattr(base_settings, key):
|
|
97
|
+
setattr(base_settings, key, value)
|
|
98
|
+
|
|
99
|
+
# Override Engine configuration
|
|
100
|
+
if "engines" in data and isinstance(data["engines"], dict):
|
|
101
|
+
engines = {k: EngineConfig(**v) for k, v in data["engines"].items()}
|
|
102
|
+
base_settings.engines = engines
|
|
103
|
+
else:
|
|
104
|
+
logger.warning("Configuration file '{}' not found, using defaults", yaml_path)
|
|
105
|
+
|
|
106
|
+
return base_settings
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
# Global singleton instance
|
|
110
|
+
settings = load_settings()
|
|
File without changes
|