sift-rag 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.
@@ -0,0 +1,26 @@
1
+ # Changelog
2
+
3
+ All notable changes are listed here. The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
4
+ and versions follow [Semantic Versioning](https://semver.org/).
5
+
6
+ ## [Unreleased]
7
+
8
+ ## [0.1.0] - 2026-10-18
9
+
10
+ First release.
11
+
12
+ ### Added
13
+
14
+ - `sift add`, `rm`, `index`, `embed`, `search`, `vsearch`, `query`, `get`, `status` and `serve`; also available as
15
+ `sift-rag`, so `uvx sift-rag serve` works.
16
+ - Incremental indexing of `.md`, `.markdown` and `.txt` files in parallel worker processes, with filterable YAML
17
+ front matter.
18
+ - Embedding on NVIDIA GPUs (CUDA), Apple GPUs (MPS) or the CPU. sift caps its GPU memory below what other programs
19
+ leave free, calibrates the batch size by measurement, and reports progress.
20
+ - Keyword search (SQLite FTS5, BM25), exact GPU vector search, and hybrid search with cross-encoder reranking.
21
+ - An MCP server (streamable HTTP and stdio) whose instructions tell agents what is indexed, and whose errors tell
22
+ them what went wrong.
23
+ - A start-up hint when an NVIDIA GPU can't be used, with the command that fixes it.
24
+
25
+ [Unreleased]: https://github.com/tobiaswuerth/sift/compare/v0.1.0...HEAD
26
+ [0.1.0]: https://github.com/tobiaswuerth/sift/releases/tag/v0.1.0
sift_rag-0.1.0/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Tobias Würth
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.
@@ -0,0 +1,3 @@
1
+ graft tests
2
+ include CHANGELOG.md
3
+ global-exclude __pycache__ *.py[cod]
@@ -0,0 +1,210 @@
1
+ Metadata-Version: 2.4
2
+ Name: sift-rag
3
+ Version: 0.1.0
4
+ Summary: Fast local search over text and Markdown files, built for AI agents.
5
+ Author: Tobias Würth
6
+ License-Expression: MIT
7
+ Project-URL: Homepage, https://github.com/tobiaswuerth/sift
8
+ Project-URL: Documentation, https://github.com/tobiaswuerth/sift#readme
9
+ Project-URL: Issues, https://github.com/tobiaswuerth/sift/issues
10
+ Project-URL: Changelog, https://github.com/tobiaswuerth/sift/blob/main/CHANGELOG.md
11
+ Keywords: search,rag,embeddings,mcp,markdown,bm25,semantic-search
12
+ Classifier: Development Status :: 3 - Alpha
13
+ Classifier: Environment :: Console
14
+ Classifier: Environment :: GPU :: NVIDIA CUDA
15
+ Classifier: Intended Audience :: Developers
16
+ Classifier: Intended Audience :: Science/Research
17
+ Classifier: Operating System :: MacOS
18
+ Classifier: Operating System :: Microsoft :: Windows
19
+ Classifier: Operating System :: POSIX :: Linux
20
+ Classifier: Programming Language :: Python :: 3
21
+ Classifier: Programming Language :: Python :: 3.11
22
+ Classifier: Programming Language :: Python :: 3.12
23
+ Classifier: Programming Language :: Python :: 3.13
24
+ Classifier: Programming Language :: Python :: 3.14
25
+ Classifier: Programming Language :: Python :: 3.15
26
+ Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
27
+ Classifier: Topic :: Text Processing :: Indexing
28
+ Requires-Python: >=3.11
29
+ Description-Content-Type: text/markdown
30
+ License-File: LICENSE
31
+ Requires-Dist: numpy>=1.26
32
+ Requires-Dist: torch>=2.4
33
+ Requires-Dist: sentence-transformers>=5.0
34
+ Requires-Dist: mcp>=2.0
35
+ Requires-Dist: nvidia-ml-py>=12
36
+ Provides-Extra: dev
37
+ Requires-Dist: pytest>=8; extra == "dev"
38
+ Requires-Dist: pytest-cov>=5; extra == "dev"
39
+ Requires-Dist: ruff>=0.6; extra == "dev"
40
+ Requires-Dist: pre-commit>=4; extra == "dev"
41
+ Requires-Dist: build>=1.2; extra == "dev"
42
+ Requires-Dist: twine>=6; extra == "dev"
43
+ Dynamic: license-file
44
+
45
+ # sift
46
+
47
+ [![PyPI](https://img.shields.io/pypi/v/sift-rag)](https://pypi.org/project/sift-rag/)
48
+ [![Python](https://img.shields.io/pypi/pyversions/sift-rag)](https://pypi.org/project/sift-rag/)
49
+ [![CI](https://github.com/tobiaswuerth/sift/actions/workflows/ci.yml/badge.svg)](https://github.com/tobiaswuerth/sift/actions/workflows/ci.yml)
50
+ [![Coverage](https://img.shields.io/badge/coverage-100%25-brightgreen)](https://github.com/tobiaswuerth/sift/actions/workflows/ci.yml)
51
+ [![Ruff](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/astral-sh/ruff/main/assets/badge/v2.json)](https://github.com/astral-sh/ruff)
52
+ [![License: MIT](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE)
53
+
54
+ Fast local search over your text and Markdown files, built for AI agents.
55
+
56
+ sift indexes folders of `.md` and `.txt` files, embeds them on your GPU, and runs a server that agents query over MCP.
57
+ It offers three kinds of search: exact keyword matching, semantic search, and hybrid retrieval with reranking for RAG.
58
+ Everything runs locally. No cloud services, no API keys.
59
+
60
+ ## Install
61
+
62
+ Requires Python 3.11 or newer. With [uv](https://docs.astral.sh/uv/):
63
+
64
+ ```bash
65
+ uv tool install --torch-backend auto sift-rag # installs the `sift` command
66
+ ```
67
+
68
+ Or run it without installing: `uvx --torch-backend auto sift-rag serve`. With pip, in a virtual environment:
69
+ `pip install sift-rag`.
70
+
71
+ On first use, sift downloads its two models from Hugging Face (about 1.5 GB).
72
+
73
+ ### GPU support
74
+
75
+ sift uses an NVIDIA GPU (CUDA) if there is one, else an Apple GPU, else the CPU, which works but embeds slowly.
76
+
77
+ - **Linux, macOS:** nothing to do. PyTorch from PyPI includes CUDA on Linux and uses the Apple GPU on macOS.
78
+ - **Windows:** PyTorch from PyPI has no CUDA support. uv's `--torch-backend auto`, used above, picks the CUDA build
79
+ that matches your driver. With pip, install it afterwards:
80
+ `pip install --force-reinstall --no-deps torch --index-url https://download.pytorch.org/whl/cu128`.
81
+
82
+ If an NVIDIA GPU sits unused, sift says so on start-up and prints the command that fixes it.
83
+
84
+ ## Usage
85
+
86
+ ```bash
87
+ sift add ~/papers --name papers # register a folder as a collection
88
+ sift index # scan files, chunk them, build the keyword index
89
+ sift embed # embed every chunk that has no vector yet
90
+ sift search '"reward shaping"' # exact keyword search
91
+ sift vsearch "how do robots plan brush strokes"
92
+ sift query "mixing pigments to match a target colour"
93
+ sift get papers/2401.02225.md # full document
94
+ sift status # collections and counts
95
+ sift rm papers # remove a collection from the index; its files stay on disk
96
+ sift serve # MCP server on http://127.0.0.1:8765/mcp
97
+ ```
98
+
99
+ `sift index` and `sift embed` are incremental: run them again after files change, and only new or changed files are
100
+ processed. Deleted files are removed. An interrupted `sift embed` continues where it stopped. A running server picks up
101
+ new vectors on its own.
102
+
103
+ Search commands accept `-k` (number of results), `-c` (collection), `--path` (glob within the collection, e.g.
104
+ `"2024/*"`), `-f KEY=VALUE` (front-matter filter, repeatable) and `--json`.
105
+
106
+ ### Front matter
107
+
108
+ YAML front matter with flat `key: value` lines becomes filterable metadata. A list field matches when it contains the
109
+ value:
110
+
111
+ ```markdown
112
+ ---
113
+ title: "Brush stroke planning"
114
+ categories: ["cs.RO", "cs.GR"]
115
+ year: 2024
116
+ ---
117
+ ```
118
+
119
+ ```bash
120
+ sift query "stroke order" -f categories=cs.RO -f year=2024
121
+ ```
122
+
123
+ The `title` field, or else the first heading, or else the file name, becomes the document title.
124
+
125
+ ## MCP
126
+
127
+ | Tool | Purpose |
128
+ |---|---|
129
+ | `search` | Exact keyword search, ranked by BM25. Supports `"phrases"`, `prefix*`, `AND`, `OR`, `NOT`. |
130
+ | `vsearch` | Semantic search. |
131
+ | `query` | Hybrid search for RAG: keyword and semantic candidates, reranked by a cross-encoder. |
132
+ | `get` | Full text of a document by its id. |
133
+ | `status` | Collections and counts. |
134
+
135
+ The search tools take `query`, `k`, `collection`, `path` and `filters`. Every result carries `id`
136
+ (`collection/path`), `title`, `lines`, `score` and the chunk `text`, so agents can cite their sources.
137
+
138
+ Claude Code, with the server running (`sift serve`), shared by any number of agents:
139
+
140
+ ```bash
141
+ claude mcp add --transport http sift http://127.0.0.1:8765/mcp
142
+ ```
143
+
144
+ Or let Claude Code start its own server over stdio:
145
+
146
+ ```bash
147
+ claude mcp add sift -- sift serve --stdio
148
+ ```
149
+
150
+ ## Configuration
151
+
152
+ None needed: sift picks the device, the GPU memory budget, the batch size and the number of worker processes itself.
153
+ Three optional settings exist for special cases:
154
+
155
+ | Variable | Default | Purpose |
156
+ |---|---|---|
157
+ | `SIFT_HOME` | `~/.sift` | Where the index lives |
158
+ | `SIFT_EMBED_MODEL` | `Qwen/Qwen3-Embedding-0.6B` | Any Sentence Transformers embedding model |
159
+ | `SIFT_RERANK_MODEL` | `Alibaba-NLP/gte-reranker-modernbert-base` | Any Sentence Transformers cross-encoder |
160
+
161
+ After changing the embedding model, run `sift embed --reset`. Models are downloaded from Hugging Face on first use.
162
+
163
+ ## How it works
164
+
165
+ - **Indexing:** worker processes read and parse files in parallel while the previous batch is written; small
166
+ updates skip the process start-up. Files are compared by modification time and size, then by content hash, so
167
+ unchanged files cost nothing. Markdown is split into chunks of about 3,000 characters (about 750 tokens) on line
168
+ boundaries, preferring headings, with a small overlap.
169
+ - **Storage:** one SQLite database in WAL mode holds documents, chunks, metadata, an FTS5 keyword index and the
170
+ vectors (fp16).
171
+ - **Embedding:** PyTorch in fp16 on the GPU (NVIDIA, else Apple), in batches sorted by length to keep padding low.
172
+ Each slab of 20,000 chunks is committed, so progress is never lost.
173
+ - **GPU memory:** sift shares the GPU with other programs. On NVIDIA GPUs it caps itself at the memory that is free
174
+ when it starts, minus headroom (10% of the GPU, at least 1 GB), asking NVML as well as CUDA, since on Windows CUDA
175
+ counts memory held by other programs as free. It then measures how much memory its longest text needs at growing
176
+ batch sizes and uses the largest batch that fills 90% of that budget; if a batch still doesn't fit, it is halved.
177
+ With less than 2 GB to spare, sift uses the CPU instead.
178
+ - **`search`:** SQLite FTS5 with BM25, titles weighted double. Input that isn't valid FTS5 syntax is treated as exact
179
+ phrases.
180
+ - **`vsearch`:** exact nearest-neighbour search as one matrix multiply over all vectors on the GPU. No approximate
181
+ index, so nothing to tune and no recall loss; a million 1024-dimensional vectors take about 2 GB of VRAM.
182
+ - **`query`:** the top 40 keyword and top 40 semantic candidates are merged by reciprocal rank fusion and reranked in
183
+ one GPU batch.
184
+ - **Server:** `sift serve` loads models and vectors once and keeps them in memory.
185
+
186
+ ## Scope
187
+
188
+ In scope: plain text and Markdown files; Linux, macOS and Windows; NVIDIA GPUs (CUDA) and Apple GPUs (MPS), otherwise
189
+ CPU; one user, any number of local agents.
190
+
191
+ Out of scope for now: PDF, Word and HTML parsing (convert to Markdown first); answer generation (sift retrieves, the
192
+ agent writes); approximate nearest-neighbour indexes; live file watching; remote access and authentication.
193
+
194
+ ## Development
195
+
196
+ ```bash
197
+ git clone https://github.com/tobiaswuerth/sift
198
+ cd sift
199
+ uv venv && uv pip install --torch-backend auto -e ".[dev]" # or with pip: pip install -r requirements.txt -e ".[dev]"
200
+ pre-commit install # lint, format and config checks on every commit
201
+ pytest --cov # 100% line and branch coverage is required
202
+ ```
203
+
204
+ The tests replace the embedding and reranking models with small deterministic fakes, so they need no GPU and no
205
+ model downloads; everything else, including the MCP server, runs for real. See [CONTRIBUTING.md](CONTRIBUTING.md)
206
+ for how changes and releases work.
207
+
208
+ ## License
209
+
210
+ [MIT](LICENSE)
@@ -0,0 +1,166 @@
1
+ # sift
2
+
3
+ [![PyPI](https://img.shields.io/pypi/v/sift-rag)](https://pypi.org/project/sift-rag/)
4
+ [![Python](https://img.shields.io/pypi/pyversions/sift-rag)](https://pypi.org/project/sift-rag/)
5
+ [![CI](https://github.com/tobiaswuerth/sift/actions/workflows/ci.yml/badge.svg)](https://github.com/tobiaswuerth/sift/actions/workflows/ci.yml)
6
+ [![Coverage](https://img.shields.io/badge/coverage-100%25-brightgreen)](https://github.com/tobiaswuerth/sift/actions/workflows/ci.yml)
7
+ [![Ruff](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/astral-sh/ruff/main/assets/badge/v2.json)](https://github.com/astral-sh/ruff)
8
+ [![License: MIT](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE)
9
+
10
+ Fast local search over your text and Markdown files, built for AI agents.
11
+
12
+ sift indexes folders of `.md` and `.txt` files, embeds them on your GPU, and runs a server that agents query over MCP.
13
+ It offers three kinds of search: exact keyword matching, semantic search, and hybrid retrieval with reranking for RAG.
14
+ Everything runs locally. No cloud services, no API keys.
15
+
16
+ ## Install
17
+
18
+ Requires Python 3.11 or newer. With [uv](https://docs.astral.sh/uv/):
19
+
20
+ ```bash
21
+ uv tool install --torch-backend auto sift-rag # installs the `sift` command
22
+ ```
23
+
24
+ Or run it without installing: `uvx --torch-backend auto sift-rag serve`. With pip, in a virtual environment:
25
+ `pip install sift-rag`.
26
+
27
+ On first use, sift downloads its two models from Hugging Face (about 1.5 GB).
28
+
29
+ ### GPU support
30
+
31
+ sift uses an NVIDIA GPU (CUDA) if there is one, else an Apple GPU, else the CPU, which works but embeds slowly.
32
+
33
+ - **Linux, macOS:** nothing to do. PyTorch from PyPI includes CUDA on Linux and uses the Apple GPU on macOS.
34
+ - **Windows:** PyTorch from PyPI has no CUDA support. uv's `--torch-backend auto`, used above, picks the CUDA build
35
+ that matches your driver. With pip, install it afterwards:
36
+ `pip install --force-reinstall --no-deps torch --index-url https://download.pytorch.org/whl/cu128`.
37
+
38
+ If an NVIDIA GPU sits unused, sift says so on start-up and prints the command that fixes it.
39
+
40
+ ## Usage
41
+
42
+ ```bash
43
+ sift add ~/papers --name papers # register a folder as a collection
44
+ sift index # scan files, chunk them, build the keyword index
45
+ sift embed # embed every chunk that has no vector yet
46
+ sift search '"reward shaping"' # exact keyword search
47
+ sift vsearch "how do robots plan brush strokes"
48
+ sift query "mixing pigments to match a target colour"
49
+ sift get papers/2401.02225.md # full document
50
+ sift status # collections and counts
51
+ sift rm papers # remove a collection from the index; its files stay on disk
52
+ sift serve # MCP server on http://127.0.0.1:8765/mcp
53
+ ```
54
+
55
+ `sift index` and `sift embed` are incremental: run them again after files change, and only new or changed files are
56
+ processed. Deleted files are removed. An interrupted `sift embed` continues where it stopped. A running server picks up
57
+ new vectors on its own.
58
+
59
+ Search commands accept `-k` (number of results), `-c` (collection), `--path` (glob within the collection, e.g.
60
+ `"2024/*"`), `-f KEY=VALUE` (front-matter filter, repeatable) and `--json`.
61
+
62
+ ### Front matter
63
+
64
+ YAML front matter with flat `key: value` lines becomes filterable metadata. A list field matches when it contains the
65
+ value:
66
+
67
+ ```markdown
68
+ ---
69
+ title: "Brush stroke planning"
70
+ categories: ["cs.RO", "cs.GR"]
71
+ year: 2024
72
+ ---
73
+ ```
74
+
75
+ ```bash
76
+ sift query "stroke order" -f categories=cs.RO -f year=2024
77
+ ```
78
+
79
+ The `title` field, or else the first heading, or else the file name, becomes the document title.
80
+
81
+ ## MCP
82
+
83
+ | Tool | Purpose |
84
+ |---|---|
85
+ | `search` | Exact keyword search, ranked by BM25. Supports `"phrases"`, `prefix*`, `AND`, `OR`, `NOT`. |
86
+ | `vsearch` | Semantic search. |
87
+ | `query` | Hybrid search for RAG: keyword and semantic candidates, reranked by a cross-encoder. |
88
+ | `get` | Full text of a document by its id. |
89
+ | `status` | Collections and counts. |
90
+
91
+ The search tools take `query`, `k`, `collection`, `path` and `filters`. Every result carries `id`
92
+ (`collection/path`), `title`, `lines`, `score` and the chunk `text`, so agents can cite their sources.
93
+
94
+ Claude Code, with the server running (`sift serve`), shared by any number of agents:
95
+
96
+ ```bash
97
+ claude mcp add --transport http sift http://127.0.0.1:8765/mcp
98
+ ```
99
+
100
+ Or let Claude Code start its own server over stdio:
101
+
102
+ ```bash
103
+ claude mcp add sift -- sift serve --stdio
104
+ ```
105
+
106
+ ## Configuration
107
+
108
+ None needed: sift picks the device, the GPU memory budget, the batch size and the number of worker processes itself.
109
+ Three optional settings exist for special cases:
110
+
111
+ | Variable | Default | Purpose |
112
+ |---|---|---|
113
+ | `SIFT_HOME` | `~/.sift` | Where the index lives |
114
+ | `SIFT_EMBED_MODEL` | `Qwen/Qwen3-Embedding-0.6B` | Any Sentence Transformers embedding model |
115
+ | `SIFT_RERANK_MODEL` | `Alibaba-NLP/gte-reranker-modernbert-base` | Any Sentence Transformers cross-encoder |
116
+
117
+ After changing the embedding model, run `sift embed --reset`. Models are downloaded from Hugging Face on first use.
118
+
119
+ ## How it works
120
+
121
+ - **Indexing:** worker processes read and parse files in parallel while the previous batch is written; small
122
+ updates skip the process start-up. Files are compared by modification time and size, then by content hash, so
123
+ unchanged files cost nothing. Markdown is split into chunks of about 3,000 characters (about 750 tokens) on line
124
+ boundaries, preferring headings, with a small overlap.
125
+ - **Storage:** one SQLite database in WAL mode holds documents, chunks, metadata, an FTS5 keyword index and the
126
+ vectors (fp16).
127
+ - **Embedding:** PyTorch in fp16 on the GPU (NVIDIA, else Apple), in batches sorted by length to keep padding low.
128
+ Each slab of 20,000 chunks is committed, so progress is never lost.
129
+ - **GPU memory:** sift shares the GPU with other programs. On NVIDIA GPUs it caps itself at the memory that is free
130
+ when it starts, minus headroom (10% of the GPU, at least 1 GB), asking NVML as well as CUDA, since on Windows CUDA
131
+ counts memory held by other programs as free. It then measures how much memory its longest text needs at growing
132
+ batch sizes and uses the largest batch that fills 90% of that budget; if a batch still doesn't fit, it is halved.
133
+ With less than 2 GB to spare, sift uses the CPU instead.
134
+ - **`search`:** SQLite FTS5 with BM25, titles weighted double. Input that isn't valid FTS5 syntax is treated as exact
135
+ phrases.
136
+ - **`vsearch`:** exact nearest-neighbour search as one matrix multiply over all vectors on the GPU. No approximate
137
+ index, so nothing to tune and no recall loss; a million 1024-dimensional vectors take about 2 GB of VRAM.
138
+ - **`query`:** the top 40 keyword and top 40 semantic candidates are merged by reciprocal rank fusion and reranked in
139
+ one GPU batch.
140
+ - **Server:** `sift serve` loads models and vectors once and keeps them in memory.
141
+
142
+ ## Scope
143
+
144
+ In scope: plain text and Markdown files; Linux, macOS and Windows; NVIDIA GPUs (CUDA) and Apple GPUs (MPS), otherwise
145
+ CPU; one user, any number of local agents.
146
+
147
+ Out of scope for now: PDF, Word and HTML parsing (convert to Markdown first); answer generation (sift retrieves, the
148
+ agent writes); approximate nearest-neighbour indexes; live file watching; remote access and authentication.
149
+
150
+ ## Development
151
+
152
+ ```bash
153
+ git clone https://github.com/tobiaswuerth/sift
154
+ cd sift
155
+ uv venv && uv pip install --torch-backend auto -e ".[dev]" # or with pip: pip install -r requirements.txt -e ".[dev]"
156
+ pre-commit install # lint, format and config checks on every commit
157
+ pytest --cov # 100% line and branch coverage is required
158
+ ```
159
+
160
+ The tests replace the embedding and reranking models with small deterministic fakes, so they need no GPU and no
161
+ model downloads; everything else, including the MCP server, runs for real. See [CONTRIBUTING.md](CONTRIBUTING.md)
162
+ for how changes and releases work.
163
+
164
+ ## License
165
+
166
+ [MIT](LICENSE)
@@ -0,0 +1,84 @@
1
+ [build-system]
2
+ requires = ["setuptools>=77"] # SPDX license expressions (PEP 639)
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "sift-rag"
7
+ dynamic = ["version"]
8
+ description = "Fast local search over text and Markdown files, built for AI agents."
9
+ readme = "README.md"
10
+ license = "MIT"
11
+ license-files = ["LICENSE"]
12
+ authors = [{ name = "Tobias Würth" }]
13
+ requires-python = ">=3.11"
14
+ dependencies = [
15
+ "numpy>=1.26",
16
+ "torch>=2.4",
17
+ "sentence-transformers>=5.0",
18
+ "mcp>=2.0",
19
+ "nvidia-ml-py>=12", # NVML: free GPU memory as nvidia-smi sees it
20
+ ]
21
+ keywords = ["search", "rag", "embeddings", "mcp", "markdown", "bm25", "semantic-search"]
22
+ classifiers = [
23
+ "Development Status :: 3 - Alpha",
24
+ "Environment :: Console",
25
+ "Environment :: GPU :: NVIDIA CUDA",
26
+ "Intended Audience :: Developers",
27
+ "Intended Audience :: Science/Research",
28
+ "Operating System :: MacOS",
29
+ "Operating System :: Microsoft :: Windows",
30
+ "Operating System :: POSIX :: Linux",
31
+ "Programming Language :: Python :: 3",
32
+ "Programming Language :: Python :: 3.11",
33
+ "Programming Language :: Python :: 3.12",
34
+ "Programming Language :: Python :: 3.13",
35
+ "Programming Language :: Python :: 3.14",
36
+ "Programming Language :: Python :: 3.15",
37
+ "Topic :: Scientific/Engineering :: Artificial Intelligence",
38
+ "Topic :: Text Processing :: Indexing",
39
+ ]
40
+
41
+ [project.urls]
42
+ Homepage = "https://github.com/tobiaswuerth/sift"
43
+ Documentation = "https://github.com/tobiaswuerth/sift#readme"
44
+ Issues = "https://github.com/tobiaswuerth/sift/issues"
45
+ Changelog = "https://github.com/tobiaswuerth/sift/blob/main/CHANGELOG.md"
46
+
47
+ [project.optional-dependencies]
48
+ dev = ["pytest>=8", "pytest-cov>=5", "ruff>=0.6", "pre-commit>=4", "build>=1.2", "twine>=6"]
49
+
50
+ [project.scripts]
51
+ sift = "sift.cli:main"
52
+ sift-rag = "sift.cli:main" # so that `uvx sift-rag ...` works: uvx runs the command named like the package
53
+
54
+ [tool.setuptools]
55
+ packages = ["sift"]
56
+
57
+ [tool.setuptools.dynamic]
58
+ version = { attr = "sift.__version__" }
59
+
60
+ [tool.ruff]
61
+ line-length = 120
62
+ target-version = "py311"
63
+
64
+ [tool.ruff.lint]
65
+ select = ["E", "F", "W", "I", "B", "UP", "SIM", "C4", "PIE", "PT", "RUF", "PERF"]
66
+ ignore = [
67
+ "B905", # zip() without strict=
68
+ "SIM905", # a word list as one split string reads better than a long list literal
69
+ ]
70
+
71
+ [tool.ruff.lint.flake8-pytest-style]
72
+ parametrize-names-type = "csv" # @pytest.mark.parametrize("a, b", ...)
73
+
74
+ [tool.pytest.ini_options]
75
+ testpaths = ["tests"]
76
+
77
+ [tool.coverage.run]
78
+ source = ["sift"]
79
+ branch = true
80
+
81
+ [tool.coverage.report]
82
+ fail_under = 100
83
+ show_missing = true
84
+ skip_covered = true
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,3 @@
1
+ """sift: fast local search over text and Markdown files, built for AI agents."""
2
+
3
+ __version__ = "0.1.0"
@@ -0,0 +1,3 @@
1
+ from .cli import main
2
+
3
+ main()
@@ -0,0 +1,109 @@
1
+ """Command-line interface."""
2
+
3
+ import argparse
4
+ import json
5
+ import sqlite3
6
+ import sys
7
+ import textwrap
8
+
9
+ from . import __version__, store
10
+
11
+
12
+ def _filters(pairs):
13
+ out = {}
14
+ for pair in pairs:
15
+ key, sep, value = pair.partition("=")
16
+ if not sep:
17
+ raise SystemExit(f"filter must be KEY=VALUE, got {pair!r}")
18
+ try:
19
+ out[key] = json.loads(value)
20
+ except ValueError:
21
+ out[key] = value
22
+ return out
23
+
24
+
25
+ def _print_hits(hits, as_json):
26
+ if as_json:
27
+ print(json.dumps(hits, ensure_ascii=False, indent=2))
28
+ return
29
+ for h in hits:
30
+ print(f"{h['score']:>8.4f} {h['id']}:{h['lines']} {h['title']}")
31
+ print(textwrap.indent(textwrap.shorten(h["text"], 300), " " * 10))
32
+
33
+
34
+ def main(argv=None):
35
+ p = argparse.ArgumentParser(prog="sift", description="Fast local search over text and Markdown files.")
36
+ p.add_argument("--version", action="version", version=f"sift {__version__}")
37
+ sub = p.add_subparsers(dest="cmd", required=True)
38
+
39
+ s = sub.add_parser("add", help="register a folder as a collection")
40
+ s.add_argument("path")
41
+ s.add_argument("--name", help="collection name (default: folder name)")
42
+ s = sub.add_parser("rm", help="remove a collection from the index (its files stay on disk)")
43
+ s.add_argument("name")
44
+ s = sub.add_parser("index", help="scan collections and update the keyword index")
45
+ s.add_argument("names", nargs="*", help="collections to scan (default: all)")
46
+ s = sub.add_parser("embed", help="embed all chunks that have no vector yet")
47
+ s.add_argument("--reset", action="store_true", help="drop all vectors first (after changing the model)")
48
+ for name, desc in (
49
+ ("search", "exact keyword search (BM25)"),
50
+ ("vsearch", "semantic search"),
51
+ ("query", "hybrid search with reranking"),
52
+ ):
53
+ s = sub.add_parser(name, help=desc)
54
+ s.add_argument("query")
55
+ s.add_argument("-k", type=int, default=10, help="number of results (default: 10)")
56
+ s.add_argument("-c", "--collection")
57
+ s.add_argument("--path", help='path glob within the collection, e.g. "2024/*"')
58
+ s.add_argument(
59
+ "-f", "--filter", action="append", default=[], metavar="KEY=VALUE", help="front-matter filter, repeatable"
60
+ )
61
+ s.add_argument("--json", action="store_true", help="print results as JSON")
62
+ s = sub.add_parser("get", help="print a document by its id (collection/path)")
63
+ s.add_argument("id")
64
+ sub.add_parser("status", help="show collections and index statistics")
65
+ s = sub.add_parser("serve", help="run the MCP server")
66
+ s.add_argument("--host", default="127.0.0.1")
67
+ s.add_argument("--port", type=int, default=8765)
68
+ s.add_argument("--stdio", action="store_true", help="serve over stdio instead of HTTP")
69
+
70
+ a = p.parse_args(argv)
71
+ if (sys.stdout.encoding or "").lower() != "utf-8": # e.g. a Windows pipe: titles may be any script
72
+ sys.stdout.reconfigure(encoding="utf-8")
73
+ db = store.connect()
74
+ try:
75
+ if a.cmd == "add":
76
+ name = store.add_collection(db, a.path, a.name)
77
+ print(f"added collection {name!r}; run `sift index` next", file=sys.stderr)
78
+ elif a.cmd == "rm":
79
+ n = store.remove_collection(db, a.name)
80
+ documents = "document" if n == 1 else "documents"
81
+ print(f"removed collection {a.name!r} and its {n} {documents} from the index;", end=" ", file=sys.stderr)
82
+ print("the files on disk are untouched", file=sys.stderr)
83
+ elif a.cmd == "index":
84
+ store.index(db, a.names)
85
+ elif a.cmd == "embed":
86
+ from .embed import embed_pending
87
+
88
+ embed_pending(db, a.reset)
89
+ elif a.cmd in ("search", "vsearch", "query"):
90
+ from .search import Index
91
+
92
+ fn = getattr(Index(db), a.cmd)
93
+ _print_hits(fn(a.query, a.k, a.collection, a.path, _filters(a.filter)), a.json)
94
+ elif a.cmd == "get":
95
+ from .search import Index
96
+
97
+ sys.stdout.write(Index(db).get(a.id))
98
+ elif a.cmd == "status":
99
+ from .search import Index
100
+
101
+ print(json.dumps(Index(db).status(), indent=2))
102
+ else: # serve
103
+ from .server import serve
104
+
105
+ serve(a.host, a.port, a.stdio)
106
+ except (LookupError, ValueError, sqlite3.Error) as e:
107
+ raise SystemExit(f"error: {e}") from None
108
+ except KeyboardInterrupt:
109
+ raise SystemExit(130) from None