Guides
Search and analytics
Find entries by meaning in any language, show related entries, and let an agent see what readers read, from the analytics your site already has.
Search and analytics
Search by words works everywhere, from the first npm create hollware. Search by meaning is opt-in, because it uses services that cost money past their free allowance. What readers read comes from the analytics your site already has, once you connect it.
Search
The tool search and the search method of a site find published entries in three ways:
mode | Finds |
|---|---|
fulltext | the entries that hold every word of the query |
semantic | the entries closest in meaning, in any language |
hybrid | both, merged; the default |
The answer says which mode ran. Without search by meaning, every mode runs as fulltext, and the answer says so.
Search by words ignores accents, so migracion finds migración. Search by meaning finds a Spanish post for an English question.
Answer questions with your content
search with passages: true also answers, for each result, the passage that matched: the whole section of the entry, with its heading, its text as a reader reads it, and the url of the entry when the site has one. That is what a model reads to answer a question with your content and cite it (retrieval-augmented generation). minScore, from 0 to 1, leaves out what matches by meaning less than that, so a question your content does not answer finds nothing instead of the least bad entry:
const found = await env.HOLLWARE.search('my-site', 'how do I cache pages?', {
passages: true,
minScore: 0.5,
limit: 3,
})
const context = found.results.map((r) => `## ${r.passage?.heading}\n${r.passage?.text}\n(${r.passage?.url})`)Hollware finds the passages; the model that writes the answer is yours to choose. Over HTTP, add passages=true&minScore=0.5.
Turn on search by meaning
It needs two bindings: Workers AI, which turns text into vectors, and a Vectorize index, which finds the nearest ones.
- Create the index, with the dimensions of the model, and the metadata indexes it filters on:
npx wrangler vectorize create my-cms-bge-m3 --dimensions=1024 --metric=cosine
npx wrangler vectorize create-metadata-index my-cms-bge-m3 --property-name=site --type=string
npx wrangler vectorize create-metadata-index my-cms-bge-m3 --property-name=collection --type=string
npx wrangler vectorize create-metadata-index my-cms-bge-m3 --property-name=locale --type=string
npx wrangler vectorize create-metadata-index my-cms-bge-m3 --property-name=piece --type=number- Bind both in
wrangler.jsonc, then deploy:
"ai": { "binding": "AI" },
"vectorize": [{ "binding": "VECTORIZE", "index_name": "my-cms-bge-m3" }]Within a few minutes Hollware has indexed what you published: each entry becomes one vector for its summary and one for each ## section. New publishes are indexed as they happen, and an unpublished entry leaves the index.
The model is @cf/baai/bge-m3 unless the variable HOLLWARE_EMBEDDING_MODEL names another. An index serves one model, since its dimensions are fixed: to change the model, create a new index, point both at it, and deploy. Hollware indexes everything again on its own.
"placement": { "mode": "smart" } to wrangler.jsonc, so the Worker runs next to it.Related entries
With search by meaning on, each published entry knows the five entries of its collection and locale closest to it. Ask for them with get_entry, or getEntry with related: true:
const answer = await env.HOLLWARE.getEntry('my-site', 'blog', 'hello-world', 'en', { related: true })
answer.related // published entries, closest firstSee what readers read
Hollware counts no page views itself. It reads them from the analytics your site already has, with a read-only key, and keeps nothing about readers. It reads four providers:
| Provider | The read-only key |
|---|---|
| Cloudflare Web Analytics | An API token with Account Analytics: Read, on the account of the site |
| PostHog | A personal API key with Query: Read and Project: Read, limited to the project of the site |
| Plausible | A key of type Stats API |
| Google Analytics 4 | The JSON key of a service account that is a Viewer of the property |
Connect one in the admin, under Site → Analytics: choose the provider, paste the key and press Connect. Hollware first asks the provider for the last 7 days, and keeps the key only when it answers:
Connected: 139 views on 21 pages in the last 7 days.A key without the right permission is refused with the permission to add. The key is sealed with HOLLWARE_SECRET, which hollware deploy makes, and is never shown again.
Some providers need one more setting:
- Cloudflare: the account id, the 32 characters after
dash.cloudflare.com/in the address of the dashboard. - GA4: the property id, the digits under Admin → Property details.
- PostHog and Plausible, self-hosted: the address of the instance.
- Other hosts: the host of the site's URL always counts. Add the others the site answers on, such as
www.example.com.
The CLI connects the same way, with a token of an owner. It reads the key from a file, never from the command line:
npx hollware analytics connect posthog --credential-file posthog.key --hosts www.example.com \
--url https://cms.example.com --token-file owner.token
npx hollware analytics status --url https://cms.example.com --token-file owner.tokenhollware analytics disconnect forgets the connection and its key. A new connection replaces the old one.
Let agents read the counts
content_stats answers the most read paths of the last days (days, 1 to 90, 30 by default), each with the published entry it shows, and source, the provider it read. The admin's home shows the same list under Most read.
{ "outcome": "ok", "source": "posthog", "pages": [{ "path": "/blog/hello-world", "views": 116, "entry": { "slug": "hello-world" } }] }Hollware keeps only the site's hosts, drops the query and the fragment, and counts /a and /a/ as one path. Cloudflare's counts are sampled, in steps of ten; they still rank pages well, which is what the counts are for.
An answer is kept for ten minutes. A provider has 25 s to answer: PostHog's query API takes 5 to 15 s, so the first read of the day can be slow. Without a connection, content_stats answers unavailable, and its fix says how to connect one.
Connect the key in the admin or with the CLI, not through an agent: then the key never passes through the agent's context.