<!-- generated by build_articles.py — edit content/answers/full-text-search-browser-history.md instead -->
---
title: "Can I full-text search my browser history file?"
canonical: "https://xerj.org/answers/full-text-search-browser-history"
updated: "2026-08-21"
source: "content/answers/full-text-search-browser-history.md"
---

# Is there a way to full-text search my browser history file itself?

**TL;DR** — Copy the Chrome `History` SQLite file, then run `xerj autoindex` on the folder. A captured run returned 60 hits for a title phrase and 1 hit for an exact URL. All 3 source checksums were unchanged, and `body` returned 0 documents because Chrome stores no page text.

## Agent prompt

```text
Act as a coding agent. Read https://xerj.org/llms.txt, close the browser, copy the Chrome History file and its -wal and -shm side files to a scratch folder, record sha256 of all 3, start a local XERJ node, run `xerj autoindex ./browser-history --url http://127.0.0.1:9200 --prefix bh --progress plain`, re-record the checksums, then POST a match_phrase on title, a term on url, and an exists query on body, and report the hit totals plus the row count in the source urls table.
```

## Commands

### Command 1

Note: Record the source checksums before and after the run, to prove nothing was written.

```sh
bash -c 'cd ./browser-history && sha256sum History History-wal History-shm'
```

### Command 2

Note: Index the copied History database. XERJ opens SQLite read-only.

```sh
xerj autoindex ./browser-history --url http://127.0.0.1:9200 --prefix bh --progress plain
```

### Command 3

Note: Full-text search the page titles Chrome stored.

```sh
curl -s -XPOST http://127.0.0.1:9200/bh-*/_search -H 'content-type: application/json' -d '{"query":{"match_phrase":{"title":"Lemur documentation portal"}},"size":5,"_source":["url","title","visit_count","last_visit_time"],"track_total_hits":true}'
```

### Command 4

Note: Prove that no page body exists, because Chrome does not store one.

```sh
curl -s -XPOST http://127.0.0.1:9200/bh-*/_search -H 'content-type: application/json' -d '{"query":{"exists":{"field":"body"}},"size":5,"track_total_hits":true}'
```

## Copy the file, then index the folder

Copy `History` to a scratch folder while the browser is closed. XERJ opens SQLite read-only, and the captured run left the file byte-identical. A live profile can still change while you read it.

```sh
xerj autoindex ./browser-history --url http://127.0.0.1:9200 --prefix bh --progress plain
```

The run read 1 file and produced 4 datasets with 624 documents live. The indices were `bh-urls` with 300 documents, `bh-visits` with 300, `bh-keyword-search-terms` with 21 and `bh-meta` with 3. XERJ writes 1 index per table, with no configuration.

## The mandatory warning: WAL rows are invisible

A copied `History` file is read only as far as its last checkpoint. Rows that live only in `History-wal` are not indexed and return 0 hits, and nothing in the output says so.

The captured database held 340 committed rows in `urls`. 300 of them were checkpointed into `History` and indexed. The 40 rows resident only in `History-wal` returned 0 hits for a phrase that exists in all 40.

XERJ refused the side files themselves. `History-wal` and `History-shm` were each detected as `binary` with the reason `binary content (unknown)`, and produced 0 documents.

A live browser profile always has an uncheckpointed tail. Your most recent pages are therefore the first ones to disappear from the index. This reproduces a first-pass finding on an unrelated support-ticket database, in a second database with a different schema.

## Titles are full-text, URLs are exact

XERJ inferred the field types from the `urls` table without configuration. The 2 fields you search behave differently, and mixing them up is the most common way to get 0 hits.

| field | inferred type | query that works |
| --- | --- | --- |
| `title` | `text` | `match` or `match_phrase` |
| `url` | `keyword` | `term` with the whole URL |
| `visit_count`, `typed_count`, `id`, `hidden` | `long` | `range` or `term` |
| `last_visit_time` | `long` | `range` on microseconds since 1601-01-01 |

A `match_phrase` on `title` returned 60 hits. An exact `term` on `https://docs.example.invalid/page/5?ref=history` returned 1 hit. A `match` for the substring `docs.example.invalid` on the same field returned 0, because a `keyword` field is not analyzed.

```sh
curl -s -XPOST 'http://127.0.0.1:9200/bh-*/_search' \
  -H 'content-type: application/json' \
  -d '{"query":{"match_phrase":{"title":"Lemur documentation portal"}},"size":5,"_source":["url","title","visit_count","last_visit_time"],"track_total_hits":true}'
```

## There is no page text to search

Chrome does not keep the content of the pages you visited in `History`. An `exists` query on `body` returned 0 documents across every index in the captured run.

Decide on that basis. If you want to search what a page said, archive the page yourself and index the archive. Browser history alone gives you the title, the URL and the visit counters.

## The source database is unchanged

The harness recorded the sha256 of `History`, `History-wal` and `History-shm` before the run and again after it. All 3 values matched.

```sh
bash -c 'cd ./browser-history && sha256sum History History-wal History-shm'
```

Run that line on both sides of the index run if the profile matters to you. An immutable read is the property that makes this safe to do on a copy of real data.

## What this capture does not show

The `History` file in this run came from the fixture generator, written to the Chrome schema in WAL mode. No browser ran on the host, and no profile was read. Firefox uses a different schema and was not tested.

XERJ runs single-node here, with no replication and no failover. The default embedder in XERJ is lexical feature hashing, so a query and a paraphrase that share no words do not match. Neural embeddings are opt-in through `--embed-mode neural`.

Every number above comes from RUN-B, captured on 2026-08-21. The binary was a `ci-test` profile build, so no wall-clock figure from this run is published as a performance number.

## FAQ

### Is there a way to full-text search my browser history?

Yes, on the page titles and URLs the browser stores. A captured `match_phrase` on `title` returned 60 hits, each with its URL and visit count.

### How do I search Chrome places.sqlite?

Chrome keeps its history in a SQLite file named `History`; `places.sqlite` is the Firefox name. Close the browser, copy the file, index the copy, and query `title` and `url`.

### Can I index a Firefox places database?

It is a SQLite file, so `autoindex` reads it the way it reads any other. This capture used the Chrome `History` file, so the Firefox table and column names were not verified here.

### Can I search the text of the pages I visited?

No. The browser stores no page body in the history database. An `exists` query on `body` returned 0 documents in the captured run.

### Does indexing modify my History file?

No. XERJ opens SQLite read-only. The sha256 of `History`, `History-wal` and `History-shm` was identical before and after the captured run.

### Why are my most recent pages missing?

A copied `History` file is read only as far as its last checkpoint. In the captured run 40 of 340 rows lived only in `History-wal` and returned 0 hits.

### Why does a partial URL return nothing?

XERJ typed `url` as `keyword`, so it matches whole values only. An exact `term` on the full URL returned 1 hit; a `match` on a substring returned 0.

## Related

- [How do I full-text search a SQLite database I just copied onto disk?](/answers/full-text-search-sqlite-database)
- [How should an agent figure out what's in a messy data folder before searching?](/answers/catalog-files-with-autoindex-map)
- [Why would a folder search miss files that I can see on disk?](/answers/why-autoindex-skipped-files)
