embgrep 0.1.0__tar.gz
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.
- embgrep-0.1.0/.gitignore +12 -0
- embgrep-0.1.0/LICENSE +21 -0
- embgrep-0.1.0/PKG-INFO +194 -0
- embgrep-0.1.0/README.md +160 -0
- embgrep-0.1.0/embgrep/__init__.py +79 -0
- embgrep-0.1.0/embgrep/__main__.py +141 -0
- embgrep-0.1.0/embgrep/chunker.py +205 -0
- embgrep-0.1.0/embgrep/db.py +159 -0
- embgrep-0.1.0/embgrep/embedder.py +60 -0
- embgrep-0.1.0/embgrep/indexer.py +237 -0
- embgrep-0.1.0/embgrep/mcp_server.py +119 -0
- embgrep-0.1.0/pyproject.toml +48 -0
- embgrep-0.1.0/tests/__init__.py +0 -0
- embgrep-0.1.0/tests/test_chunker.py +236 -0
- embgrep-0.1.0/tests/test_db.py +172 -0
- embgrep-0.1.0/tests/test_embedder.py +68 -0
- embgrep-0.1.0/tests/test_indexer.py +199 -0
- embgrep-0.1.0/tests/test_mcp.py +113 -0
embgrep-0.1.0/.gitignore
ADDED
embgrep-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 QuartzUnit
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
embgrep-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: embgrep
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Local semantic search — embedding-powered grep for files, zero external services.
|
|
5
|
+
Project-URL: Homepage, https://github.com/QuartzUnit/embgrep
|
|
6
|
+
Project-URL: Repository, https://github.com/QuartzUnit/embgrep
|
|
7
|
+
Author: QuartzUnit
|
|
8
|
+
License-Expression: MIT
|
|
9
|
+
License-File: LICENSE
|
|
10
|
+
Keywords: embeddings,grep,local,mcp,rag,semantic-search
|
|
11
|
+
Classifier: Development Status :: 3 - Alpha
|
|
12
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
13
|
+
Classifier: Programming Language :: Python :: 3
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
17
|
+
Classifier: Typing :: Typed
|
|
18
|
+
Requires-Python: >=3.11
|
|
19
|
+
Requires-Dist: fastembed>=0.4
|
|
20
|
+
Requires-Dist: numpy>=1.24
|
|
21
|
+
Provides-Extra: all
|
|
22
|
+
Requires-Dist: click>=8.0; extra == 'all'
|
|
23
|
+
Requires-Dist: fastmcp>=2.0; extra == 'all'
|
|
24
|
+
Requires-Dist: rich>=13.0; extra == 'all'
|
|
25
|
+
Provides-Extra: cli
|
|
26
|
+
Requires-Dist: click>=8.0; extra == 'cli'
|
|
27
|
+
Requires-Dist: rich>=13.0; extra == 'cli'
|
|
28
|
+
Provides-Extra: dev
|
|
29
|
+
Requires-Dist: pytest>=8.0; extra == 'dev'
|
|
30
|
+
Requires-Dist: ruff>=0.8; extra == 'dev'
|
|
31
|
+
Provides-Extra: mcp
|
|
32
|
+
Requires-Dist: fastmcp>=2.0; extra == 'mcp'
|
|
33
|
+
Description-Content-Type: text/markdown
|
|
34
|
+
|
|
35
|
+
# embgrep
|
|
36
|
+
|
|
37
|
+
**Local semantic search — embedding-powered grep for files, zero external services.**
|
|
38
|
+
|
|
39
|
+
[](https://pypi.org/project/embgrep/)
|
|
40
|
+
[](https://pypi.org/project/embgrep/)
|
|
41
|
+
[](https://opensource.org/licenses/MIT)
|
|
42
|
+
|
|
43
|
+
Search your codebase and documentation by *meaning*, not just keywords. embgrep indexes files into local embeddings and lets you run semantic queries — no API keys, no cloud services, no vector database servers.
|
|
44
|
+
|
|
45
|
+
## Features
|
|
46
|
+
|
|
47
|
+
- **Local embeddings** — Uses [fastembed](https://github.com/qdrant/fastembed) (ONNX Runtime), no API keys needed
|
|
48
|
+
- **SQLite storage** — Single-file index, no external vector DB
|
|
49
|
+
- **Incremental indexing** — Only re-indexes changed files (SHA-256 hash comparison)
|
|
50
|
+
- **Smart chunking** — Function-level splitting for code, heading-level for docs
|
|
51
|
+
- **MCP native** — 4-tool FastMCP server for LLM agent integration
|
|
52
|
+
- **15+ file types** — `.py`, `.js`, `.ts`, `.java`, `.go`, `.rs`, `.md`, `.txt`, `.yaml`, `.json`, `.toml`, and more
|
|
53
|
+
|
|
54
|
+
## Install
|
|
55
|
+
|
|
56
|
+
```bash
|
|
57
|
+
pip install embgrep # core (fastembed + numpy)
|
|
58
|
+
pip install embgrep[cli] # + click/rich CLI
|
|
59
|
+
pip install embgrep[mcp] # + FastMCP server
|
|
60
|
+
pip install embgrep[all] # everything
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
## Quick Start
|
|
64
|
+
|
|
65
|
+
### Python API
|
|
66
|
+
|
|
67
|
+
```python
|
|
68
|
+
from embgrep import EmbGrep
|
|
69
|
+
|
|
70
|
+
eg = EmbGrep()
|
|
71
|
+
|
|
72
|
+
# Index a directory
|
|
73
|
+
eg.index("./my-project", patterns=["*.py", "*.md"])
|
|
74
|
+
|
|
75
|
+
# Semantic search
|
|
76
|
+
results = eg.search("database connection pooling", top_k=5)
|
|
77
|
+
for r in results:
|
|
78
|
+
print(f"{r.file_path}:{r.line_start}-{r.line_end} (score: {r.score:.4f})")
|
|
79
|
+
print(f" {r.chunk_text[:80]}...")
|
|
80
|
+
|
|
81
|
+
# Incremental update (only changed files)
|
|
82
|
+
eg.update()
|
|
83
|
+
|
|
84
|
+
# Index statistics
|
|
85
|
+
status = eg.status()
|
|
86
|
+
print(f"{status.total_files} files, {status.total_chunks} chunks, {status.index_size_mb} MB")
|
|
87
|
+
|
|
88
|
+
eg.close()
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
### CLI
|
|
92
|
+
|
|
93
|
+
```bash
|
|
94
|
+
# Index a project
|
|
95
|
+
embgrep index ./my-project --patterns "*.py,*.md"
|
|
96
|
+
|
|
97
|
+
# Search
|
|
98
|
+
embgrep search "error handling patterns"
|
|
99
|
+
|
|
100
|
+
# Filter by file type
|
|
101
|
+
embgrep search "async database query" --path-filter "%.py"
|
|
102
|
+
|
|
103
|
+
# Check status
|
|
104
|
+
embgrep status
|
|
105
|
+
|
|
106
|
+
# Update changed files
|
|
107
|
+
embgrep update
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
### Convenience functions
|
|
111
|
+
|
|
112
|
+
```python
|
|
113
|
+
import embgrep
|
|
114
|
+
|
|
115
|
+
embgrep.index("./src")
|
|
116
|
+
results = embgrep.search("authentication middleware")
|
|
117
|
+
status = embgrep.status()
|
|
118
|
+
embgrep.update()
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
## MCP Server
|
|
122
|
+
|
|
123
|
+
Add to your Claude Desktop / MCP client configuration:
|
|
124
|
+
|
|
125
|
+
```json
|
|
126
|
+
{
|
|
127
|
+
"mcpServers": {
|
|
128
|
+
"embgrep": {
|
|
129
|
+
"command": "embgrep-mcp"
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
Or with uvx:
|
|
136
|
+
|
|
137
|
+
```json
|
|
138
|
+
{
|
|
139
|
+
"mcpServers": {
|
|
140
|
+
"embgrep": {
|
|
141
|
+
"command": "uvx",
|
|
142
|
+
"args": ["--from", "embgrep[mcp]", "embgrep-mcp"]
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
### MCP Tools
|
|
149
|
+
|
|
150
|
+
| Tool | Description |
|
|
151
|
+
|------|-------------|
|
|
152
|
+
| `index_directory` | Index files in a directory for semantic search |
|
|
153
|
+
| `semantic_search` | Search indexed files using natural language |
|
|
154
|
+
| `index_status` | Get current index statistics |
|
|
155
|
+
| `update_index` | Incremental update — re-index changed files only |
|
|
156
|
+
|
|
157
|
+
## How It Works
|
|
158
|
+
|
|
159
|
+
1. **Chunking** — Files are split into semantically meaningful chunks:
|
|
160
|
+
- Code files (`.py`, `.js`, `.ts`, etc.): split by function/class boundaries
|
|
161
|
+
- Documents (`.md`, `.txt`): split by headings or paragraph breaks
|
|
162
|
+
- Config files: fixed-size chunking
|
|
163
|
+
|
|
164
|
+
2. **Embedding** — Each chunk is converted to a 384-dimensional vector using [BGE-small-en-v1.5](https://huggingface.co/BAAI/bge-small-en-v1.5) via ONNX Runtime (no PyTorch needed)
|
|
165
|
+
|
|
166
|
+
3. **Storage** — Embeddings are stored as BLOBs in a local SQLite database
|
|
167
|
+
|
|
168
|
+
4. **Search** — Query text is embedded and compared against all chunks using cosine similarity
|
|
169
|
+
|
|
170
|
+
## Configuration
|
|
171
|
+
|
|
172
|
+
| Parameter | Default | Description |
|
|
173
|
+
|-----------|---------|-------------|
|
|
174
|
+
| `db_path` | `~/.local/share/embgrep/embgrep.db` | SQLite database location |
|
|
175
|
+
| `model` | `BAAI/bge-small-en-v1.5` | fastembed model name |
|
|
176
|
+
| `max_chunk_size` | 1000 chars | Maximum chunk size for fixed-size splitting |
|
|
177
|
+
| `top_k` | 5 | Number of search results |
|
|
178
|
+
|
|
179
|
+
## QuartzUnit Ecosystem
|
|
180
|
+
|
|
181
|
+
| Package | Description |
|
|
182
|
+
|---------|-------------|
|
|
183
|
+
| [markgrab](https://github.com/QuartzUnit/markgrab) | HTML/YouTube/PDF/DOCX to LLM-ready markdown |
|
|
184
|
+
| [snapgrab](https://github.com/QuartzUnit/snapgrab) | URL to screenshot + metadata |
|
|
185
|
+
| [docpick](https://github.com/QuartzUnit/docpick) | OCR + LLM document structure extraction |
|
|
186
|
+
| [browsegrab](https://github.com/QuartzUnit/browsegrab) | Local LLM browser agent |
|
|
187
|
+
| [feedkit](https://github.com/QuartzUnit/feedkit) | RSS feed collection + MCP |
|
|
188
|
+
| **embgrep** | **Local semantic search for files** |
|
|
189
|
+
|
|
190
|
+
## License
|
|
191
|
+
|
|
192
|
+
MIT
|
|
193
|
+
|
|
194
|
+
<!-- mcp-name: io.github.ArkNill/embgrep -->
|
embgrep-0.1.0/README.md
ADDED
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
# embgrep
|
|
2
|
+
|
|
3
|
+
**Local semantic search — embedding-powered grep for files, zero external services.**
|
|
4
|
+
|
|
5
|
+
[](https://pypi.org/project/embgrep/)
|
|
6
|
+
[](https://pypi.org/project/embgrep/)
|
|
7
|
+
[](https://opensource.org/licenses/MIT)
|
|
8
|
+
|
|
9
|
+
Search your codebase and documentation by *meaning*, not just keywords. embgrep indexes files into local embeddings and lets you run semantic queries — no API keys, no cloud services, no vector database servers.
|
|
10
|
+
|
|
11
|
+
## Features
|
|
12
|
+
|
|
13
|
+
- **Local embeddings** — Uses [fastembed](https://github.com/qdrant/fastembed) (ONNX Runtime), no API keys needed
|
|
14
|
+
- **SQLite storage** — Single-file index, no external vector DB
|
|
15
|
+
- **Incremental indexing** — Only re-indexes changed files (SHA-256 hash comparison)
|
|
16
|
+
- **Smart chunking** — Function-level splitting for code, heading-level for docs
|
|
17
|
+
- **MCP native** — 4-tool FastMCP server for LLM agent integration
|
|
18
|
+
- **15+ file types** — `.py`, `.js`, `.ts`, `.java`, `.go`, `.rs`, `.md`, `.txt`, `.yaml`, `.json`, `.toml`, and more
|
|
19
|
+
|
|
20
|
+
## Install
|
|
21
|
+
|
|
22
|
+
```bash
|
|
23
|
+
pip install embgrep # core (fastembed + numpy)
|
|
24
|
+
pip install embgrep[cli] # + click/rich CLI
|
|
25
|
+
pip install embgrep[mcp] # + FastMCP server
|
|
26
|
+
pip install embgrep[all] # everything
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
## Quick Start
|
|
30
|
+
|
|
31
|
+
### Python API
|
|
32
|
+
|
|
33
|
+
```python
|
|
34
|
+
from embgrep import EmbGrep
|
|
35
|
+
|
|
36
|
+
eg = EmbGrep()
|
|
37
|
+
|
|
38
|
+
# Index a directory
|
|
39
|
+
eg.index("./my-project", patterns=["*.py", "*.md"])
|
|
40
|
+
|
|
41
|
+
# Semantic search
|
|
42
|
+
results = eg.search("database connection pooling", top_k=5)
|
|
43
|
+
for r in results:
|
|
44
|
+
print(f"{r.file_path}:{r.line_start}-{r.line_end} (score: {r.score:.4f})")
|
|
45
|
+
print(f" {r.chunk_text[:80]}...")
|
|
46
|
+
|
|
47
|
+
# Incremental update (only changed files)
|
|
48
|
+
eg.update()
|
|
49
|
+
|
|
50
|
+
# Index statistics
|
|
51
|
+
status = eg.status()
|
|
52
|
+
print(f"{status.total_files} files, {status.total_chunks} chunks, {status.index_size_mb} MB")
|
|
53
|
+
|
|
54
|
+
eg.close()
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
### CLI
|
|
58
|
+
|
|
59
|
+
```bash
|
|
60
|
+
# Index a project
|
|
61
|
+
embgrep index ./my-project --patterns "*.py,*.md"
|
|
62
|
+
|
|
63
|
+
# Search
|
|
64
|
+
embgrep search "error handling patterns"
|
|
65
|
+
|
|
66
|
+
# Filter by file type
|
|
67
|
+
embgrep search "async database query" --path-filter "%.py"
|
|
68
|
+
|
|
69
|
+
# Check status
|
|
70
|
+
embgrep status
|
|
71
|
+
|
|
72
|
+
# Update changed files
|
|
73
|
+
embgrep update
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
### Convenience functions
|
|
77
|
+
|
|
78
|
+
```python
|
|
79
|
+
import embgrep
|
|
80
|
+
|
|
81
|
+
embgrep.index("./src")
|
|
82
|
+
results = embgrep.search("authentication middleware")
|
|
83
|
+
status = embgrep.status()
|
|
84
|
+
embgrep.update()
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
## MCP Server
|
|
88
|
+
|
|
89
|
+
Add to your Claude Desktop / MCP client configuration:
|
|
90
|
+
|
|
91
|
+
```json
|
|
92
|
+
{
|
|
93
|
+
"mcpServers": {
|
|
94
|
+
"embgrep": {
|
|
95
|
+
"command": "embgrep-mcp"
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
Or with uvx:
|
|
102
|
+
|
|
103
|
+
```json
|
|
104
|
+
{
|
|
105
|
+
"mcpServers": {
|
|
106
|
+
"embgrep": {
|
|
107
|
+
"command": "uvx",
|
|
108
|
+
"args": ["--from", "embgrep[mcp]", "embgrep-mcp"]
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
### MCP Tools
|
|
115
|
+
|
|
116
|
+
| Tool | Description |
|
|
117
|
+
|------|-------------|
|
|
118
|
+
| `index_directory` | Index files in a directory for semantic search |
|
|
119
|
+
| `semantic_search` | Search indexed files using natural language |
|
|
120
|
+
| `index_status` | Get current index statistics |
|
|
121
|
+
| `update_index` | Incremental update — re-index changed files only |
|
|
122
|
+
|
|
123
|
+
## How It Works
|
|
124
|
+
|
|
125
|
+
1. **Chunking** — Files are split into semantically meaningful chunks:
|
|
126
|
+
- Code files (`.py`, `.js`, `.ts`, etc.): split by function/class boundaries
|
|
127
|
+
- Documents (`.md`, `.txt`): split by headings or paragraph breaks
|
|
128
|
+
- Config files: fixed-size chunking
|
|
129
|
+
|
|
130
|
+
2. **Embedding** — Each chunk is converted to a 384-dimensional vector using [BGE-small-en-v1.5](https://huggingface.co/BAAI/bge-small-en-v1.5) via ONNX Runtime (no PyTorch needed)
|
|
131
|
+
|
|
132
|
+
3. **Storage** — Embeddings are stored as BLOBs in a local SQLite database
|
|
133
|
+
|
|
134
|
+
4. **Search** — Query text is embedded and compared against all chunks using cosine similarity
|
|
135
|
+
|
|
136
|
+
## Configuration
|
|
137
|
+
|
|
138
|
+
| Parameter | Default | Description |
|
|
139
|
+
|-----------|---------|-------------|
|
|
140
|
+
| `db_path` | `~/.local/share/embgrep/embgrep.db` | SQLite database location |
|
|
141
|
+
| `model` | `BAAI/bge-small-en-v1.5` | fastembed model name |
|
|
142
|
+
| `max_chunk_size` | 1000 chars | Maximum chunk size for fixed-size splitting |
|
|
143
|
+
| `top_k` | 5 | Number of search results |
|
|
144
|
+
|
|
145
|
+
## QuartzUnit Ecosystem
|
|
146
|
+
|
|
147
|
+
| Package | Description |
|
|
148
|
+
|---------|-------------|
|
|
149
|
+
| [markgrab](https://github.com/QuartzUnit/markgrab) | HTML/YouTube/PDF/DOCX to LLM-ready markdown |
|
|
150
|
+
| [snapgrab](https://github.com/QuartzUnit/snapgrab) | URL to screenshot + metadata |
|
|
151
|
+
| [docpick](https://github.com/QuartzUnit/docpick) | OCR + LLM document structure extraction |
|
|
152
|
+
| [browsegrab](https://github.com/QuartzUnit/browsegrab) | Local LLM browser agent |
|
|
153
|
+
| [feedkit](https://github.com/QuartzUnit/feedkit) | RSS feed collection + MCP |
|
|
154
|
+
| **embgrep** | **Local semantic search for files** |
|
|
155
|
+
|
|
156
|
+
## License
|
|
157
|
+
|
|
158
|
+
MIT
|
|
159
|
+
|
|
160
|
+
<!-- mcp-name: io.github.ArkNill/embgrep -->
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
"""embgrep — Local semantic search, embedding-powered grep for files."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from embgrep.indexer import EmbGrep, IndexStatus, SearchResult
|
|
6
|
+
|
|
7
|
+
__all__ = ["EmbGrep", "IndexStatus", "SearchResult", "index", "search", "status", "update"]
|
|
8
|
+
__version__ = "0.1.0"
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def index(directory: str, patterns: list[str] | None = None, db_path: str | None = None) -> dict:
|
|
12
|
+
"""Index files in a directory.
|
|
13
|
+
|
|
14
|
+
Args:
|
|
15
|
+
directory: Path to the directory to index.
|
|
16
|
+
patterns: Optional list of glob patterns to filter files.
|
|
17
|
+
db_path: Optional path to the SQLite database.
|
|
18
|
+
|
|
19
|
+
Returns:
|
|
20
|
+
Dictionary with files_indexed, chunks_created, index_size_mb.
|
|
21
|
+
"""
|
|
22
|
+
eg = EmbGrep(db_path=db_path) if db_path else EmbGrep()
|
|
23
|
+
try:
|
|
24
|
+
return eg.index(directory, patterns=patterns)
|
|
25
|
+
finally:
|
|
26
|
+
eg.close()
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def search(
|
|
30
|
+
query: str, top_k: int = 5, path_filter: str | None = None, db_path: str | None = None
|
|
31
|
+
) -> list[SearchResult]:
|
|
32
|
+
"""Semantic search across indexed files.
|
|
33
|
+
|
|
34
|
+
Args:
|
|
35
|
+
query: Natural language search query.
|
|
36
|
+
top_k: Number of results to return.
|
|
37
|
+
path_filter: Optional LIKE pattern to filter by file path.
|
|
38
|
+
db_path: Optional path to the SQLite database.
|
|
39
|
+
|
|
40
|
+
Returns:
|
|
41
|
+
List of SearchResult sorted by similarity score.
|
|
42
|
+
"""
|
|
43
|
+
eg = EmbGrep(db_path=db_path) if db_path else EmbGrep()
|
|
44
|
+
try:
|
|
45
|
+
return eg.search(query, top_k=top_k, path_filter=path_filter)
|
|
46
|
+
finally:
|
|
47
|
+
eg.close()
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def status(db_path: str | None = None) -> IndexStatus:
|
|
51
|
+
"""Get index statistics.
|
|
52
|
+
|
|
53
|
+
Args:
|
|
54
|
+
db_path: Optional path to the SQLite database.
|
|
55
|
+
|
|
56
|
+
Returns:
|
|
57
|
+
IndexStatus with total_files, total_chunks, last_updated, index_size_mb.
|
|
58
|
+
"""
|
|
59
|
+
eg = EmbGrep(db_path=db_path) if db_path else EmbGrep()
|
|
60
|
+
try:
|
|
61
|
+
return eg.status()
|
|
62
|
+
finally:
|
|
63
|
+
eg.close()
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def update(db_path: str | None = None) -> dict:
|
|
67
|
+
"""Incremental update — re-index changed files only.
|
|
68
|
+
|
|
69
|
+
Args:
|
|
70
|
+
db_path: Optional path to the SQLite database.
|
|
71
|
+
|
|
72
|
+
Returns:
|
|
73
|
+
Dictionary with updated_files, new_chunks, removed_files.
|
|
74
|
+
"""
|
|
75
|
+
eg = EmbGrep(db_path=db_path) if db_path else EmbGrep()
|
|
76
|
+
try:
|
|
77
|
+
return eg.update()
|
|
78
|
+
finally:
|
|
79
|
+
eg.close()
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
"""CLI entry point for embgrep — embedding-powered grep for files."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import sys
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def main() -> None:
|
|
9
|
+
"""Main CLI entry point."""
|
|
10
|
+
try:
|
|
11
|
+
import click
|
|
12
|
+
from rich.console import Console
|
|
13
|
+
from rich.table import Table
|
|
14
|
+
except ImportError:
|
|
15
|
+
print("CLI requires extra dependencies: pip install embgrep[cli]")
|
|
16
|
+
sys.exit(1)
|
|
17
|
+
|
|
18
|
+
console = Console()
|
|
19
|
+
|
|
20
|
+
@click.group()
|
|
21
|
+
@click.version_option(package_name="embgrep")
|
|
22
|
+
def cli() -> None:
|
|
23
|
+
"""embgrep — Local semantic search, embedding-powered grep for files."""
|
|
24
|
+
|
|
25
|
+
@cli.command()
|
|
26
|
+
@click.argument("path", type=click.Path(exists=True))
|
|
27
|
+
@click.option("--patterns", "-p", default=None, help="Comma-separated glob patterns (e.g., '*.md,*.py').")
|
|
28
|
+
@click.option("--db-path", default=None, help="Path to SQLite database.")
|
|
29
|
+
@click.option("--model", default="BAAI/bge-small-en-v1.5", help="Embedding model name.")
|
|
30
|
+
def index(path: str, patterns: str | None, db_path: str | None, model: str) -> None:
|
|
31
|
+
"""Index files in PATH for semantic search."""
|
|
32
|
+
from embgrep.indexer import EmbGrep
|
|
33
|
+
|
|
34
|
+
pattern_list = [p.strip() for p in patterns.split(",")] if patterns else None
|
|
35
|
+
|
|
36
|
+
kwargs: dict = {"model": model}
|
|
37
|
+
if db_path:
|
|
38
|
+
kwargs["db_path"] = db_path
|
|
39
|
+
|
|
40
|
+
eg = EmbGrep(**kwargs)
|
|
41
|
+
try:
|
|
42
|
+
with console.status("[bold green]Indexing files..."):
|
|
43
|
+
result = eg.index(path, patterns=pattern_list)
|
|
44
|
+
console.print(f"[green]Indexed {result['files_indexed']} files, {result['chunks_created']} chunks[/green]")
|
|
45
|
+
console.print(f"Index size: {result['index_size_mb']} MB")
|
|
46
|
+
finally:
|
|
47
|
+
eg.close()
|
|
48
|
+
|
|
49
|
+
@cli.command()
|
|
50
|
+
@click.argument("query")
|
|
51
|
+
@click.option("--top-k", "-k", default=5, help="Number of results to return.")
|
|
52
|
+
@click.option("--path-filter", "-f", default=None, help="SQL LIKE pattern for file path filter.")
|
|
53
|
+
@click.option("--db-path", default=None, help="Path to SQLite database.")
|
|
54
|
+
@click.option("--model", default="BAAI/bge-small-en-v1.5", help="Embedding model name.")
|
|
55
|
+
def search(query: str, top_k: int, path_filter: str | None, db_path: str | None, model: str) -> None:
|
|
56
|
+
"""Semantic search across indexed files."""
|
|
57
|
+
from embgrep.indexer import EmbGrep
|
|
58
|
+
|
|
59
|
+
kwargs: dict = {"model": model}
|
|
60
|
+
if db_path:
|
|
61
|
+
kwargs["db_path"] = db_path
|
|
62
|
+
|
|
63
|
+
eg = EmbGrep(**kwargs)
|
|
64
|
+
try:
|
|
65
|
+
with console.status("[bold green]Searching..."):
|
|
66
|
+
results = eg.search(query, top_k=top_k, path_filter=path_filter)
|
|
67
|
+
|
|
68
|
+
if not results:
|
|
69
|
+
console.print("[yellow]No results found.[/yellow]")
|
|
70
|
+
return
|
|
71
|
+
|
|
72
|
+
table = Table(title=f"Search: {query!r}", show_lines=True)
|
|
73
|
+
table.add_column("#", style="dim", width=3)
|
|
74
|
+
table.add_column("Score", style="cyan", width=7)
|
|
75
|
+
table.add_column("File", style="green")
|
|
76
|
+
table.add_column("Lines", style="yellow", width=10)
|
|
77
|
+
table.add_column("Preview", max_width=60)
|
|
78
|
+
|
|
79
|
+
for i, r in enumerate(results, 1):
|
|
80
|
+
preview = r.chunk_text[:120].replace("\n", " ").strip()
|
|
81
|
+
if len(r.chunk_text) > 120:
|
|
82
|
+
preview += "..."
|
|
83
|
+
table.add_row(
|
|
84
|
+
str(i),
|
|
85
|
+
f"{r.score:.4f}",
|
|
86
|
+
r.file_path,
|
|
87
|
+
f"{r.line_start}-{r.line_end}",
|
|
88
|
+
preview,
|
|
89
|
+
)
|
|
90
|
+
|
|
91
|
+
console.print(table)
|
|
92
|
+
finally:
|
|
93
|
+
eg.close()
|
|
94
|
+
|
|
95
|
+
@cli.command()
|
|
96
|
+
@click.option("--db-path", default=None, help="Path to SQLite database.")
|
|
97
|
+
def status(db_path: str | None) -> None:
|
|
98
|
+
"""Show index statistics."""
|
|
99
|
+
from embgrep.indexer import EmbGrep
|
|
100
|
+
|
|
101
|
+
kwargs: dict = {}
|
|
102
|
+
if db_path:
|
|
103
|
+
kwargs["db_path"] = db_path
|
|
104
|
+
|
|
105
|
+
eg = EmbGrep(**kwargs)
|
|
106
|
+
try:
|
|
107
|
+
st = eg.status()
|
|
108
|
+
console.print("[bold]embgrep Index Status[/bold]")
|
|
109
|
+
console.print(f" Files: {st.total_files}")
|
|
110
|
+
console.print(f" Chunks: {st.total_chunks}")
|
|
111
|
+
console.print(f" Last updated: {st.last_updated}")
|
|
112
|
+
console.print(f" Index size: {st.index_size_mb} MB")
|
|
113
|
+
finally:
|
|
114
|
+
eg.close()
|
|
115
|
+
|
|
116
|
+
@cli.command()
|
|
117
|
+
@click.option("--db-path", default=None, help="Path to SQLite database.")
|
|
118
|
+
@click.option("--model", default="BAAI/bge-small-en-v1.5", help="Embedding model name.")
|
|
119
|
+
def update(db_path: str | None, model: str) -> None:
|
|
120
|
+
"""Incremental update — re-index changed files only."""
|
|
121
|
+
from embgrep.indexer import EmbGrep
|
|
122
|
+
|
|
123
|
+
kwargs: dict = {"model": model}
|
|
124
|
+
if db_path:
|
|
125
|
+
kwargs["db_path"] = db_path
|
|
126
|
+
|
|
127
|
+
eg = EmbGrep(**kwargs)
|
|
128
|
+
try:
|
|
129
|
+
with console.status("[bold green]Updating index..."):
|
|
130
|
+
result = eg.update()
|
|
131
|
+
console.print(f"[green]Updated {result['updated_files']} files, {result['new_chunks']} new chunks[/green]")
|
|
132
|
+
if result["removed_files"]:
|
|
133
|
+
console.print(f"[yellow]Removed {result['removed_files']} deleted files[/yellow]")
|
|
134
|
+
finally:
|
|
135
|
+
eg.close()
|
|
136
|
+
|
|
137
|
+
cli()
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
if __name__ == "__main__":
|
|
141
|
+
main()
|