clustergrep 0.8.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,52 @@
1
+ name: publish
2
+
3
+ # Fires only when you publish a GitHub Release, so nothing reaches PyPI
4
+ # without a deliberate act. A PyPI version can be yanked but never replaced,
5
+ # so the tests run again here rather than trusting that the push-time run of
6
+ # the same commit is still representative.
7
+ on:
8
+ release:
9
+ types: [published]
10
+
11
+ jobs:
12
+ publish:
13
+ runs-on: ubuntu-latest
14
+ # No `environment:` here, deliberately: the trusted publisher on PyPI was
15
+ # registered without one, and PyPI checks the environment claim only when
16
+ # the publisher names an environment to check it against. Declaring one
17
+ # here that PyPI does not know about buys nothing. To add the extra gate
18
+ # later, set the environment on both sides at once -- a mismatch fails the
19
+ # upload at the last step, after the build has already succeeded.
20
+ permissions:
21
+ # Required for trusted publishing: GitHub mints a short-lived OIDC token
22
+ # that PyPI verifies against the publisher you configured. No long-lived
23
+ # API token is stored in this repository.
24
+ id-token: write
25
+
26
+ steps:
27
+ # Install and test with exactly the steps test.yml already proves green
28
+ # on every push. This workflow runs once, at the only moment it matters,
29
+ # and cannot be rehearsed without publishing something -- so it reuses a
30
+ # known-good path rather than a tidier untested one. uv appears only for
31
+ # the two steps that need it.
32
+ - uses: actions/checkout@v4
33
+ - uses: actions/setup-python@v5
34
+ with:
35
+ python-version: "3.13"
36
+
37
+ - name: Install
38
+ run: python -m pip install --upgrade pip && python -m pip install -e '.[dev]'
39
+
40
+ - name: Download the WordNet corpus
41
+ run: python -m clustergrep --install-data
42
+
43
+ - name: Test
44
+ run: python -m pytest -q
45
+
46
+ - uses: astral-sh/setup-uv@v5
47
+
48
+ - name: Build
49
+ run: uv build
50
+
51
+ - name: Publish to PyPI
52
+ run: uv publish --trusted-publishing always
@@ -0,0 +1,41 @@
1
+ name: test
2
+
3
+ on:
4
+ push:
5
+ branches: [main]
6
+ pull_request:
7
+
8
+ jobs:
9
+ test:
10
+ runs-on: ${{ matrix.os }}
11
+ strategy:
12
+ fail-fast: false
13
+ matrix:
14
+ # The full interpreter range on one platform, plus one job each on
15
+ # macOS and Windows -- the per-platform data directories are real
16
+ # branching logic and deserve to be exercised where they apply.
17
+ os: [ubuntu-latest]
18
+ python-version: ["3.10", "3.11", "3.12", "3.13"]
19
+ include:
20
+ - os: macos-latest
21
+ python-version: "3.13"
22
+ - os: windows-latest
23
+ python-version: "3.13"
24
+
25
+ steps:
26
+ - uses: actions/checkout@v4
27
+ - uses: actions/setup-python@v5
28
+ with:
29
+ python-version: ${{ matrix.python-version }}
30
+
31
+ - name: Install
32
+ run: python -m pip install --upgrade pip && python -m pip install -e '.[dev]'
33
+
34
+ - name: Download the WordNet corpus
35
+ run: python -m clustergrep --install-data
36
+
37
+ - name: Show resolved paths
38
+ run: python -m clustergrep --paths
39
+
40
+ - name: Test
41
+ run: python -m pytest -q
@@ -0,0 +1,7 @@
1
+ __pycache__/
2
+ *.py[cod]
3
+ .venv/
4
+ .pytest_cache/
5
+ *.egg-info/
6
+ build/
7
+ dist/
@@ -0,0 +1,21 @@
1
+ # Changelog
2
+
3
+ ## 0.8.0
4
+
5
+ First public release.
6
+
7
+ - `clustergrep WORD FILE` matches a word and the words that mean roughly the
8
+ same thing, reporting the distance and the term that fired on every line.
9
+ - `--threshold 0 --no-inflect -s` is exactly `grep -w -F`, verified against
10
+ real grep in the test suite.
11
+ - Three backends behind one distance contract: `wordnet` (default; weighted
12
+ shortest path over the lexical graph, every distance an explainable path),
13
+ `vectors` (cosine distance over GloVe/word2vec), `thesaurus` (a TSV you pin,
14
+ edit and commit).
15
+ - `--explain` prints the cluster before you trust it; `--tsv` writes it out as
16
+ a thesaurus file; `--stats` reports which terms actually fired.
17
+ - Options may appear anywhere among the file names, as they may in grep.
18
+ - The WordNet corpus is downloaded on request with `--install-data`, never
19
+ bundled. It lands in a per-user data directory — `%LOCALAPPDATA%` on Windows,
20
+ `~/Library/Application Support` on macOS, `~/.local/share` elsewhere — and an
21
+ existing copy is reused. `--paths` shows where everything is.
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Niall Richard Murphy
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,285 @@
1
+ Metadata-Version: 2.5
2
+ Name: clustergrep
3
+ Version: 0.8.0
4
+ Summary: grep for a concept rather than a string, with the distance reported
5
+ Project-URL: Homepage, https://github.com/niallrmurphy/clustergrep
6
+ Project-URL: Repository, https://github.com/niallrmurphy/clustergrep
7
+ Project-URL: Issues, https://github.com/niallrmurphy/clustergrep/issues
8
+ Author: Niall Richard Murphy
9
+ License: MIT
10
+ License-File: LICENSE
11
+ Keywords: cli,grep,logs,nlp,search,semantic,wordnet
12
+ Classifier: Development Status :: 4 - Beta
13
+ Classifier: Environment :: Console
14
+ Classifier: Intended Audience :: Developers
15
+ Classifier: Intended Audience :: System Administrators
16
+ Classifier: License :: OSI Approved :: MIT License
17
+ Classifier: Operating System :: OS Independent
18
+ Classifier: Programming Language :: Python :: 3
19
+ Classifier: Programming Language :: Python :: 3 :: Only
20
+ Classifier: Topic :: Text Processing :: Indexing
21
+ Classifier: Topic :: Utilities
22
+ Requires-Python: >=3.10
23
+ Requires-Dist: nltk>=3.8
24
+ Provides-Extra: dev
25
+ Requires-Dist: numpy>=1.24; extra == 'dev'
26
+ Requires-Dist: pytest>=7; extra == 'dev'
27
+ Provides-Extra: vectors
28
+ Requires-Dist: numpy>=1.24; extra == 'vectors'
29
+ Description-Content-Type: text/markdown
30
+
31
+ # clustergrep
32
+
33
+ grep for a concept rather than a string, and tell you how far each match is
34
+ from what you asked for.
35
+
36
+ `grep` is mostly used for fixed or moderately varying text that you can determine
37
+ in advance: a fixed log line string (e.g. "reboot"), a well-formed timestamp,
38
+ a string which is known to occur around another particular string ("login for niallm
39
+ failed"). It is a poor fit for the other common search, the one where you
40
+ know the *idea* and not the words — for example the concept "escape".
41
+ Today we use something like `grep -E 'escape|flee|jailbreak|breakout'`, which
42
+ is inelegant, incomplete, and stale after we invent a new word for something,
43
+ which we seem to be doing a lot these days. Handing the whole job to a language model
44
+ trades those problems for different, potentially worse ones: hallucination,
45
+ skipping lines or an entire file, or answering differently this Tuesday as
46
+ opposed to last one.
47
+
48
+ clustergrep splits the difference. A lexical model decides *what to
49
+ look for*, once, up front, where you can read it and argue with it. After that,
50
+ finding it is ordinary deterministic matching with nothing in the loop.
51
+
52
+ ```console
53
+ $ clustergrep -t 0.25 escape incidents.log
54
+ 1:0.00:escape:2024-01-02 The prisoner escaped through the laundry chute.
55
+ 2:0.25:breakout:2024-01-03 Guards reported a breakout on B wing at 0300.
56
+ 4:0.20:flee:2024-01-05 Two inmates fled across the yard before dawn.
57
+ 5:0.25:jailbreak:2024-01-06 A jailbreak attempt was foiled by the perimeter fence.
58
+ 6:0.25:fly the coop:2024-01-07 He flew the coop while the van was being loaded.
59
+ 8:0.15:elude:2024-01-09 The suspect eluded officers for six hours.
60
+ 9:0.15:escapism:2024-01-10 Staff described a general air of escapism among the population.
61
+ 10:0.20:getaway:2024-01-11 Getaway vehicle recovered near the motorway.
62
+ 12:0.15:break loose:2024-01-13 The detainee broke loose during transfer.
63
+ ```
64
+
65
+ Of course, this matching surrenders the previous boolean does/doesn't match
66
+ approach, in favour of a notion of the _conceptual distance_ between the term
67
+ you searched for and what was found. This means that we report lines that matched,
68
+ and also the extent to which they matched. In the above, `0.25:jailbreak` says
69
+ *this line matched because "jailbreak" is 0.25 away from "escape"*. You can tune
70
+ `-t`, the flag that sets the conceptual/lexical distance until you're satisfied
71
+ with the results.
72
+
73
+ Note "fled" and "broke loose": clustergrep matches irregular inflections, so
74
+ the cluster does not have to enumerate them.
75
+
76
+ At the moment clustergrep does not handle non-English, but this is very definitely
77
+ an ambition. (Technically, the thesaurus is multi-language, but the inflection
78
+ rules are not.)
79
+
80
+ ## Install
81
+
82
+ ```bash
83
+ pip install clustergrep
84
+ clustergrep --install-data # once: fetches the WordNet corpus
85
+ ```
86
+
87
+ **The corpus is not bundled.** WordNet is about 10MB and is licensed separately
88
+ by Princeton, so clustergrep downloads it on request into a per-user directory
89
+ rather than shipping a copy. If you already have it — from a previous `nltk`
90
+ install, a system package, or `NLTK_DATA` — that copy is found first and
91
+ nothing is downloaded.
92
+
93
+ | | data | cache |
94
+ |------------|----------------------------------------|--------------------------------------|
95
+ | Linux | `~/.local/share/clustergrep` | `~/.cache/clustergrep` |
96
+ | macOS | `~/Library/Application Support/clustergrep` | `~/Library/Caches/clustergrep` |
97
+ | Windows | `%LOCALAPPDATA%\clustergrep\Data` | `%LOCALAPPDATA%\clustergrep\Cache` |
98
+
99
+ `XDG_DATA_HOME` and `XDG_CACHE_HOME` are honoured wherever they are set, and
100
+ `CLUSTERGREP_DATA` / `CLUSTERGREP_CACHE` override everything — necessary for
101
+ containers, CI, and read-only home directories.
102
+
103
+ ```console
104
+ $ clustergrep --paths
105
+ data /home/you/.local/share/clustergrep
106
+ cache /home/you/.cache/clustergrep
107
+ wordnet /home/you/.local/share/clustergrep/corpora/wordnet.zip
108
+ ```
109
+
110
+ Uninstalling is `pip uninstall clustergrep` and deleting those two
111
+ directories. Nothing else is written. The cache holds only files clustergrep
112
+ can rebuild, so deleting it alone is always safe.
113
+
114
+ ### From source
115
+
116
+ ```bash
117
+ git clone https://github.com/niallrmurphy/clustergrep
118
+ cd clustergrep
119
+ uv pip install -e '.[dev]'
120
+ uv run pytest -q
121
+ ```
122
+
123
+ ## Distance
124
+
125
+ Distance runs from 0.0 to 1.0.
126
+
127
+ | | |
128
+ |---|---|
129
+ | `0.0` | the word you typed, and nothing else |
130
+ | `~0.15` | another word for the same thing |
131
+ | `~0.25` | a narrower or related kind of the same thing |
132
+ | `~0.4` | recognisably connected, plausibly a different concept |
133
+ | `1.0` | unrelated |
134
+
135
+ `--threshold 0` admits only the literal word, so clustergrep becomes plain grep.
136
+ That is exact, not approximate:
137
+
138
+ ```console
139
+ $ clustergrep -t 0 --no-inflect -s --no-distance Guards incidents.log
140
+ $ grep -n -w -F Guards incidents.log # identical output
141
+ ```
142
+
143
+ *Notable distinction from grep*: cluster matching is always word-oriented,
144
+ like `grep -w`, because concepts are words, and root/syllabic tokenisation would
145
+ result in undue implementation complexity right now.
146
+
147
+ ### Where the numbers come from
148
+
149
+ The default backend walks WordNet as a weighted graph with costed edges,
150
+ and a penalty per "sense". Every distance is therefore a summed path,
151
+ which `--explain` will show:
152
+
153
+ ```console
154
+ $ clustergrep --explain -t 0.2 escape
155
+ 'escape' via wordnet, threshold 0.2: 15 term(s)
156
+ 0.00 escape
157
+ 0.15 break loose escape.v.01
158
+ 0.15 dodging evasion.n.03
159
+ 0.15 elude elude.v.02
160
+ 0.15 escapism escape.n.02
161
+ 0.15 evasion evasion.n.03
162
+ 0.15 flight escape.n.01
163
+ 0.15 get away escape.v.01
164
+ 0.15 get by get_off.v.05
165
+ 0.15 get off get_off.v.05
166
+ 0.15 get out get_off.v.05
167
+ 0.15 miss miss.v.09
168
+ 0.20 escapee escape.v.01 -derivation-> escapee
169
+ 0.20 flee escape.n.01 -derivation-> flee
170
+ 0.20 getaway escape.v.01 -derivation-> getaway
171
+ ```
172
+
173
+ | relation | cost | |
174
+ |---|---|---|
175
+ | synonym | 0.15 | another lemma of the same sense |
176
+ | similar / derivation / verb group | 0.20 | escape → escapee |
177
+ | hyponym | 0.25 | escape → jailbreak (narrower) |
178
+ | also-see | 0.35 | |
179
+ | hypernym | 0.40 | escape → movement (broader) |
180
+ | meronym | 0.45 | part, member, substance |
181
+ | antonym | 0.60 | `--antonyms`, off by default |
182
+
183
+ Narrowing costs less than broadening, because a narrower term keeps you inside
184
+ the concept while a broader one leaves it. Each successive dictionary sense of
185
+ the word adds 0.05, so the dominant reading dominates the cluster. There is little
186
+ that is truly objective about this scoring, but is pragmatically enough to work
187
+ with right now, and we are open to other suggestions.
188
+
189
+ ## Backends
190
+
191
+ ```
192
+ --backend wordnet (default) offline, explainable, English
193
+ --backend vectors --model glove.6B.100d.txt
194
+ --backend thesaurus --thesaurus terms.tsv
195
+ ```
196
+
197
+ **wordnet** just knows lexicographic relations: relatively precise, but only
198
+ lexically bound, so "escape" and "jail" have no plausible path.
199
+
200
+ **vectors** is the opposite. Cosine distance over any GloVe or word2vec
201
+ text export "knows" that `escape → jail` immediately but isn't comparably
202
+ enumerable as a path from an accessible graph search.
203
+
204
+ **thesaurus** is a patch file you can use to add your own terms (but
205
+ _replaces_ WordNet rather than augments it, so you need to seed from the
206
+ original):
207
+
208
+ ```
209
+ # concept term distance note
210
+ escape jailbreak 0.25
211
+ escape exfil 0.30 our term
212
+ ```
213
+
214
+ ```bash
215
+ clustergrep --explain -t 0.3 escape --tsv > escape.tsv
216
+ clustergrep -b thesaurus --thesaurus escape.tsv escape incidents.log
217
+ ```
218
+
219
+ From then on the search is fully reproducible and a new term is a one-line diff.
220
+
221
+ ## Options
222
+
223
+ Familiar from grep: `-i` `-v` `-c` `-l` `-L` `-o` `-n` `-r` `-m` `-H`
224
+ `--include` `--exclude` `--color`. Exit codes match too — 0 matched, 1 nothing
225
+ matched, 2 error.
226
+
227
+ Particular to this tool:
228
+
229
+ | | |
230
+ |---|---|
231
+ | `-t, --threshold` | how far to reach (default 0.4) |
232
+ | `--explain`, `--tsv` | print the cluster and stop |
233
+ | `--senses` | list the word's WordNet senses, for `--sense` |
234
+ | `--pos n\|v\|a\|r` | one part of speech only |
235
+ | `--sense N` | pin one reading of the word |
236
+ | `--stats` | which cluster terms actually fired |
237
+ | `--sort` | nearest matches first |
238
+ | `--json` | one object per match |
239
+ | `--no-inflect` | exact surface forms only |
240
+ | `--no-distance` | grep-shaped output |
241
+
242
+ `--stats` is for tuning:
243
+
244
+ ```console
245
+ $ clustergrep -t 0.4 escape incidents.log --stats -c
246
+ 10 match(es) from 10 of 62 cluster term(s)
247
+ 0.00 escape 1
248
+ 0.25 breakout 1
249
+ 0.20 flee 1
250
+ 0.25 jailbreak 1
251
+ 0.25 fly the coop 1
252
+ 0.30 leakage 1
253
+ 0.15 elude 1
254
+ 0.15 escapism 1
255
+ 0.20 getaway 1
256
+ 0.15 break loose 1
257
+ ```
258
+
259
+ ## Known limits
260
+
261
+ **Polysemy.** A cluster covers every sense of the word. At `-t 0.4`, "escape"
262
+ reaches `leakage` and `outflow` via the concept of fluid-discharge, so a line about
263
+ reactor coolant will match.
264
+
265
+ **Distances are not probabilities** and are not comparable between backends.
266
+ They only order matches within one search.
267
+
268
+ **English only**, and only as current as WordNet 3.0.
269
+
270
+ **Irregular inflections** (`fled`, `broke`) come from WordNet's exception lists,
271
+ so they are available under the default backend. The vectors and thesaurus
272
+ backends get regular suffix rules only: if you have a specific requirement,
273
+ put them in the TSV.
274
+
275
+ **Speed.** Roughly 8µs per line — about 5s for a 24MB, 300k-line file, against
276
+ 0.02s for `grep -E`. This is Python's regex engine over a large alternation
277
+ and the extra columns. For now, if you need fast loops over lots of data, use
278
+ `--explain` to generate the alternation and hand it to real grep.
279
+
280
+ ## Licence
281
+
282
+ clustergrep is MIT licensed. The WordNet corpus it downloads is **not** part of
283
+ this distribution and is covered by [Princeton's WordNet
284
+ licence](https://wordnet.princeton.edu/license-and-commercial-use). Any vector
285
+ model you point `--model` at carries whatever licence its publisher gave it.
@@ -0,0 +1,255 @@
1
+ # clustergrep
2
+
3
+ grep for a concept rather than a string, and tell you how far each match is
4
+ from what you asked for.
5
+
6
+ `grep` is mostly used for fixed or moderately varying text that you can determine
7
+ in advance: a fixed log line string (e.g. "reboot"), a well-formed timestamp,
8
+ a string which is known to occur around another particular string ("login for niallm
9
+ failed"). It is a poor fit for the other common search, the one where you
10
+ know the *idea* and not the words — for example the concept "escape".
11
+ Today we use something like `grep -E 'escape|flee|jailbreak|breakout'`, which
12
+ is inelegant, incomplete, and stale after we invent a new word for something,
13
+ which we seem to be doing a lot these days. Handing the whole job to a language model
14
+ trades those problems for different, potentially worse ones: hallucination,
15
+ skipping lines or an entire file, or answering differently this Tuesday as
16
+ opposed to last one.
17
+
18
+ clustergrep splits the difference. A lexical model decides *what to
19
+ look for*, once, up front, where you can read it and argue with it. After that,
20
+ finding it is ordinary deterministic matching with nothing in the loop.
21
+
22
+ ```console
23
+ $ clustergrep -t 0.25 escape incidents.log
24
+ 1:0.00:escape:2024-01-02 The prisoner escaped through the laundry chute.
25
+ 2:0.25:breakout:2024-01-03 Guards reported a breakout on B wing at 0300.
26
+ 4:0.20:flee:2024-01-05 Two inmates fled across the yard before dawn.
27
+ 5:0.25:jailbreak:2024-01-06 A jailbreak attempt was foiled by the perimeter fence.
28
+ 6:0.25:fly the coop:2024-01-07 He flew the coop while the van was being loaded.
29
+ 8:0.15:elude:2024-01-09 The suspect eluded officers for six hours.
30
+ 9:0.15:escapism:2024-01-10 Staff described a general air of escapism among the population.
31
+ 10:0.20:getaway:2024-01-11 Getaway vehicle recovered near the motorway.
32
+ 12:0.15:break loose:2024-01-13 The detainee broke loose during transfer.
33
+ ```
34
+
35
+ Of course, this matching surrenders the previous boolean does/doesn't match
36
+ approach, in favour of a notion of the _conceptual distance_ between the term
37
+ you searched for and what was found. This means that we report lines that matched,
38
+ and also the extent to which they matched. In the above, `0.25:jailbreak` says
39
+ *this line matched because "jailbreak" is 0.25 away from "escape"*. You can tune
40
+ `-t`, the flag that sets the conceptual/lexical distance until you're satisfied
41
+ with the results.
42
+
43
+ Note "fled" and "broke loose": clustergrep matches irregular inflections, so
44
+ the cluster does not have to enumerate them.
45
+
46
+ At the moment clustergrep does not handle non-English, but this is very definitely
47
+ an ambition. (Technically, the thesaurus is multi-language, but the inflection
48
+ rules are not.)
49
+
50
+ ## Install
51
+
52
+ ```bash
53
+ pip install clustergrep
54
+ clustergrep --install-data # once: fetches the WordNet corpus
55
+ ```
56
+
57
+ **The corpus is not bundled.** WordNet is about 10MB and is licensed separately
58
+ by Princeton, so clustergrep downloads it on request into a per-user directory
59
+ rather than shipping a copy. If you already have it — from a previous `nltk`
60
+ install, a system package, or `NLTK_DATA` — that copy is found first and
61
+ nothing is downloaded.
62
+
63
+ | | data | cache |
64
+ |------------|----------------------------------------|--------------------------------------|
65
+ | Linux | `~/.local/share/clustergrep` | `~/.cache/clustergrep` |
66
+ | macOS | `~/Library/Application Support/clustergrep` | `~/Library/Caches/clustergrep` |
67
+ | Windows | `%LOCALAPPDATA%\clustergrep\Data` | `%LOCALAPPDATA%\clustergrep\Cache` |
68
+
69
+ `XDG_DATA_HOME` and `XDG_CACHE_HOME` are honoured wherever they are set, and
70
+ `CLUSTERGREP_DATA` / `CLUSTERGREP_CACHE` override everything — necessary for
71
+ containers, CI, and read-only home directories.
72
+
73
+ ```console
74
+ $ clustergrep --paths
75
+ data /home/you/.local/share/clustergrep
76
+ cache /home/you/.cache/clustergrep
77
+ wordnet /home/you/.local/share/clustergrep/corpora/wordnet.zip
78
+ ```
79
+
80
+ Uninstalling is `pip uninstall clustergrep` and deleting those two
81
+ directories. Nothing else is written. The cache holds only files clustergrep
82
+ can rebuild, so deleting it alone is always safe.
83
+
84
+ ### From source
85
+
86
+ ```bash
87
+ git clone https://github.com/niallrmurphy/clustergrep
88
+ cd clustergrep
89
+ uv pip install -e '.[dev]'
90
+ uv run pytest -q
91
+ ```
92
+
93
+ ## Distance
94
+
95
+ Distance runs from 0.0 to 1.0.
96
+
97
+ | | |
98
+ |---|---|
99
+ | `0.0` | the word you typed, and nothing else |
100
+ | `~0.15` | another word for the same thing |
101
+ | `~0.25` | a narrower or related kind of the same thing |
102
+ | `~0.4` | recognisably connected, plausibly a different concept |
103
+ | `1.0` | unrelated |
104
+
105
+ `--threshold 0` admits only the literal word, so clustergrep becomes plain grep.
106
+ That is exact, not approximate:
107
+
108
+ ```console
109
+ $ clustergrep -t 0 --no-inflect -s --no-distance Guards incidents.log
110
+ $ grep -n -w -F Guards incidents.log # identical output
111
+ ```
112
+
113
+ *Notable distinction from grep*: cluster matching is always word-oriented,
114
+ like `grep -w`, because concepts are words, and root/syllabic tokenisation would
115
+ result in undue implementation complexity right now.
116
+
117
+ ### Where the numbers come from
118
+
119
+ The default backend walks WordNet as a weighted graph with costed edges,
120
+ and a penalty per "sense". Every distance is therefore a summed path,
121
+ which `--explain` will show:
122
+
123
+ ```console
124
+ $ clustergrep --explain -t 0.2 escape
125
+ 'escape' via wordnet, threshold 0.2: 15 term(s)
126
+ 0.00 escape
127
+ 0.15 break loose escape.v.01
128
+ 0.15 dodging evasion.n.03
129
+ 0.15 elude elude.v.02
130
+ 0.15 escapism escape.n.02
131
+ 0.15 evasion evasion.n.03
132
+ 0.15 flight escape.n.01
133
+ 0.15 get away escape.v.01
134
+ 0.15 get by get_off.v.05
135
+ 0.15 get off get_off.v.05
136
+ 0.15 get out get_off.v.05
137
+ 0.15 miss miss.v.09
138
+ 0.20 escapee escape.v.01 -derivation-> escapee
139
+ 0.20 flee escape.n.01 -derivation-> flee
140
+ 0.20 getaway escape.v.01 -derivation-> getaway
141
+ ```
142
+
143
+ | relation | cost | |
144
+ |---|---|---|
145
+ | synonym | 0.15 | another lemma of the same sense |
146
+ | similar / derivation / verb group | 0.20 | escape → escapee |
147
+ | hyponym | 0.25 | escape → jailbreak (narrower) |
148
+ | also-see | 0.35 | |
149
+ | hypernym | 0.40 | escape → movement (broader) |
150
+ | meronym | 0.45 | part, member, substance |
151
+ | antonym | 0.60 | `--antonyms`, off by default |
152
+
153
+ Narrowing costs less than broadening, because a narrower term keeps you inside
154
+ the concept while a broader one leaves it. Each successive dictionary sense of
155
+ the word adds 0.05, so the dominant reading dominates the cluster. There is little
156
+ that is truly objective about this scoring, but is pragmatically enough to work
157
+ with right now, and we are open to other suggestions.
158
+
159
+ ## Backends
160
+
161
+ ```
162
+ --backend wordnet (default) offline, explainable, English
163
+ --backend vectors --model glove.6B.100d.txt
164
+ --backend thesaurus --thesaurus terms.tsv
165
+ ```
166
+
167
+ **wordnet** just knows lexicographic relations: relatively precise, but only
168
+ lexically bound, so "escape" and "jail" have no plausible path.
169
+
170
+ **vectors** is the opposite. Cosine distance over any GloVe or word2vec
171
+ text export "knows" that `escape → jail` immediately but isn't comparably
172
+ enumerable as a path from an accessible graph search.
173
+
174
+ **thesaurus** is a patch file you can use to add your own terms (but
175
+ _replaces_ WordNet rather than augments it, so you need to seed from the
176
+ original):
177
+
178
+ ```
179
+ # concept term distance note
180
+ escape jailbreak 0.25
181
+ escape exfil 0.30 our term
182
+ ```
183
+
184
+ ```bash
185
+ clustergrep --explain -t 0.3 escape --tsv > escape.tsv
186
+ clustergrep -b thesaurus --thesaurus escape.tsv escape incidents.log
187
+ ```
188
+
189
+ From then on the search is fully reproducible and a new term is a one-line diff.
190
+
191
+ ## Options
192
+
193
+ Familiar from grep: `-i` `-v` `-c` `-l` `-L` `-o` `-n` `-r` `-m` `-H`
194
+ `--include` `--exclude` `--color`. Exit codes match too — 0 matched, 1 nothing
195
+ matched, 2 error.
196
+
197
+ Particular to this tool:
198
+
199
+ | | |
200
+ |---|---|
201
+ | `-t, --threshold` | how far to reach (default 0.4) |
202
+ | `--explain`, `--tsv` | print the cluster and stop |
203
+ | `--senses` | list the word's WordNet senses, for `--sense` |
204
+ | `--pos n\|v\|a\|r` | one part of speech only |
205
+ | `--sense N` | pin one reading of the word |
206
+ | `--stats` | which cluster terms actually fired |
207
+ | `--sort` | nearest matches first |
208
+ | `--json` | one object per match |
209
+ | `--no-inflect` | exact surface forms only |
210
+ | `--no-distance` | grep-shaped output |
211
+
212
+ `--stats` is for tuning:
213
+
214
+ ```console
215
+ $ clustergrep -t 0.4 escape incidents.log --stats -c
216
+ 10 match(es) from 10 of 62 cluster term(s)
217
+ 0.00 escape 1
218
+ 0.25 breakout 1
219
+ 0.20 flee 1
220
+ 0.25 jailbreak 1
221
+ 0.25 fly the coop 1
222
+ 0.30 leakage 1
223
+ 0.15 elude 1
224
+ 0.15 escapism 1
225
+ 0.20 getaway 1
226
+ 0.15 break loose 1
227
+ ```
228
+
229
+ ## Known limits
230
+
231
+ **Polysemy.** A cluster covers every sense of the word. At `-t 0.4`, "escape"
232
+ reaches `leakage` and `outflow` via the concept of fluid-discharge, so a line about
233
+ reactor coolant will match.
234
+
235
+ **Distances are not probabilities** and are not comparable between backends.
236
+ They only order matches within one search.
237
+
238
+ **English only**, and only as current as WordNet 3.0.
239
+
240
+ **Irregular inflections** (`fled`, `broke`) come from WordNet's exception lists,
241
+ so they are available under the default backend. The vectors and thesaurus
242
+ backends get regular suffix rules only: if you have a specific requirement,
243
+ put them in the TSV.
244
+
245
+ **Speed.** Roughly 8µs per line — about 5s for a 24MB, 300k-line file, against
246
+ 0.02s for `grep -E`. This is Python's regex engine over a large alternation
247
+ and the extra columns. For now, if you need fast loops over lots of data, use
248
+ `--explain` to generate the alternation and hand it to real grep.
249
+
250
+ ## Licence
251
+
252
+ clustergrep is MIT licensed. The WordNet corpus it downloads is **not** part of
253
+ this distribution and is covered by [Princeton's WordNet
254
+ licence](https://wordnet.princeton.edu/license-and-commercial-use). Any vector
255
+ model you point `--model` at carries whatever licence its publisher gave it.