---
title: Search and analytics
description: 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.
section: guides
order: 5
---

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

```ts
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:

```sh
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
```

2. Bind both in `wrangler.jsonc`, then deploy:

```jsonc
"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.

::callout{type="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.
::

## 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`:

```ts
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:

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

```txt
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:

```sh
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**.

```json
{ "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.
