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.

The tool search and the search method of a site find published entries in three ways:

modeFinds
fulltextthe entries that hold every word of the query
semanticthe entries closest in meaning, in any language
hybridboth, 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.

  1. 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
  1. 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.

Tip
Search by meaning makes a few calls to D1 for each query. If your database is far from your readers, add "placement": { "mode": "smart" } to wrangler.jsonc, so the Worker runs next to it.

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 first

See 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:

ProviderThe read-only key
Cloudflare Web AnalyticsAn API token with Account Analytics: Read, on the account of the site
PostHogA personal API key with Query: Read and Project: Read, limited to the project of the site
PlausibleA key of type Stats API
Google Analytics 4The 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.token

hollware 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.