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.
- clustergrep-0.8.0/.github/workflows/publish.yml +52 -0
- clustergrep-0.8.0/.github/workflows/test.yml +41 -0
- clustergrep-0.8.0/.gitignore +7 -0
- clustergrep-0.8.0/CHANGELOG.md +21 -0
- clustergrep-0.8.0/LICENSE +21 -0
- clustergrep-0.8.0/PKG-INFO +285 -0
- clustergrep-0.8.0/README.md +255 -0
- clustergrep-0.8.0/examples/incidents.log +13 -0
- clustergrep-0.8.0/pyproject.toml +59 -0
- clustergrep-0.8.0/src/clustergrep/__init__.py +3 -0
- clustergrep-0.8.0/src/clustergrep/__main__.py +4 -0
- clustergrep-0.8.0/src/clustergrep/cli.py +570 -0
- clustergrep-0.8.0/src/clustergrep/cluster.py +129 -0
- clustergrep-0.8.0/src/clustergrep/matcher.py +178 -0
- clustergrep-0.8.0/src/clustergrep/paths.py +97 -0
- clustergrep-0.8.0/src/clustergrep/thesaurus.py +112 -0
- clustergrep-0.8.0/src/clustergrep/vectors.py +165 -0
- clustergrep-0.8.0/src/clustergrep/wordnet.py +344 -0
- clustergrep-0.8.0/tests/test_cli.py +263 -0
- clustergrep-0.8.0/tests/test_cluster.py +57 -0
- clustergrep-0.8.0/tests/test_install_data.py +80 -0
- clustergrep-0.8.0/tests/test_matcher.py +92 -0
- clustergrep-0.8.0/tests/test_paths.py +76 -0
- clustergrep-0.8.0/tests/test_thesaurus.py +76 -0
- clustergrep-0.8.0/tests/test_vectors.py +87 -0
- clustergrep-0.8.0/tests/test_wordnet.py +117 -0
|
@@ -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,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.
|