dirsql-plugin-embeddings 0.1.14__tar.gz → 0.1.16__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.
Files changed (37) hide show
  1. {dirsql_plugin_embeddings-0.1.14 → dirsql_plugin_embeddings-0.1.16}/PKG-INFO +6 -1
  2. {dirsql_plugin_embeddings-0.1.14 → dirsql_plugin_embeddings-0.1.16}/README.md +5 -0
  3. dirsql_plugin_embeddings-0.1.16/changelog.d/2026-08-12-empty-search-reports-why.md +3 -0
  4. dirsql_plugin_embeddings-0.1.16/changelog.d/2026-08-12-null-embedding-guard.md +3 -0
  5. dirsql_plugin_embeddings-0.1.16/e2e-attestations/claude-816-empty-corpus-error.json +7 -0
  6. dirsql_plugin_embeddings-0.1.16/e2e-attestations/claude-817-null-embedding-guard.json +7 -0
  7. {dirsql_plugin_embeddings-0.1.14 → dirsql_plugin_embeddings-0.1.16}/src/dirsql_plugin_embeddings/cli/search.py +7 -2
  8. dirsql_plugin_embeddings-0.1.16/src/dirsql_plugin_embeddings/search/run.py +45 -0
  9. {dirsql_plugin_embeddings-0.1.14 → dirsql_plugin_embeddings-0.1.16}/src/dirsql_plugin_embeddings/search/sql.py +9 -0
  10. dirsql_plugin_embeddings-0.1.14/src/dirsql_plugin_embeddings/search/run.py +0 -20
  11. {dirsql_plugin_embeddings-0.1.14 → dirsql_plugin_embeddings-0.1.16}/.gitignore +0 -0
  12. {dirsql_plugin_embeddings-0.1.14 → dirsql_plugin_embeddings-0.1.16}/changelog.d/2026-08-10-delete-old-surface.md +0 -0
  13. {dirsql_plugin_embeddings-0.1.14 → dirsql_plugin_embeddings-0.1.16}/changelog.d/2026-08-10-embed-worker.md +0 -0
  14. {dirsql_plugin_embeddings-0.1.14 → dirsql_plugin_embeddings-0.1.16}/changelog.d/2026-08-11-one-liner-cli.md +0 -0
  15. {dirsql_plugin_embeddings-0.1.14 → dirsql_plugin_embeddings-0.1.16}/changelog.d/2026-08-12-no-per-call-progress-bar.md +0 -0
  16. {dirsql_plugin_embeddings-0.1.14 → dirsql_plugin_embeddings-0.1.16}/e2e-attestations/claude-804-embed-worker.json +0 -0
  17. {dirsql_plugin_embeddings-0.1.14 → dirsql_plugin_embeddings-0.1.16}/e2e-attestations/claude-805-one-liner.json +0 -0
  18. {dirsql_plugin_embeddings-0.1.14 → dirsql_plugin_embeddings-0.1.16}/e2e-attestations/claude-814-suppress-per-call-progress.json +0 -0
  19. {dirsql_plugin_embeddings-0.1.14 → dirsql_plugin_embeddings-0.1.16}/migrations.d/2026-08-10-delete-old-surface.md +0 -0
  20. {dirsql_plugin_embeddings-0.1.14 → dirsql_plugin_embeddings-0.1.16}/pyproject.toml +0 -0
  21. {dirsql_plugin_embeddings-0.1.14 → dirsql_plugin_embeddings-0.1.16}/src/dirsql_plugin_embeddings/__init__.py +0 -0
  22. {dirsql_plugin_embeddings-0.1.14 → dirsql_plugin_embeddings-0.1.16}/src/dirsql_plugin_embeddings/cli/__init__.py +0 -0
  23. {dirsql_plugin_embeddings-0.1.14 → dirsql_plugin_embeddings-0.1.16}/src/dirsql_plugin_embeddings/cli/main.py +0 -0
  24. {dirsql_plugin_embeddings-0.1.14 → dirsql_plugin_embeddings-0.1.16}/src/dirsql_plugin_embeddings/cli/worker.py +0 -0
  25. {dirsql_plugin_embeddings-0.1.14 → dirsql_plugin_embeddings-0.1.16}/src/dirsql_plugin_embeddings/dirsql.toml +0 -0
  26. {dirsql_plugin_embeddings-0.1.14 → dirsql_plugin_embeddings-0.1.16}/src/dirsql_plugin_embeddings/embedding/__init__.py +0 -0
  27. {dirsql_plugin_embeddings-0.1.14 → dirsql_plugin_embeddings-0.1.16}/src/dirsql_plugin_embeddings/embedding/cache.py +0 -0
  28. {dirsql_plugin_embeddings-0.1.14 → dirsql_plugin_embeddings-0.1.16}/src/dirsql_plugin_embeddings/embedding/model.py +0 -0
  29. {dirsql_plugin_embeddings-0.1.14 → dirsql_plugin_embeddings-0.1.16}/src/dirsql_plugin_embeddings/embedding/progress.py +0 -0
  30. {dirsql_plugin_embeddings-0.1.14 → dirsql_plugin_embeddings-0.1.16}/src/dirsql_plugin_embeddings/embedding/values.py +0 -0
  31. {dirsql_plugin_embeddings-0.1.14 → dirsql_plugin_embeddings-0.1.16}/src/dirsql_plugin_embeddings/embedding/worker.py +0 -0
  32. {dirsql_plugin_embeddings-0.1.14 → dirsql_plugin_embeddings-0.1.16}/src/dirsql_plugin_embeddings/search/__init__.py +0 -0
  33. {dirsql_plugin_embeddings-0.1.14 → dirsql_plugin_embeddings-0.1.16}/src/dirsql_plugin_embeddings/search/output.py +0 -0
  34. {dirsql_plugin_embeddings-0.1.14 → dirsql_plugin_embeddings-0.1.16}/testing-conventions.toml +0 -0
  35. {dirsql_plugin_embeddings-0.1.14 → dirsql_plugin_embeddings-0.1.16}/tests/conftest.py +0 -0
  36. {dirsql_plugin_embeddings-0.1.14 → dirsql_plugin_embeddings-0.1.16}/tests/e2e/__init__.py +0 -0
  37. {dirsql_plugin_embeddings-0.1.14 → dirsql_plugin_embeddings-0.1.16}/tests/integration/__init__.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: dirsql-plugin-embeddings
3
- Version: 0.1.14
3
+ Version: 0.1.16
4
4
  Summary: First-party dirsql plugin: semantic search over files.
5
5
  Requires-Python: >=3.10
6
6
  Requires-Dist: cachetta>=0.7
@@ -26,10 +26,15 @@ uvx --with dirsql-plugin-embeddings dirsql "
26
26
  SELECT path
27
27
  FROM (SELECT path, embed(content ->> 'abstract') AS emb
28
28
  FROM './arxiv-firehose/data/**/metadata.json')
29
+ WHERE emb IS NOT NULL
29
30
  ORDER BY vec_distance_cosine(emb, embed('local private models'))
30
31
  LIMIT 10"
31
32
  ```
32
33
 
34
+ `WHERE emb IS NOT NULL` is not optional bookkeeping: a file that is unreadable
35
+ or not valid UTF-8 has NULL content, so its distance is NULL, and SQLite sorts
36
+ NULLs *first* ascending — without the guard those files take the top slots.
37
+
33
38
  For the common case — one glob, one question, top-k paths — the package is
34
39
  also its own command, generating and running exactly that SQL:
35
40
 
@@ -13,10 +13,15 @@ uvx --with dirsql-plugin-embeddings dirsql "
13
13
  SELECT path
14
14
  FROM (SELECT path, embed(content ->> 'abstract') AS emb
15
15
  FROM './arxiv-firehose/data/**/metadata.json')
16
+ WHERE emb IS NOT NULL
16
17
  ORDER BY vec_distance_cosine(emb, embed('local private models'))
17
18
  LIMIT 10"
18
19
  ```
19
20
 
21
+ `WHERE emb IS NOT NULL` is not optional bookkeeping: a file that is unreadable
22
+ or not valid UTF-8 has NULL content, so its distance is NULL, and SQLite sorts
23
+ NULLs *first* ascending — without the guard those files take the top slots.
24
+
20
25
  For the common case — one glob, one question, top-k paths — the package is
21
26
  also its own command, generating and running exactly that SQL:
22
27
 
@@ -0,0 +1,3 @@
1
+ **Fixed**
2
+
3
+ - **A search that ranks nothing now says why and exits nonzero, instead of printing nothing and exiting 0.** Silent empty output was indistinguishable from "searched and found nothing relevant" — the reported case was running `dirsql-plugin-embeddings 'docs/**/*.md' <query>` from inside `docs/`, where the glob matches nothing. The message names the glob and the directory it searched from (`no files matched 'docs/**/*.md' (searched from /current/dir)`), and distinguishes that from a corpus whose files all matched but had no embeddable text, so it never claims the wrong reason. (#816)
@@ -0,0 +1,3 @@
1
+ **Fixed**
2
+
3
+ - **Files that cannot be embedded no longer take the top of the ranking (and no longer break the search).** A matched file that is unreadable, not valid UTF-8, or deleted mid-scan has NULL `content`, so `embed(content)` is NULL — and SQLite sorts NULLs *first* ascending, so those files occupied the top-k slots before anything else went wrong. The generated search SQL now carries `WHERE emb IS NOT NULL`, which fixes the ranking and the failure it led to (a `TypeError` formatting a NULL distance, or, depending on the sqlite-vec build, `Error reading 1st vector: ... found NULL` from SQLite itself). The documented hand-written idiom carries the same guard. (#817)
@@ -0,0 +1,7 @@
1
+ {
2
+ "command": "uv run --with-editable ../../packages/python python -m pytest tests/e2e/ -x -q",
3
+ "ran_at": 1786553959,
4
+ "exit_code": 0,
5
+ "commit": "c349f1ed7b83b0817d68019550264edb0a255ddd",
6
+ "branch": "claude/816-empty-corpus-error"
7
+ }
@@ -0,0 +1,7 @@
1
+ {
2
+ "command": "uv run --with-editable ../../packages/python python -m pytest tests/e2e/ -x -q",
3
+ "ran_at": 1786558054,
4
+ "exit_code": 0,
5
+ "commit": "f1af2bf7bf4c21dd62350b48b4d441356047ef59",
6
+ "branch": "claude/817-null-embedding-guard"
7
+ }
@@ -1,6 +1,6 @@
1
1
  import click
2
2
 
3
- from ..search.run import run_search
3
+ from ..search.run import NothingToRank, run_search
4
4
 
5
5
 
6
6
  @click.command(hidden=True)
@@ -22,5 +22,10 @@ from ..search.run import run_search
22
22
  )
23
23
  def search(glob, query, limit, model):
24
24
  """Rank files matching GLOB by semantic similarity to QUERY."""
25
- for line in run_search(glob, query, limit, model):
25
+ try:
26
+ lines = run_search(glob, query, limit, model)
27
+ except NothingToRank as error:
28
+ click.echo(f"dirsql-plugin-embeddings: {error}", err=True)
29
+ raise SystemExit(1) from None
30
+ for line in lines:
26
31
  click.echo(line)
@@ -0,0 +1,45 @@
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)
@@ -25,8 +25,17 @@ def embed_call(argument, model):
25
25
  def build_search_sql(glob, query, limit, model=None):
26
26
  outer = embed_call(quote(query), model)
27
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.
28
32
  return (
29
33
  f"SELECT path, vec_distance_cosine(emb, {outer}) AS distance"
30
34
  f" FROM (SELECT path, {inner} AS emb FROM {quote(normalize_glob(glob))})"
35
+ f" WHERE emb IS NOT NULL"
31
36
  f" ORDER BY distance LIMIT {int(limit):d}"
32
37
  )
38
+
39
+
40
+ def count_corpus_sql(glob):
41
+ return f"SELECT COUNT(*) AS n FROM {quote(normalize_glob(glob))}"
@@ -1,20 +0,0 @@
1
- import asyncio
2
- from importlib import resources
3
-
4
- import dirsql
5
-
6
- from .output import format_rows
7
- from .sql import build_search_sql
8
-
9
-
10
- def config_fragment():
11
- return str(resources.files("dirsql_plugin_embeddings").joinpath("dirsql.toml"))
12
-
13
-
14
- async def _query(sql):
15
- return await dirsql.DirSQL(config=config_fragment()).query(sql)
16
-
17
-
18
- def run_search(glob, query, limit, model=None):
19
- rows = asyncio.run(_query(build_search_sql(glob, query, limit, model)))
20
- return format_rows(rows)