jekyll-client-search 0.3.5 → 0.5.0
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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +107 -1
- data/README.developer.md +30 -3
- data/README.md +209 -32
- data/assets/client-search-dropdown.js +57 -27
- data/assets/query-embedders/ollama-api.js +1 -1
- data/lib/jekyll/client_search/configuration.rb +11 -10
- data/lib/jekyll/client_search/configuration_accessors.rb +13 -0
- data/lib/jekyll/client_search/dropdown_tag.rb +7 -3
- data/lib/jekyll/client_search/embedding_configuration.rb +51 -15
- data/lib/jekyll/client_search/generator.rb +66 -19
- data/lib/jekyll/client_search/index_cache.rb +3 -2
- data/lib/jekyll/client_search/ollama_embedding_adapter.rb +47 -1
- data/lib/jekyll/client_search/query_embedder_configuration.rb +3 -0
- data/lib/jekyll/client_search/related_analyzer.rb +4 -1
- data/lib/jekyll/client_search/related_configuration.rb +15 -0
- data/lib/jekyll/client_search/semantic_configuration.rb +46 -0
- data/lib/jekyll/client_search/version.rb +1 -1
- data/lib/jekyll/client_search.rb +1 -0
- metadata +2 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 3e0accecae865157aa09bda9bf9df5060bcad7a45eeb55555137f963fe1cca93
|
|
4
|
+
data.tar.gz: 8bb447d56a507fb5c7a632ae38433606317bccfda3e2cb01388c129617a49b8a
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: bcb6ee7ce75d7b75facd5c69db2f0c8479f4e70c0d5e6830b92f65cec936235596f3eb8e97db6621e9b6ec3559868b1f34d3b3ea1b8f5610464b880b88261104
|
|
7
|
+
data.tar.gz: 1c35127143ea928d0156ba202c0c4e6a485729de69afd69e3203f45d5625e0f00848a17e2f4cfe50f9085b8c61e76d16f81b36e05ffa8f27352cb655a42f5eaf
|
data/CHANGELOG.md
CHANGED
|
@@ -1,6 +1,112 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
-
##
|
|
3
|
+
## 0.5.0 — 2026-11-08
|
|
4
|
+
|
|
5
|
+
### Added
|
|
6
|
+
|
|
7
|
+
- `rake docs:demo` — captures the checked-in illustrative overview as
|
|
8
|
+
animated AVIF + WebP + static PNG (headless Chrome + ffmpeg + img2webp;
|
|
9
|
+
build-host tools), embedded in README via `<picture>`. Examples are
|
|
10
|
+
schematic, not generated from a live Jekyll build.
|
|
11
|
+
|
|
12
|
+
- Dev-tooling bumps: `markdownlint-cli2` 0.24.0, `eslint` ~10.14.0,
|
|
13
|
+
`globals` 17.14.0, `rubocop` 1.92.2, `ollama-ruby` 1.26.0,
|
|
14
|
+
`excon` 1.11.0, `sass-embedded` 1.108.1, `addressable` 2.9.2,
|
|
15
|
+
`csv` 3.3.7, `io-console` 0.9.5, `parser` 3.3.13.0,
|
|
16
|
+
`irb` 1.19.0, `tins` 1.62.0; `BUNDLED WITH` 4.0.9 → 4.0.24.
|
|
17
|
+
|
|
18
|
+
### Changed
|
|
19
|
+
|
|
20
|
+
- The live dropdown now follows the ARIA combobox/listbox pattern:
|
|
21
|
+
focused input with `aria-activedescendant`, uniquely identified plain
|
|
22
|
+
text options, and no nested links inside listbox options.
|
|
23
|
+
- CI Node.js matrix is now `24` / `26` — matching the `engines:
|
|
24
|
+
>=24` floor. Node 22 dropped (maintenance-only).
|
|
25
|
+
- CI action pins bumped: `ruby/setup-ruby` v1.338.0,
|
|
26
|
+
`sonarqube-scan-action` v8.4.1, `semgrep` image 1.183.0.
|
|
27
|
+
- `spec/search_index_page_spec.rb` expanded — now covers the default
|
|
28
|
+
root-level output path (`search-index.json`), an empty document
|
|
29
|
+
list, and field/order preservation.
|
|
30
|
+
- `test/meta.test.js` cross-engine check split into one named `test()`
|
|
31
|
+
per exact-match query — a single failure no longer hides the
|
|
32
|
+
remaining queries.
|
|
33
|
+
|
|
34
|
+
### Fixed
|
|
35
|
+
|
|
36
|
+
- Benchmark scripts access environment via `ENV.fetch("KEY", default)`
|
|
37
|
+
instead of `ENV["KEY"] || default` (SonarQube: environment variable
|
|
38
|
+
access without a default value).
|
|
39
|
+
- SonarQube coverage denominator no longer counts `benchmarks/**` —
|
|
40
|
+
the ~850 lines of manual benchmark tooling are not instrumented by
|
|
41
|
+
SimpleCov, which diluted the reported project coverage to ~50%.
|
|
42
|
+
|
|
43
|
+
### Security
|
|
44
|
+
|
|
45
|
+
- `overrides` pins `re2js` 2.8.8 — markdownlint 0.42.0 (via
|
|
46
|
+
markdownlint-cli2 0.24.0) pins the vulnerable 2.8.6 (ReDoS in
|
|
47
|
+
lookbehind matching, GHSA-gg6h-894w-4m9c).
|
|
48
|
+
- `nokogiri` 1.19.7 (dev-only, via `ollama-ruby`) — fixes
|
|
49
|
+
GHSA-3w22-6jfg-527w (use-after-free), GHSA-4p39-36wj-23h4
|
|
50
|
+
(mutation XSS), GHSA-v995-44v6-5fqm (JRuby SSRF).
|
|
51
|
+
|
|
52
|
+
## 0.4.0 — 2026-10-24
|
|
53
|
+
|
|
54
|
+
- Dev-tooling bump: `jsdom` 30.1.3 (patch).
|
|
55
|
+
|
|
56
|
+
### Added
|
|
57
|
+
|
|
58
|
+
- `benchmarks/embedding_model_benchmark.rb`,
|
|
59
|
+
`benchmarks/related_model_benchmark.rb`, and
|
|
60
|
+
`benchmarks/related_quality_judge.rb` — measure Ollama embedding
|
|
61
|
+
latency per model on a real site, compare `related` output across
|
|
62
|
+
models, and score judged relevance with a blind LLM judge
|
|
63
|
+
(10 local embedding models benchmarked on a 152-document site).
|
|
64
|
+
- `embeddinggemma-2` document/query prefixes (same prompt format as
|
|
65
|
+
`embeddinggemma`), `voyage-4-nano` prefixes (open-weight Voyage
|
|
66
|
+
model via community GGUF), and `qwen3-embedding` prefixes (official
|
|
67
|
+
instruct-format query prefix; empty document prefix).
|
|
68
|
+
- README model-selection guidance: measured per-document latency table,
|
|
69
|
+
LLM-judged quality ratings, quality-per-cost value index, amortized
|
|
70
|
+
yearly cost, `qwen3-embedding` build-time-only notes, and the
|
|
71
|
+
`localhost` → `127.0.0.1` loopback gotcha.
|
|
72
|
+
- Split index/related embedding models: `semantic.index_model` selects
|
|
73
|
+
the Ollama model whose vectors go into `search-index.json` (must match
|
|
74
|
+
the browser query embedder); `related.index_model` selects the model
|
|
75
|
+
used for related-article analysis. Both default to `embedding.model`;
|
|
76
|
+
equal models share one embedding pass, divergent models embed each
|
|
77
|
+
document twice into separate cached vector fields.
|
|
78
|
+
`semantic.document_prefix` / `semantic.query_prefix` override the
|
|
79
|
+
index model's prefixes. Related-model vectors are cached separately
|
|
80
|
+
in `.jekyll-client-search-related-cache.json` and never written to
|
|
81
|
+
the index.
|
|
82
|
+
- `embeddinggemma-2` (`:270m`, `:570m`, untagged) maps to
|
|
83
|
+
`onnx-community/embeddinggemma-2-ONNX` for the transformers.js query
|
|
84
|
+
embedder. `test/onnx-parity.test.js` is an opt-in check
|
|
85
|
+
(`ONNX_PARITY=1`, requires `@huggingface/transformers` and a running
|
|
86
|
+
Ollama) that compares ONNX and Ollama vectors and rankings;
|
|
87
|
+
`embeddinggemma:300m` remains the verified mapping.
|
|
88
|
+
|
|
89
|
+
### Changed
|
|
90
|
+
|
|
91
|
+
- Default `embedding.model` is now `embeddinggemma-2:570m` (was
|
|
92
|
+
`embeddinggemma:300m`) — the judged-quality leader among license-free
|
|
93
|
+
local models (111 vs 96) at ~2.5× the per-document latency; its text
|
|
94
|
+
vectors are byte-identical to `embeddinggemma-2:270m`. The
|
|
95
|
+
transformers.js query embedder maps it to
|
|
96
|
+
`onnx-community/embeddinggemma-2-ONNX`; sites that prefer the verified
|
|
97
|
+
browser mapping can set `semantic.index_model: embeddinggemma:300m`
|
|
98
|
+
while related analysis stays on the default model.
|
|
99
|
+
- CI: Semgrep moved from an inline `pip3 install semgrep==1.176.0` pin to hashed `.github/requirements-ci.txt` (compiled from `requirements-ci.in`, Python 3.12), installed with `--require-hashes`. Bumped Semgrep to 1.180.0; the version is now Dependabot-visible via a new `pip` ecosystem entry for `/.github`.
|
|
100
|
+
|
|
101
|
+
### Fixed
|
|
102
|
+
|
|
103
|
+
- A `localhost` `embedding.base_url` (the default) is resolved once to
|
|
104
|
+
the loopback literal that accepts a TCP connection — `127.0.0.1` or
|
|
105
|
+
`[::1]` — because `ollama-ruby`/Excon does not fall back across
|
|
106
|
+
address families and Ollama commonly listens on IPv4 loopback only.
|
|
107
|
+
Embedding calls that died with `Excon::Error::Socket "Broken pipe"`
|
|
108
|
+
when `localhost` resolved to `::1` first now connect on both
|
|
109
|
+
IPv4-only and IPv6-only servers.
|
|
4
110
|
|
|
5
111
|
## 0.3.5 — 2026-10-06
|
|
6
112
|
|
data/README.developer.md
CHANGED
|
@@ -20,12 +20,27 @@ Both test suites pass with only the committed Wikipedia fixtures and the
|
|
|
20
20
|
committed baseline JSON artifacts. No network access or external services
|
|
21
21
|
are required for the default test run.
|
|
22
22
|
|
|
23
|
+
Specs that need a Jekyll site use `jekyll-test-harness` (`jekyll_build` +
|
|
24
|
+
`jekyll_files` DSL, installed in `spec/spec_helper.rb`) instead of hand-rolled
|
|
25
|
+
`Dir.mktmpdir`/`Jekyll::Site.new` scaffolding — see
|
|
26
|
+
`spec/support/site_helpers.rb` and `spec/generator_spec.rb` for the pattern.
|
|
27
|
+
Note that the gem's `Jekyll::ClientSearch::Generator` subclasses
|
|
28
|
+
`Jekyll::Generator`, so it runs automatically inside `jekyll_build`'s
|
|
29
|
+
`site.process`; specs should assert on the built site directly and stub
|
|
30
|
+
`build_embedding_adapter` (see `stub_embedding_adapter`) before building
|
|
31
|
+
rather than calling `generate` manually. Specs that rebuild the same source
|
|
32
|
+
directory multiple times (`spec/embedding_cache_spec.rb`,
|
|
33
|
+
`spec/system_spec.rb`, Ollama integration) still use their own helpers,
|
|
34
|
+
since the harness builds each site in a fresh temporary directory.
|
|
35
|
+
|
|
23
36
|
## Quality checks and git hooks
|
|
24
37
|
|
|
25
38
|
The project uses a focused quality stack: RuboCop (style), bundler-audit
|
|
26
39
|
(security), RSpec (Ruby tests), `npm test` (JavaScript tests), and Semgrep
|
|
27
40
|
(security scanning). This covers the actionable ground without the
|
|
28
41
|
maintenance burden of additional code-smell tools on a small focused gem.
|
|
42
|
+
`npm test` also runs ESLint, Markdownlint, and jsdom tests for the browser
|
|
43
|
+
runtime, including the dropdown combobox/listbox keyboard and ARIA contract.
|
|
29
44
|
|
|
30
45
|
External automated code review services:
|
|
31
46
|
|
|
@@ -112,7 +127,7 @@ CI. Skip either with `git commit --no-verify` or `git push --no-verify`.
|
|
|
112
127
|
### CI checks
|
|
113
128
|
|
|
114
129
|
The [CI workflow](.github/workflows/ci.yml) runs on every push and pull
|
|
115
|
-
request across Ruby 3.3/3.4 and Node
|
|
130
|
+
request across Ruby 3.3/3.4 and Node 24/26:
|
|
116
131
|
|
|
117
132
|
- `bundle exec rake ci` (rspec, rubocop, syntax checks, npm test, gem build)
|
|
118
133
|
- `bundle exec bundle-audit check --update` (Ruby dependency security)
|
|
@@ -237,9 +252,10 @@ To run the full integration tests that call a real Ollama server:
|
|
|
237
252
|
|
|
238
253
|
1. Install [Ollama](https://ollama.ai/)
|
|
239
254
|
|
|
240
|
-
2. Pull the required
|
|
255
|
+
2. Pull the required models:
|
|
241
256
|
|
|
242
257
|
```bash
|
|
258
|
+
ollama pull embeddinggemma-2:570m
|
|
243
259
|
ollama pull embeddinggemma:300m
|
|
244
260
|
```
|
|
245
261
|
|
|
@@ -250,6 +266,16 @@ To run the full integration tests that call a real Ollama server:
|
|
|
250
266
|
OLLAMA_INTEGRATION=1 bundle exec rspec spec/llm_injection_spec.rb
|
|
251
267
|
```
|
|
252
268
|
|
|
269
|
+
To verify cross-runtime parity between the transformers.js ONNX twin and
|
|
270
|
+
the Ollama build model (opt-in — needs the model downloaded once):
|
|
271
|
+
|
|
272
|
+
```bash
|
|
273
|
+
npm i -D @huggingface/transformers@3.8.1
|
|
274
|
+
ONNX_PARITY=1 node --test test/onnx-parity.test.js
|
|
275
|
+
```
|
|
276
|
+
|
|
277
|
+
Set `OLLAMA_MODEL`, `ONNX_MODEL`, or `ONNX_DTYPE` to check other mappings.
|
|
278
|
+
|
|
253
279
|
These tests:
|
|
254
280
|
|
|
255
281
|
- Build the full 80-post index with real Ollama embeddings
|
|
@@ -450,7 +476,8 @@ This check also runs in the release workflow — the publish will fail if
|
|
|
450
476
|
```
|
|
451
477
|
|
|
452
478
|
The Ollama integration tests require a running [Ollama](https://ollama.ai/)
|
|
453
|
-
server with the `embeddinggemma:
|
|
479
|
+
server with the `embeddinggemma-2:570m` and `embeddinggemma:300m` models
|
|
480
|
+
pulled (`ollama pull embeddinggemma-2:570m` and `ollama pull
|
|
454
481
|
embeddinggemma:300m`). Do not publish a release until the Ollama
|
|
455
482
|
integration tests pass locally — they are skipped by default and will
|
|
456
483
|
not block CI, so they must be run manually before each release.
|
data/README.md
CHANGED
|
@@ -15,7 +15,6 @@
|
|
|
15
15
|
[](https://semgrep.dev)
|
|
16
16
|
[](https://www.codefactor.io/repository/github/gundestrup/jekyll-client-search)
|
|
17
17
|
[](https://sonarcloud.io/project/overview?id=gundestrup_jekyll-client-search)
|
|
18
|
-
[](https://codecov.io/gh/gundestrup/jekyll-client-search)
|
|
19
18
|
|
|
20
19
|
A Jekyll plugin that generates a JSON document index for client-side search.
|
|
21
20
|
Supports lexical search ([MiniSearch](https://lucaong.github.io/minisearch/),
|
|
@@ -24,6 +23,19 @@ via pre-computed embeddings from a local [Ollama](https://ollama.ai) server.
|
|
|
24
23
|
Related articles can be generated at build time from shared tags, categories,
|
|
25
24
|
and vector similarity.
|
|
26
25
|
|
|
26
|
+
<!-- markdownlint-disable MD033 -->
|
|
27
|
+
<picture>
|
|
28
|
+
<source
|
|
29
|
+
srcset="docs/assets/jekyll-client-search-demo.avif"
|
|
30
|
+
type="image/avif">
|
|
31
|
+
<source
|
|
32
|
+
srcset="docs/assets/jekyll-client-search-demo.webp"
|
|
33
|
+
type="image/webp">
|
|
34
|
+
<img src="docs/assets/jekyll-client-search-demo.png"
|
|
35
|
+
alt="jekyll-client-search demo">
|
|
36
|
+
</picture>
|
|
37
|
+
<!-- markdownlint-enable MD033 -->
|
|
38
|
+
|
|
27
39
|
## Table of contents
|
|
28
40
|
|
|
29
41
|
- [Lightning start](#lightning-start)
|
|
@@ -269,7 +281,8 @@ Tag modes:
|
|
|
269
281
|
`/search/`) with `?q=...` for full results on the search page
|
|
270
282
|
- Arrow Up/Down → highlight items, Enter on highlighted → navigate
|
|
271
283
|
- Click an item → navigate to that document's URL
|
|
272
|
-
- Escape
|
|
284
|
+
- Escape → close the popup and keep focus in the combobox; click outside
|
|
285
|
+
→ close the dropdown
|
|
273
286
|
- Index is lazy-loaded on first keystroke (no cost on pages where the
|
|
274
287
|
visitor doesn't search)
|
|
275
288
|
|
|
@@ -290,24 +303,32 @@ and the search index includes that field (e.g. from jekyll-documents),
|
|
|
290
303
|
an `<img>` icon is rendered before each result title. Set `icon_field:
|
|
291
304
|
null` to disable.
|
|
292
305
|
|
|
293
|
-
**CSS styling:** The dropdown emits no framework classes.
|
|
306
|
+
**CSS styling:** The dropdown emits no framework classes. Its input uses
|
|
307
|
+
the ARIA combobox pattern; the popup is a listbox whose options are plain
|
|
308
|
+
text containers (not nested links or controls). Style with:
|
|
294
309
|
|
|
295
310
|
```css
|
|
296
311
|
.client-search-dropdown { position: relative; }
|
|
297
|
-
.client-search-dropdown
|
|
312
|
+
.client-search-dropdown [role="listbox"] {
|
|
298
313
|
position: absolute; top: 100%; right: 0;
|
|
299
314
|
min-width: 300px; z-index: 30;
|
|
300
315
|
background: white; box-shadow: 0 2px 8px rgba(0,0,0,0.1);
|
|
316
|
+
margin: 0; padding: 0;
|
|
317
|
+
}
|
|
318
|
+
.client-search-dropdown [role="listbox"] ul[role="presentation"] {
|
|
301
319
|
list-style: none; margin: 0; padding: 0;
|
|
302
320
|
}
|
|
303
|
-
.client-search-dropdown
|
|
304
|
-
.
|
|
305
|
-
|
|
306
|
-
.client-search-dropdown
|
|
321
|
+
.client-search-dropdown [role="option"] {
|
|
322
|
+
cursor: pointer; padding: 0.5rem 1rem;
|
|
323
|
+
}
|
|
324
|
+
.client-search-dropdown [role="option"]:hover,
|
|
325
|
+
.client-search-dropdown [role="option"][aria-selected="true"] {
|
|
326
|
+
background: #f5f5f5;
|
|
327
|
+
}
|
|
307
328
|
|
|
308
329
|
/* Type-based styling via data attributes */
|
|
309
|
-
.client-search-dropdown
|
|
310
|
-
.client-search-dropdown
|
|
330
|
+
.client-search-dropdown [role="option"][data-source="documents"] { border-left: 3px solid #48c774; }
|
|
331
|
+
.client-search-dropdown [role="option"][data-file-type="pdf"]::before {
|
|
311
332
|
content: "PDF"; background: #ff0000; color: white;
|
|
312
333
|
padding: 0 0.3em; font-size: 0.7em; margin-right: 0.3em;
|
|
313
334
|
}
|
|
@@ -450,15 +471,18 @@ client_search:
|
|
|
450
471
|
output: search-relations.json
|
|
451
472
|
minimum_similarity: 0.55
|
|
452
473
|
max_items: null
|
|
474
|
+
index_model: null # default: embedding.model
|
|
453
475
|
embedding:
|
|
454
476
|
enabled: false
|
|
455
|
-
model: embeddinggemma:
|
|
477
|
+
model: embeddinggemma-2:570m
|
|
456
478
|
base_url: http://localhost:11434
|
|
457
479
|
connect_timeout: 5
|
|
458
480
|
read_timeout: 120
|
|
459
481
|
fail_on_error: true
|
|
460
482
|
query_embedder:
|
|
461
483
|
type: transformers
|
|
484
|
+
semantic:
|
|
485
|
+
index_model: null # default: embedding.model
|
|
462
486
|
```
|
|
463
487
|
|
|
464
488
|
All options:
|
|
@@ -487,9 +511,13 @@ All options:
|
|
|
487
511
|
| `related.semantic` | `true` | Include vector relations when document embeddings are available. |
|
|
488
512
|
| `related.minimum_similarity` | `0.55` | Cosine cutoff for semantic relations; no fixed relation count. |
|
|
489
513
|
| `related.max_items` | `null` | Optional safety cap after cutoff; `null` keeps every matching relation. |
|
|
514
|
+
| `related.index_model` | `embedding.model` | Ollama model for related-article similarity. May differ from the index model since relations never reach the browser. |
|
|
515
|
+
| `semantic.index_model` | `embedding.model` | Ollama model whose vectors go into the index; must match the browser query model. |
|
|
516
|
+
| `semantic.document_prefix` | Model-specific | Document prefix for `semantic.index_model` (defaults to `embedding.document_prefix` when the models match). |
|
|
517
|
+
| `semantic.query_prefix` | Model-specific | Query prefix for `semantic.index_model` (defaults to `embedding.query_prefix` when the models match). |
|
|
490
518
|
| `embedding.enabled` | `false` | Generate embedding vectors at build time via Ollama. |
|
|
491
|
-
| `embedding.model` | `embeddinggemma:
|
|
492
|
-
| `embedding.base_url` | `http://localhost:11434` | Ollama server URL. |
|
|
519
|
+
| `embedding.model` | `embeddinggemma-2:570m` | Ollama model name for embedding generation. |
|
|
520
|
+
| `embedding.base_url` | `http://localhost:11434` | Ollama server URL. `localhost` is resolved once to the loopback literal (`127.0.0.1` or `[::1]`) that accepts a TCP connection, so IPv4-only and IPv6-only servers both work. |
|
|
493
521
|
| `embedding.connect_timeout` | `5` | Ollama connection timeout in seconds. |
|
|
494
522
|
| `embedding.read_timeout` | `120` | Ollama response timeout in seconds. |
|
|
495
523
|
| `embedding.fail_on_error` | `true` | Fail the build when an embedding cannot be generated. |
|
|
@@ -610,7 +638,7 @@ Jekyll build time using a local [Ollama](https://ollama.ai/) server and the
|
|
|
610
638
|
1. Install [Ollama](https://ollama.ai/) and pull an embedding model:
|
|
611
639
|
|
|
612
640
|
```bash
|
|
613
|
-
ollama pull embeddinggemma:
|
|
641
|
+
ollama pull embeddinggemma-2:570m
|
|
614
642
|
```
|
|
615
643
|
|
|
616
644
|
2. Add `ollama-ruby` to your site's Gemfile (optional dependency):
|
|
@@ -626,10 +654,18 @@ Jekyll build time using a local [Ollama](https://ollama.ai/) server and the
|
|
|
626
654
|
engine: semantic
|
|
627
655
|
embedding:
|
|
628
656
|
enabled: true
|
|
629
|
-
model: embeddinggemma:300m
|
|
630
657
|
base_url: http://localhost:11434
|
|
631
658
|
```
|
|
632
659
|
|
|
660
|
+
With `query_embedder.type: transformers` (the default), the build
|
|
661
|
+
model needs a packaged browser ONNX mapping — `embeddinggemma` and
|
|
662
|
+
`embeddinggemma-2` models have one (see
|
|
663
|
+
[Browser query models](#browser-query-models)); any other
|
|
664
|
+
`embedding.model` requires an explicit `query_embedder.model` or
|
|
665
|
+
`query_embedder.type: ollama_api`. For build-time-only embeddings — a
|
|
666
|
+
lexical engine with `related` analysis — any Ollama model works
|
|
667
|
+
as-is.
|
|
668
|
+
|
|
633
669
|
### How it works
|
|
634
670
|
|
|
635
671
|
- At build time, the `OllamaEmbeddingAdapter` sends each document's text
|
|
@@ -642,34 +678,175 @@ Jekyll build time using a local [Ollama](https://ollama.ai/) server and the
|
|
|
642
678
|
- A content-hash cache (`.jekyll-client-search-cache.json` in the site
|
|
643
679
|
source) tracks which documents have already been embedded. Unchanged
|
|
644
680
|
documents reuse the cached embedding — only new or modified documents
|
|
645
|
-
are sent to the model.
|
|
681
|
+
are sent to the model. When the index and related models differ, the
|
|
682
|
+
related vectors are cached separately in
|
|
683
|
+
`.jekyll-client-search-related-cache.json`.
|
|
646
684
|
- Cache entries include the provider, model, endpoint, and cache schema. A
|
|
647
685
|
model or embedding configuration change automatically invalidates old
|
|
648
686
|
vectors and re-embeds the documents.
|
|
649
|
-
- Cache writes are atomic. The cache
|
|
650
|
-
(
|
|
687
|
+
- Cache writes are atomic. The cache files are git-ignored and safe to
|
|
688
|
+
delete (they will be rebuilt on the next `jekyll build`).
|
|
651
689
|
- By default, an embedding failure fails the build rather than silently
|
|
652
690
|
producing an unusable semantic index. Set `embedding.fail_on_error: false`
|
|
653
691
|
only when warning-only behavior is intentional.
|
|
654
692
|
|
|
655
693
|
### Choosing a model
|
|
656
694
|
|
|
657
|
-
Any Ollama-compatible embedding model works.
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
695
|
+
Any Ollama-compatible embedding model works. Hosted embedding APIs (OpenAI
|
|
696
|
+
`text-embedding-3-small`, Gemini Embedding, the Voyage API) are not
|
|
697
|
+
supported — the adapter only speaks Ollama's `/api/embed` protocol, and
|
|
698
|
+
local embeddings keep the corpus on your machine. Open-weight models that
|
|
699
|
+
ship GGUF builds (e.g. `voyage-4-nano`) can still be tested through a
|
|
700
|
+
local Ollama server.
|
|
701
|
+
|
|
702
|
+
Measured on a 152-document site (Apple Silicon, sequential `/api/embed`
|
|
703
|
+
calls, GPU-resident models, `related.minimum_similarity: 0.55`):
|
|
704
|
+
|
|
705
|
+
| Model | Dims | ~ms/doc | Full corpus | Quality* | Value† | ~Cost/yr‡ | Notes |
|
|
706
|
+
| --- | --- | --- | --- | --- | --- | --- | --- |
|
|
707
|
+
| `embeddinggemma-2:570m` | 768 | ~246 | ~37 s | 111 | 0.50 | ~63 s | **Default** — best judged relatedness short of non-commercial jina; text vectors byte-identical to 270m; ONNX twin packaged (verify parity) |
|
|
708
|
+
| `embeddinggemma-2:270m` | 768 | ~260 | ~40 s | 111 | 0.47 | ~67 s | Same text vectors as 570m; shares its ONNX twin (verify parity) |
|
|
709
|
+
| `embeddinggemma:300m` | 768 | ~105 | ~16 s | 96 | 1.00 | ~27 s | Fast; verified transformers.js browser mapping |
|
|
710
|
+
| `nomic-embed-text` | 768 | ~100 | ~15 s | 105 | **1.16** | ~25 s | Fast; best measured value |
|
|
711
|
+
| `voyage-4-nano` | 1024 | ~115 | ~17 s | 107 | 1.03 | ~29 s | Open-weight (Apache 2.0); community GGUF at its 1024-dim base — official 2048 dim (post-hoc linear head) retested, judged equivalent (103) |
|
|
712
|
+
| `bge-m3` | 1024 | ~305 | ~46 s | 107 | 0.38 | ~78 s | Multilingual; strong judged quality |
|
|
713
|
+
| `jina-embeddings-v3` | 1024 | ~241 | ~37 s | **112** | 0.51 | ~62 s | CC-BY-NC-4.0 (non-commercial); community GGUF base weights — best judged picks + 88% corroboration; text-matching LoRA tested via llama.cpp: worse (98) |
|
|
714
|
+
| `bge-large` | 1024 | ~115 | ~17 s | 106 | 1.01 | ~29 s | English-only yet still strong judged quality |
|
|
715
|
+
| `mxbai-embed-large` | 1024 | ~340 | ~52 s | 107 | 0.35 | ~87 s | Strong judged quality |
|
|
716
|
+
| `snowflake-arctic-embed2` | 1024 | ~505 | ~77 s | 81 | 0.17 | ~130 s | Conservative scores — few edges pass the 0.55 cutoff, weakest judged picks |
|
|
717
|
+
| `all-minilm` | 384 | — | — | — | — | — | English only; not measured |
|
|
718
|
+
| `qwen3-embedding:0.6b` | 1024 | ~350 | ~53 s | 90 | 0.28 | ~89 s | Strong multilingual results upstream |
|
|
719
|
+
| `qwen3-embedding:4b` | 2560 | ~4000 | ~10 min | 87 | 0.02 | ~17 min | Poor value: ~38× the cost for below-average judged picks |
|
|
720
|
+
| `qwen3-embedding:8b` | 4096 | ~10800 | ~27 min | 100 | 0.01 | ~46 min | Reference quality but ~103× the cost; build-time only |
|
|
721
|
+
|
|
722
|
+
\* judged relevance: `gpt-oss:120b-cloud` scored every deduplicated
|
|
723
|
+
related-article suggestion 0–2, blind to which model proposed it
|
|
724
|
+
(`benchmarks/related_quality_judge.rb`); mean precision normalized to
|
|
725
|
+
`qwen3-embedding:8b` = 100. Raw mean scores ranged 0.70–0.96 of 2.00.
|
|
726
|
+
† quality ÷ ms/doc, normalized to `embeddinggemma:300m` = 1.00.
|
|
727
|
+
‡ amortized embedding time per year assuming one cold re-embed plus weekly
|
|
728
|
+
builds changing two posts — at this scale every model is affordable, so
|
|
729
|
+
quality dominates the choice.
|
|
730
|
+
|
|
731
|
+
The index cache amortizes cold-corpus cost — only new or changed documents
|
|
732
|
+
re-embed — but every changed post pays the per-document latency at each
|
|
733
|
+
build.
|
|
734
|
+
|
|
735
|
+
Judged quality inverted the earlier agreement-based ranking: models with
|
|
736
|
+
"hot" similarity distributions (`embeddinggemma-2`, `nomic-embed-text`,
|
|
737
|
+
`voyage-4-nano`, `bge-m3`, `mxbai-embed-large`) were not producing noise —
|
|
738
|
+
the judge rated their novel picks as the *most* relevant. Conservative
|
|
739
|
+
models (snowflake, the small Qwen3s) scored worst on judged precision
|
|
740
|
+
despite high agreement with `qwen3-embedding:8b`. Practical takeaway for
|
|
741
|
+
this corpus size: `nomic-embed-text`, `voyage-4-nano`, and
|
|
742
|
+
`embeddinggemma:300m` offer the best measured value; the default
|
|
743
|
+
`embeddinggemma-2:570m` (and its identical-text-vector twin `:270m`)
|
|
744
|
+
trades ~2.5× the per-document latency for the best judged relatedness of
|
|
745
|
+
any license-free model; the large Qwen3 models do not pay for themselves
|
|
746
|
+
below several thousand documents.
|
|
747
|
+
|
|
748
|
+
"Build-time only" models are too large to run in the browser for
|
|
749
|
+
`engine: semantic` — use them only with a lexical engine, where embeddings
|
|
750
|
+
serve `related` analysis and never reach the visitor's browser, or with
|
|
751
|
+
`query_embedder.type: ollama_api` where every visitor can reach the API.
|
|
665
752
|
|
|
666
753
|
The model used at build time must match the query model's weights, vector
|
|
667
754
|
space, dimensions, and preprocessing. Cross-runtime compatibility is verified
|
|
668
|
-
for the
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
755
|
+
for the `embeddinggemma:300m` → `onnx-community/embeddinggemma-300m-ONNX`
|
|
756
|
+
mapping. The `embeddinggemma-2` family maps to
|
|
757
|
+
`onnx-community/embeddinggemma-2-ONNX` — packaged but not yet parity-verified
|
|
758
|
+
against the Ollama vectors, so confirm ranking on your own corpus. An
|
|
759
|
+
`all-minilm` mapping is also provided. For any other Ollama model, set
|
|
760
|
+
`embedding.query_embedder.model` to a compatible transformers.js model
|
|
761
|
+
explicitly or use `ollama_api` so both paths call the same Ollama model.
|
|
762
|
+
|
|
763
|
+
### Splitting the index and related models
|
|
764
|
+
|
|
765
|
+
`search-relations.json` is computed entirely at build time, so related
|
|
766
|
+
analysis can use a different vector space than the semantic index — the
|
|
767
|
+
index vectors are the only ones the browser query embedder must match.
|
|
768
|
+
Two optional keys control this; both default to `embedding.model`:
|
|
769
|
+
|
|
770
|
+
```yaml
|
|
771
|
+
client_search:
|
|
772
|
+
engine: semantic
|
|
773
|
+
embedding:
|
|
774
|
+
enabled: true
|
|
775
|
+
model: embeddinggemma-2:570m # default build model
|
|
776
|
+
semantic:
|
|
777
|
+
index_model: embeddinggemma:300m # vectors written into the index
|
|
778
|
+
related:
|
|
779
|
+
enabled: true
|
|
780
|
+
index_model: embeddinggemma-2:570m # vectors used for related analysis
|
|
781
|
+
```
|
|
782
|
+
|
|
783
|
+
When `semantic.index_model` and `related.index_model` resolve to the same
|
|
784
|
+
model, documents are embedded once and the vector serves both outputs —
|
|
785
|
+
there is no extra cost. When they differ, each document is embedded twice
|
|
786
|
+
(once per model) and the related vectors are kept in a separate field,
|
|
787
|
+
analyzed by `RelatedAnalyzer`, then stripped before `search-index.json`
|
|
788
|
+
is written. Each model gets its own content-hash cache, so per-model
|
|
789
|
+
vectors still amortize across builds. `semantic.document_prefix` /
|
|
790
|
+
`semantic.query_prefix` can override the index model's prefixes; explicit
|
|
791
|
+
`embedding.document_prefix` / `embedding.query_prefix` apply to
|
|
792
|
+
`embedding.model`.
|
|
793
|
+
|
|
794
|
+
`semantic.index_model` matters whenever vectors go into the index —
|
|
795
|
+
including a lexical engine with `embedding.include_in_index: true`. It
|
|
796
|
+
must always match the browser query model; `semantic.index_model:
|
|
797
|
+
embeddinggemma:300m` is the safe choice for the verified transformers.js
|
|
798
|
+
mapping.
|
|
799
|
+
|
|
800
|
+
### Browser query models
|
|
801
|
+
|
|
802
|
+
With `query_embedder.type: transformers`, the query model runs in the
|
|
803
|
+
visitor's browser and must be a transformers.js-compatible ONNX build of
|
|
804
|
+
the *index* model's weights. These Ollama models have a packaged mapping:
|
|
805
|
+
|
|
806
|
+
| `embedding.model` / `semantic.index_model` | transformers.js model |
|
|
807
|
+
| --- | --- |
|
|
808
|
+
| `embeddinggemma:300m` | `onnx-community/embeddinggemma-300m-ONNX` (verified) |
|
|
809
|
+
| `embeddinggemma-2`, `embeddinggemma-2:270m`, `embeddinggemma-2:570m` | `onnx-community/embeddinggemma-2-ONNX` (verify parity) |
|
|
810
|
+
| `all-minilm` | `Xenova/all-MiniLM-L6-v2` |
|
|
811
|
+
|
|
812
|
+
For anything else, set `embedding.query_embedder.model` to a compatible
|
|
813
|
+
transformers.js model or use `query_embedder.type: ollama_api`. The full
|
|
814
|
+
set of supported architectures and tasks is in the
|
|
815
|
+
[transformers.js docs](https://huggingface.co/docs/transformers.js/index);
|
|
816
|
+
compatible ONNX weights are tagged
|
|
817
|
+
[`library:transformers.js` on the Hub](https://huggingface.co/models?library=transformers.js).
|
|
818
|
+
|
|
819
|
+
Document/query prefixes are applied automatically for `embeddinggemma`,
|
|
820
|
+
`embeddinggemma-2`, `nomic-embed-text`, and `voyage-4-nano`; other models
|
|
821
|
+
get no prefix.
|
|
822
|
+
Qwen3 embedding models take no document prefix; for semantic search the
|
|
823
|
+
instruct-format query prefix must be set via `embedding.query_prefix`.
|
|
824
|
+
Cosine similarity distributions differ substantially per model — some run
|
|
825
|
+
"narrow" (snowflake: almost nothing passes 0.55) and some run "hot"
|
|
826
|
+
(nomic, embeddinggemma-2: nearly everything does) — so retune
|
|
827
|
+
`related.minimum_similarity` and the semantic threshold when switching
|
|
828
|
+
rather than comparing at the default.
|
|
829
|
+
|
|
830
|
+
To benchmark your own site and corpus:
|
|
831
|
+
|
|
832
|
+
```bash
|
|
833
|
+
cd jekyll-client-search
|
|
834
|
+
bundle install
|
|
835
|
+
OLLAMA_BASE_URL=http://127.0.0.1:11434 \
|
|
836
|
+
EMBEDDING_MODELS="embeddinggemma:300m,nomic-embed-text,bge-m3" \
|
|
837
|
+
bundle exec ruby benchmarks/related_model_benchmark.rb /path/to/site
|
|
838
|
+
```
|
|
839
|
+
|
|
840
|
+
The script times `OllamaEmbeddingAdapter.embed` per document and runs the
|
|
841
|
+
real `RelatedAnalyzer`, then diffs each model's related lists against the
|
|
842
|
+
baseline (`BASELINE_MODEL` env, default `embeddinggemma-2:570m`). Raw vectors
|
|
843
|
+
are cached under `benchmarks/.related-embeddings/` so re-runs are cheap.
|
|
844
|
+
|
|
845
|
+
A `localhost` `base_url` is resolved automatically to the loopback literal
|
|
846
|
+
that accepts a TCP connection (`127.0.0.1` or `[::1]`), because the HTTP
|
|
847
|
+
client does not fall back across address families and Ollama often listens
|
|
848
|
+
on IPv4 loopback only. If `ollama embed` calls still fail with a socket
|
|
849
|
+
error, set `base_url: http://127.0.0.1:11434` explicitly.
|
|
673
850
|
|
|
674
851
|
## Related articles reference
|
|
675
852
|
|
|
@@ -977,7 +1154,7 @@ client_search:
|
|
|
977
1154
|
engine: semantic
|
|
978
1155
|
embedding:
|
|
979
1156
|
enabled: true
|
|
980
|
-
model: embeddinggemma:
|
|
1157
|
+
model: embeddinggemma-2:570m
|
|
981
1158
|
query_embedder:
|
|
982
1159
|
type: ollama_api
|
|
983
1160
|
api_url: https://embeddings.example.com/api/embed
|