dirsql-plugin-embeddings 0.1.18__tar.gz → 0.1.19__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.
- {dirsql_plugin_embeddings-0.1.18 → dirsql_plugin_embeddings-0.1.19}/PKG-INFO +1 -1
- dirsql_plugin_embeddings-0.1.19/changelog.d/2026-09-05-one-function-per-file.md +7 -0
- dirsql_plugin_embeddings-0.1.19/e2e-attestations/claude-1095-ofpf-embeddings.json +7 -0
- dirsql_plugin_embeddings-0.1.19/src/dirsql_plugin_embeddings/embedding/model.py +7 -0
- dirsql_plugin_embeddings-0.1.18/src/dirsql_plugin_embeddings/embedding/model.py → dirsql_plugin_embeddings-0.1.19/src/dirsql_plugin_embeddings/embedding/model_identifier.py +0 -9
- {dirsql_plugin_embeddings-0.1.18 → dirsql_plugin_embeddings-0.1.19}/src/dirsql_plugin_embeddings/embedding/worker.py +2 -1
- dirsql_plugin_embeddings-0.1.19/src/dirsql_plugin_embeddings/search/build_search_sql.py +18 -0
- dirsql_plugin_embeddings-0.1.19/src/dirsql_plugin_embeddings/search/count_corpus_sql.py +6 -0
- dirsql_plugin_embeddings-0.1.19/src/dirsql_plugin_embeddings/search/embed_call.py +7 -0
- dirsql_plugin_embeddings-0.1.19/src/dirsql_plugin_embeddings/search/no_rows_message.py +7 -0
- dirsql_plugin_embeddings-0.1.19/src/dirsql_plugin_embeddings/search/normalize_glob.py +11 -0
- dirsql_plugin_embeddings-0.1.19/src/dirsql_plugin_embeddings/search/quote.py +3 -0
- dirsql_plugin_embeddings-0.1.19/src/dirsql_plugin_embeddings/search/run.py +17 -0
- dirsql_plugin_embeddings-0.1.19/src/dirsql_plugin_embeddings/search/search_rows.py +23 -0
- {dirsql_plugin_embeddings-0.1.18 → dirsql_plugin_embeddings-0.1.19}/testing-conventions.toml +1 -1
- dirsql_plugin_embeddings-0.1.18/src/dirsql_plugin_embeddings/search/run.py +0 -45
- dirsql_plugin_embeddings-0.1.18/src/dirsql_plugin_embeddings/search/sql.py +0 -41
- {dirsql_plugin_embeddings-0.1.18 → dirsql_plugin_embeddings-0.1.19}/.gitignore +0 -0
- {dirsql_plugin_embeddings-0.1.18 → dirsql_plugin_embeddings-0.1.19}/README.md +0 -0
- {dirsql_plugin_embeddings-0.1.18 → dirsql_plugin_embeddings-0.1.19}/changelog.d/2026-08-10-delete-old-surface.md +0 -0
- {dirsql_plugin_embeddings-0.1.18 → dirsql_plugin_embeddings-0.1.19}/changelog.d/2026-08-10-embed-worker.md +0 -0
- {dirsql_plugin_embeddings-0.1.18 → dirsql_plugin_embeddings-0.1.19}/changelog.d/2026-08-11-one-liner-cli.md +0 -0
- {dirsql_plugin_embeddings-0.1.18 → dirsql_plugin_embeddings-0.1.19}/changelog.d/2026-08-12-empty-search-reports-why.md +0 -0
- {dirsql_plugin_embeddings-0.1.18 → dirsql_plugin_embeddings-0.1.19}/changelog.d/2026-08-12-no-per-call-progress-bar.md +0 -0
- {dirsql_plugin_embeddings-0.1.18 → dirsql_plugin_embeddings-0.1.19}/changelog.d/2026-08-12-null-embedding-guard.md +0 -0
- {dirsql_plugin_embeddings-0.1.18 → dirsql_plugin_embeddings-0.1.19}/changelog.d/2026-08-12-worker-semaphore-leak.md +0 -0
- {dirsql_plugin_embeddings-0.1.18 → dirsql_plugin_embeddings-0.1.19}/changelog.d/2026-09-04-report-cache-hits.md +0 -0
- {dirsql_plugin_embeddings-0.1.18 → dirsql_plugin_embeddings-0.1.19}/e2e-attestations/claude-804-embed-worker.json +0 -0
- {dirsql_plugin_embeddings-0.1.18 → dirsql_plugin_embeddings-0.1.19}/e2e-attestations/claude-805-one-liner.json +0 -0
- {dirsql_plugin_embeddings-0.1.18 → dirsql_plugin_embeddings-0.1.19}/e2e-attestations/claude-814-suppress-per-call-progress.json +0 -0
- {dirsql_plugin_embeddings-0.1.18 → dirsql_plugin_embeddings-0.1.19}/e2e-attestations/claude-815-loky-semaphore.json +0 -0
- {dirsql_plugin_embeddings-0.1.18 → dirsql_plugin_embeddings-0.1.19}/e2e-attestations/claude-816-empty-corpus-error.json +0 -0
- {dirsql_plugin_embeddings-0.1.18 → dirsql_plugin_embeddings-0.1.19}/e2e-attestations/claude-817-null-embedding-guard.json +0 -0
- {dirsql_plugin_embeddings-0.1.18 → dirsql_plugin_embeddings-0.1.19}/e2e-attestations/claude-tackle-1034-plugin-flag.json +0 -0
- {dirsql_plugin_embeddings-0.1.18 → dirsql_plugin_embeddings-0.1.19}/migrations.d/2026-08-10-delete-old-surface.md +0 -0
- {dirsql_plugin_embeddings-0.1.18 → dirsql_plugin_embeddings-0.1.19}/migrations.d/2026-09-04-report-cache-hits.md +0 -0
- {dirsql_plugin_embeddings-0.1.18 → dirsql_plugin_embeddings-0.1.19}/pyproject.toml +0 -0
- {dirsql_plugin_embeddings-0.1.18 → dirsql_plugin_embeddings-0.1.19}/src/dirsql_plugin_embeddings/__init__.py +0 -0
- {dirsql_plugin_embeddings-0.1.18 → dirsql_plugin_embeddings-0.1.19}/src/dirsql_plugin_embeddings/cli/__init__.py +0 -0
- {dirsql_plugin_embeddings-0.1.18 → dirsql_plugin_embeddings-0.1.19}/src/dirsql_plugin_embeddings/cli/main.py +0 -0
- {dirsql_plugin_embeddings-0.1.18 → dirsql_plugin_embeddings-0.1.19}/src/dirsql_plugin_embeddings/cli/search.py +0 -0
- {dirsql_plugin_embeddings-0.1.18 → dirsql_plugin_embeddings-0.1.19}/src/dirsql_plugin_embeddings/cli/worker.py +0 -0
- {dirsql_plugin_embeddings-0.1.18 → dirsql_plugin_embeddings-0.1.19}/src/dirsql_plugin_embeddings/dirsql.toml +0 -0
- {dirsql_plugin_embeddings-0.1.18 → dirsql_plugin_embeddings-0.1.19}/src/dirsql_plugin_embeddings/embedding/__init__.py +0 -0
- {dirsql_plugin_embeddings-0.1.18 → dirsql_plugin_embeddings-0.1.19}/src/dirsql_plugin_embeddings/embedding/cache.py +0 -0
- {dirsql_plugin_embeddings-0.1.18 → dirsql_plugin_embeddings-0.1.19}/src/dirsql_plugin_embeddings/embedding/progress.py +0 -0
- {dirsql_plugin_embeddings-0.1.18 → dirsql_plugin_embeddings-0.1.19}/src/dirsql_plugin_embeddings/embedding/values.py +0 -0
- {dirsql_plugin_embeddings-0.1.18 → dirsql_plugin_embeddings-0.1.19}/src/dirsql_plugin_embeddings/search/__init__.py +0 -0
- {dirsql_plugin_embeddings-0.1.18 → dirsql_plugin_embeddings-0.1.19}/src/dirsql_plugin_embeddings/search/output.py +0 -0
- {dirsql_plugin_embeddings-0.1.18 → dirsql_plugin_embeddings-0.1.19}/tests/conftest.py +0 -0
- {dirsql_plugin_embeddings-0.1.18 → dirsql_plugin_embeddings-0.1.19}/tests/e2e/__init__.py +0 -0
- {dirsql_plugin_embeddings-0.1.18 → dirsql_plugin_embeddings-0.1.19}/tests/integration/__init__.py +0 -0
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
**Changed**
|
|
2
|
+
|
|
3
|
+
Split `search/sql.py`, `search/run.py`, and `embedding/model.py` into one
|
|
4
|
+
module per non-trivial function, so the tree is clean at
|
|
5
|
+
`one-function-per-file`'s strictest threshold. Every symbol keeps its behavior;
|
|
6
|
+
`search.run._search` is now `search.search_rows.search_rows`, and the SQL
|
|
7
|
+
builders moved to modules named for them.
|
|
@@ -1,12 +1,3 @@
|
|
|
1
|
-
DEFAULT_MODEL_ID = "minishlab/potion-retrieval-32M"
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
def load_model(model_id):
|
|
5
|
-
from model2vec import StaticModel
|
|
6
|
-
|
|
7
|
-
return StaticModel.from_pretrained(model_id)
|
|
8
|
-
|
|
9
|
-
|
|
10
1
|
def model_identifier(model_id, model):
|
|
11
2
|
config = getattr(model, "config", None) or {}
|
|
12
3
|
version = config.get("model2vec_version")
|
|
@@ -3,6 +3,7 @@ from hashlib import sha256
|
|
|
3
3
|
|
|
4
4
|
from . import model
|
|
5
5
|
from .cache import make_cache
|
|
6
|
+
from .model_identifier import model_identifier
|
|
6
7
|
from .values import ProtocolError, decode_value
|
|
7
8
|
|
|
8
9
|
MALFORMED_SHAPE = (
|
|
@@ -40,7 +41,7 @@ class Worker:
|
|
|
40
41
|
def embed(self, text, model_id):
|
|
41
42
|
"""Return the vector for ``text``, and whether the cache served it."""
|
|
42
43
|
loaded = self._model(model_id)
|
|
43
|
-
identifier =
|
|
44
|
+
identifier = model_identifier(model_id, loaded)
|
|
44
45
|
digest = sha256(text.encode("utf-8")).hexdigest()
|
|
45
46
|
self._pending = (text, loaded)
|
|
46
47
|
self._computed = False
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
from .embed_call import embed_call
|
|
2
|
+
from .normalize_glob import normalize_glob
|
|
3
|
+
from .quote import quote
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def build_search_sql(glob, query, limit, model=None):
|
|
7
|
+
outer = embed_call(quote(query), model)
|
|
8
|
+
inner = embed_call("content", model)
|
|
9
|
+
# A matched file that is unreadable, not valid UTF-8, or deleted mid-scan
|
|
10
|
+
# has NULL content, so its embedding is NULL too. Dropping those rows is
|
|
11
|
+
# what keeps the ranking honest: a NULL distance sorts FIRST ascending in
|
|
12
|
+
# SQLite, so unrankable files would otherwise take the top-k slots.
|
|
13
|
+
return (
|
|
14
|
+
f"SELECT path, vec_distance_cosine(emb, {outer}) AS distance"
|
|
15
|
+
f" FROM (SELECT path, {inner} AS emb FROM {quote(normalize_glob(glob))})"
|
|
16
|
+
f" WHERE emb IS NOT NULL"
|
|
17
|
+
f" ORDER BY distance LIMIT {int(limit):d}"
|
|
18
|
+
)
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
def no_rows_message(glob, matched, root):
|
|
2
|
+
if not matched:
|
|
3
|
+
return f"no files matched {glob!r} (searched from {root})"
|
|
4
|
+
return (
|
|
5
|
+
f"{glob!r} matched {matched} file(s), but none had text content to"
|
|
6
|
+
f" embed -- unreadable or not valid UTF-8 (searched from {root})"
|
|
7
|
+
)
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
PATH_PREFIXES = ("./", "../", "/", "~/")
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
def normalize_glob(glob):
|
|
5
|
+
# The core only rescues path-shaped missing tables (./, ../, /, ~/); a
|
|
6
|
+
# bare relative glob like '**/*.md' would error with a "did you mean
|
|
7
|
+
# './...'" hint. Here GLOB is unambiguously a corpus glob, so spare the
|
|
8
|
+
# user the round trip.
|
|
9
|
+
if glob.startswith(PATH_PREFIXES):
|
|
10
|
+
return glob
|
|
11
|
+
return f"./{glob}"
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
import os
|
|
3
|
+
|
|
4
|
+
from .no_rows_message import no_rows_message
|
|
5
|
+
from .output import format_rows
|
|
6
|
+
from .search_rows import search_rows
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class NothingToRank(Exception):
|
|
10
|
+
"""The search produced no rows, and why -- so the CLI can say which."""
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def run_search(glob, query, limit, model=None):
|
|
14
|
+
rows, matched = asyncio.run(search_rows(glob, query, limit, model))
|
|
15
|
+
if not rows:
|
|
16
|
+
raise NothingToRank(no_rows_message(glob, matched, os.getcwd()))
|
|
17
|
+
return format_rows(rows)
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
from importlib import resources
|
|
2
|
+
|
|
3
|
+
import dirsql
|
|
4
|
+
|
|
5
|
+
from .build_search_sql import build_search_sql
|
|
6
|
+
from .count_corpus_sql import count_corpus_sql
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def config_fragment():
|
|
10
|
+
return str(resources.files("dirsql_plugin_embeddings").joinpath("dirsql.toml"))
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
async def search_rows(glob, query, limit, model):
|
|
14
|
+
db = dirsql.DirSQL(config=config_fragment())
|
|
15
|
+
rows = await db.query(build_search_sql(glob, query, limit, model))
|
|
16
|
+
if rows:
|
|
17
|
+
return rows, None
|
|
18
|
+
# Only now, on the error path, is the second scan worth its cost: it is
|
|
19
|
+
# what tells "no files matched" apart from "matched, none embeddable".
|
|
20
|
+
# One row, always: `SELECT COUNT(*)` cannot return anything else, and
|
|
21
|
+
# unpacking says so rather than trusting an index.
|
|
22
|
+
(counted,) = await db.query(count_corpus_sql(glob))
|
|
23
|
+
return rows, counted["n"]
|
{dirsql_plugin_embeddings-0.1.18 → dirsql_plugin_embeddings-0.1.19}/testing-conventions.toml
RENAMED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
# exemptions, and deliberately no `[e2e] extra_scope` -- this package compiles
|
|
4
4
|
# none of packages/rust/src, so a core change never stales its attestation.
|
|
5
5
|
[python]
|
|
6
|
-
one_function_per_file = { max_lines =
|
|
6
|
+
one_function_per_file = { max_lines = 10 }
|
|
7
7
|
|
|
8
8
|
[python.coverage]
|
|
9
9
|
fail_under = 100
|
|
@@ -1,45 +0,0 @@
|
|
|
1
|
-
import asyncio
|
|
2
|
-
import os
|
|
3
|
-
from importlib import resources
|
|
4
|
-
|
|
5
|
-
import dirsql
|
|
6
|
-
|
|
7
|
-
from .output import format_rows
|
|
8
|
-
from .sql import build_search_sql, count_corpus_sql
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
class NothingToRank(Exception):
|
|
12
|
-
"""The search produced no rows, and why -- so the CLI can say which."""
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
def config_fragment():
|
|
16
|
-
return str(resources.files("dirsql_plugin_embeddings").joinpath("dirsql.toml"))
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
def no_rows_message(glob, matched, root):
|
|
20
|
-
if not matched:
|
|
21
|
-
return f"no files matched {glob!r} (searched from {root})"
|
|
22
|
-
return (
|
|
23
|
-
f"{glob!r} matched {matched} file(s), but none had text content to"
|
|
24
|
-
f" embed -- unreadable or not valid UTF-8 (searched from {root})"
|
|
25
|
-
)
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
async def _search(glob, query, limit, model):
|
|
29
|
-
db = dirsql.DirSQL(config=config_fragment())
|
|
30
|
-
rows = await db.query(build_search_sql(glob, query, limit, model))
|
|
31
|
-
if rows:
|
|
32
|
-
return rows, None
|
|
33
|
-
# Only now, on the error path, is the second scan worth its cost: it is
|
|
34
|
-
# what tells "no files matched" apart from "matched, none embeddable".
|
|
35
|
-
# One row, always: `SELECT COUNT(*)` cannot return anything else, and
|
|
36
|
-
# unpacking says so rather than trusting an index.
|
|
37
|
-
(counted,) = await db.query(count_corpus_sql(glob))
|
|
38
|
-
return rows, counted["n"]
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
def run_search(glob, query, limit, model=None):
|
|
42
|
-
rows, matched = asyncio.run(_search(glob, query, limit, model))
|
|
43
|
-
if not rows:
|
|
44
|
-
raise NothingToRank(no_rows_message(glob, matched, os.getcwd()))
|
|
45
|
-
return format_rows(rows)
|
|
@@ -1,41 +0,0 @@
|
|
|
1
|
-
PATH_PREFIXES = ("./", "../", "/", "~/")
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
def quote(text):
|
|
5
|
-
escaped = text.replace("'", "''")
|
|
6
|
-
return f"'{escaped}'"
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
def normalize_glob(glob):
|
|
10
|
-
# The core only rescues path-shaped missing tables (./, ../, /, ~/); a
|
|
11
|
-
# bare relative glob like '**/*.md' would error with a "did you mean
|
|
12
|
-
# './...'" hint. Here GLOB is unambiguously a corpus glob, so spare the
|
|
13
|
-
# user the round trip.
|
|
14
|
-
if glob.startswith(PATH_PREFIXES):
|
|
15
|
-
return glob
|
|
16
|
-
return f"./{glob}"
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
def embed_call(argument, model):
|
|
20
|
-
if model is None:
|
|
21
|
-
return f"embed({argument})"
|
|
22
|
-
return f"embed({argument}, {quote(model)})"
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
def build_search_sql(glob, query, limit, model=None):
|
|
26
|
-
outer = embed_call(quote(query), model)
|
|
27
|
-
inner = embed_call("content", model)
|
|
28
|
-
# A matched file that is unreadable, not valid UTF-8, or deleted mid-scan
|
|
29
|
-
# has NULL content, so its embedding is NULL too. Dropping those rows is
|
|
30
|
-
# what keeps the ranking honest: a NULL distance sorts FIRST ascending in
|
|
31
|
-
# SQLite, so unrankable files would otherwise take the top-k slots.
|
|
32
|
-
return (
|
|
33
|
-
f"SELECT path, vec_distance_cosine(emb, {outer}) AS distance"
|
|
34
|
-
f" FROM (SELECT path, {inner} AS emb FROM {quote(normalize_glob(glob))})"
|
|
35
|
-
f" WHERE emb IS NOT NULL"
|
|
36
|
-
f" ORDER BY distance LIMIT {int(limit):d}"
|
|
37
|
-
)
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
def count_corpus_sql(glob):
|
|
41
|
-
return f"SELECT COUNT(*) AS n FROM {quote(normalize_glob(glob))}"
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{dirsql_plugin_embeddings-0.1.18 → dirsql_plugin_embeddings-0.1.19}/tests/integration/__init__.py
RENAMED
|
File without changes
|