Skip to content

Commit b4619e0

Browse files
jonaspauletaclaude
andcommitted
chore!: reset version history and ship clean v1.0.0
Collapse pre-1.0 changelog entries, drop ApexScout namespace history and post-1.0 upgrade notes, soften positioning vs Meilisearch/Algolia/ Typesense, add FAQ + "Should I use this?" sections, scope ensureSearchable cache + lookup by database/schema/table, drop minimum-stability=dev, add ext-pdo_pgsql requirement. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent df41a5b commit b4619e0

11 files changed

Lines changed: 172 additions & 324 deletions

File tree

.github/ISSUE_TEMPLATE/bug.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ body:
1515
attributes:
1616
label: Package version
1717
description: Output of `composer show jonaspauleta/scout-postgres | grep versions`.
18-
placeholder: "0.3.0"
18+
placeholder: "1.0.0"
1919
validations:
2020
required: true
2121

.github/PULL_REQUEST_TEMPLATE.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ Closes #
2222
- [ ] Pest test added that fails before this change and passes after.
2323
- [ ] `composer quality` passes clean (Rector, Pint, PHPStan level max).
2424
- [ ] `composer test` passes locally against Postgres 14+.
25-
- [ ] `CHANGELOG.md` updated under `[Unreleased]` using Keep a Changelog categories.
25+
- [ ] `CHANGELOG.md` updated with an entry describing the change, using Keep a Changelog categories.
2626
- [ ] README / docblocks updated if public API changed.
2727
- [ ] Conventional Commit prefix on the PR title (`feat:`, `fix:`, `docs:`, …).
2828

CHANGELOG.md

Lines changed: 64 additions & 234 deletions
Large diffs are not rendered by default.

CLAUDE.md

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,8 @@
11
# scout-postgres — Agent Guide
22

3-
Native Postgres 18 FTS + pg_trgm engine for Laravel Scout. Standalone
3+
Native Postgres FTS + pg_trgm engine for Laravel Scout. Standalone
44
package — published as `jonaspauleta/scout-postgres` on Packagist.
5-
Root PHP namespace is `ScoutPostgres\`. The legacy `ApexScout\ScoutPostgres\`
6-
namespace was removed in `1.1.0` — pre-1.1 imports must be migrated to
7-
`ScoutPostgres\…`.
5+
Root PHP namespace is `ScoutPostgres\`.
86

97
## Installation
108

CONTRIBUTING.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ Thanks for considering a contribution. This document is short and concrete.
1414
composer test # pest
1515
```
1616

17-
5. Update `CHANGELOG.md` under the `[Unreleased]` section using [Keep a Changelog](https://keepachangelog.com/en/1.1.0/) categories (`Added` / `Changed` / `Fixed` / `Deprecated` / `Removed` / `Security`).
17+
5. Update `CHANGELOG.md` with an entry describing the change, using [Keep a Changelog](https://keepachangelog.com/en/1.1.0/) categories (`Added` / `Changed` / `Fixed` / `Deprecated` / `Removed` / `Security`). Maintainers cut and tag releases.
1818
6. Commit using [Conventional Commits](https://www.conventionalcommits.org/) (`feat:`, `fix:`, `docs:`, `refactor:`, `test:`, `chore:` …).
1919
7. Open a PR against `main`. Link any related issue.
2020

README.md

Lines changed: 48 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -6,14 +6,32 @@
66
[![Total Downloads](https://img.shields.io/packagist/dt/jonaspauleta/scout-postgres.svg?style=flat-square)](https://packagist.org/packages/jonaspauleta/scout-postgres)
77
[![License](https://img.shields.io/packagist/l/jonaspauleta/scout-postgres.svg?style=flat-square)](LICENSE.md)
88

9-
Postgres-native search engine for [Laravel Scout](https://laravel.com/docs/scout). Removes the need for Meilisearch / Algolia / Typesense for the common 80% case when you already run Postgres — no extra service, no sync queue, no separate index. See [Comparison](#comparison) and [Limitations](#limitations) for the cases it does **not** cover.
9+
A Postgres-native Laravel Scout engine for apps that want search without running Meilisearch, Algolia, or Typesense. Targets the common 80% case when you already run Postgres — no extra service, no sync queue, no separate index. See [Should I use this?](#should-i-use-this), [Comparison](#comparison), and [Limitations](#limitations) for the cases it does **not** cover.
1010

1111
Works on any Postgres 14+ where `pg_trgm` and `unaccent` are available — managed (Neon, Laravel Cloud, RDS, Supabase, DigitalOcean) or self-managed.
1212

1313
## Why
1414

1515
Most Laravel apps already have Postgres. Adding Meilisearch or Typesense means another service to deploy, secure, monitor, scale, and keep in sync. For mid-sized catalogs (millions of rows, sub-100ms p95) Postgres FTS combined with `pg_trgm` similarity is good enough, cheaper, and stays consistent with your source of truth — there is no separate index to drift out of sync.
1616

17+
## Should I use this?
18+
19+
**Use it when:**
20+
21+
- You already run Postgres and don't want to operate a second search service.
22+
- Your corpus is in the hundreds-of-thousands to low-millions of rows.
23+
- You need typo tolerance, prefix / as-you-type matching, and accent-insensitive search.
24+
- You're happy with raw-SQL faceting and aggregation (Scout returns model IDs; you join from there).
25+
- A single `regconfig` (text search configuration) per table is enough — usually `simple_unaccent` or one language per migration.
26+
27+
**Use a dedicated engine instead when:**
28+
29+
- You need first-class faceting, highlighting, or synonym dictionaries.
30+
- You need per-document language switching (English row, Portuguese row, Japanese row in the same table).
31+
- Your corpus is hundreds of millions of rows.
32+
- You need geo-search and don't want to add PostGIS.
33+
- You need learning-to-rank, vector hybrid search, or relevance feedback features.
34+
1735
## Comparison
1836

1937
Honest side-by-side. Use this to decide whether `scout-postgres` is the right tool, or whether you actually need a dedicated engine.
@@ -266,8 +284,8 @@ LIMIT :perPage OFFSET :offset;
266284

267285
So in the worst case adaptive runs two queries; in the common case it runs
268286
one. `query_strategy=hybrid` forces single-pass FTS+trigram for every
269-
query (the pre-1.0 behaviour). `query_strategy=fts_only` never runs the
270-
trigram pass — fastest but loses typo recovery.
287+
query. `query_strategy=fts_only` never runs the trigram pass — fastest but
288+
loses typo recovery.
271289

272290
### Short-prefix fast path
273291

@@ -329,8 +347,8 @@ that matter:
329347
are available as opt-in tunings; their thresholds have different
330348
semantics so re-tune the threshold when switching.
331349

332-
See [`benchmarks/`](benchmarks/) for measured numbers on a 50,150-row
333-
corpus, including before/after deltas vs `1.0.0`.
350+
See [`benchmarks/`](benchmarks/) for measured numbers on 50k and 500k
351+
row corpora, with full methodology and the artisan harness.
334352

335353
## Production notes
336354

@@ -382,8 +400,7 @@ Book::search('foo')
382400
->paginate(20);
383401
```
384402

385-
Or globally via `SCOUT_POSTGRES_TOTAL_COUNT=true` to restore the pre-1.0
386-
default for every search.
403+
Or globally via `SCOUT_POSTGRES_TOTAL_COUNT=true` for every search.
387404

388405
When `total_count` is enabled the window aggregate forces Postgres to
389406
materialise every matching row before applying `LIMIT`, so latency scales
@@ -394,8 +411,6 @@ million-row corpora.
394411

395412
The package is **stable** as of `v1.0.0`. The public API — the `postgresSearchable()` migration macro, the `dropPostgresSearchable()` macro, the `PostgresSearchable` contract, the `pgsql` Scout engine driver name, and the keys in `config/scout-postgres.php` — is committed across the entire `1.x` line. Breaking changes will land on a `2.0.0` release tag and will be documented in `CHANGELOG.md` with a migration note.
396413

397-
The legacy `ApexScout\ScoutPostgres\` namespace was dropped in `1.1.0` — migrate any remaining imports to `ScoutPostgres\…`.
398-
399414
## Limitations
400415

401416
By design, this package targets the 80% case. The following are **not** in scope for the `1.x` line:
@@ -421,32 +436,34 @@ The model's connection driver is not `pgsql`. Check `config/database.php` and th
421436
**`text search configuration "simple_unaccent" does not exist`**
422437
The package migration did not run. Run `php artisan migrate`. The package's own migration is loaded automatically — no `--path` flag is needed.
423438

424-
## Upgrading from earlier 1.x releases
439+
## FAQ
425440

426-
The post-1.0.0 work flips several behavioural defaults to make the
427-
out-of-the-box experience faster on common Scout-driver workloads. Each is
428-
backed by a config key so the old behaviour can be restored without code
429-
changes — useful when an existing deployment depends on the previous
430-
ranking or pagination semantics.
441+
**Do I need to run `php artisan scout:import`?**
442+
No. The `search_vector` and `search_text` columns are `STORED GENERATED`,
443+
so Postgres recomputes them automatically on every `INSERT` / `UPDATE`.
444+
There is no external index to seed.
431445

432-
| Change | Restore old behaviour |
433-
|---------------------------------------------|------------------------------------------------------|
434-
| Adaptive query strategy (FTS-first) | `SCOUT_POSTGRES_QUERY_STRATEGY=hybrid` |
435-
| Short-prefix fast path | `SCOUT_POSTGRES_PREFIX_FAST_PATH=false` |
436-
| `COUNT(*) OVER()` omitted by default | `SCOUT_POSTGRES_TOTAL_COUNT=true` |
437-
| `SET LOCAL jit = off` per query | `SCOUT_POSTGRES_DISABLE_JIT=false` |
438-
| `search_text` capped at 1000 chars (new migrations only) | pass `0` as the third macro arg: `$table->postgresSearchable($weights, null, 0)` |
446+
**Should I enable `SCOUT_QUEUE=true`?**
447+
No — keep it `false`. Scout's queue exists to debounce writes to a remote
448+
index; this engine has no remote index. Enabling the queue just adds a
449+
no-op job to your worker.
439450

440-
Existing `search_text` columns are unaffected by the cap — only new
441-
migrations applying `postgresSearchable()` pick up the `LEFT(...)` wrapper.
451+
**Why are `update`, `delete`, `flush`, `createIndex`, and `deleteIndex`
452+
no-ops?**
453+
The Postgres generated columns and GIN indexes are the source of truth.
454+
There is no separate index to push to, so Scout's per-row sync hooks have
455+
nothing to do — the engine implements them as no-ops on purpose. Adding
456+
`postgresSearchable()` in a migration is the only "indexing" step.
442457

443-
The legacy `ApexScout\ScoutPostgres\` namespace was removed in `1.1.0`.
444-
Replace any remaining imports:
458+
**What text search configuration does the engine use?**
459+
`simple_unaccent` — a copy of the built-in `simple` config mapped through
460+
the `unaccent` dictionary. The package migration creates it. Override via
461+
`SCOUT_POSTGRES_CONFIG` or per model via `scoutPostgresConfig()`.
445462

446-
```diff
447-
-use ApexScout\ScoutPostgres\Contracts\PostgresSearchable;
448-
+use ScoutPostgres\Contracts\PostgresSearchable;
449-
```
463+
**Can different models use different connections?**
464+
Yes. The model's connection driver must be `pgsql`, otherwise the engine
465+
throws `UnsupportedDriverException`. The engine reads the connection from
466+
the model on every query.
450467

451468
## Testing
452469

@@ -467,7 +484,7 @@ When sending a PR:
467484

468485
1. Add a Pest test that fails before your change and passes after.
469486
2. Run `composer quality` — it must pass clean (Pint, Rector, PHPStan level max).
470-
3. Update `CHANGELOG.md` under the `[Unreleased]` section.
487+
3. Update `CHANGELOG.md` with an entry describing the change.
471488

472489
## Security
473490

SECURITY.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -34,8 +34,8 @@ Security fixes are issued for the latest minor release. Older minors receive fix
3434

3535
| Version | Supported |
3636
|---------|--------------------|
37-
| 0.3.x ||
38-
| < 0.3 ||
37+
| 1.x ||
38+
| < 1.0 ||
3939

4040
## Credit
4141

benchmarks/README.md

Lines changed: 20 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -52,12 +52,12 @@ php artisan bench:scout --seed=500000 --runs=30 --warmup=3
5252

5353
Run on Postgres 18.3, PHP 8.5, Laravel 13, 500,150 rows, hot cache.
5454

55-
### Current defaults (adaptive strategy, prefix fast path, JIT off, total_count off)
55+
### Default config (500,150 rows)
5656

57-
Run with the 1.1.0 default config: `query_strategy=adaptive`,
57+
Run with the shipped defaults: `query_strategy=adaptive`,
5858
`prefix_fast_path=true`, `disable_jit=true`, `total_count=false`,
5959
`trigram_function=similarity`, `trigram_threshold=0.3`. Schema migrated with
60-
the new `search_text` cap (1000 chars).
60+
the `search_text` cap (1000 chars).
6161

6262
| query | `pgsql` p50 (ms) | `database` p50 (ms) | hits (pg / db) | notes |
6363
|--------------------|-----------------:|--------------------:|:---------------|:-----------------------------------------------------|
@@ -81,7 +81,9 @@ holds latency bounded and where it doesn't:
8181
- `qwxzqwxzqwxz` stays cheap because the trigram bitmap is empty — the
8282
GIN index returns no candidates regardless of corpus size.
8383

84-
### 50,150-row results (post-1.0 defaults) — for reference
84+
### Default config (50,150 rows) — for corpus-size scaling
85+
86+
Same configuration, smaller corpus, same hardware:
8587

8688
| query | `pgsql` p50 (ms) | `database` p50 (ms) | hits (pg / db) |
8789
|--------------------|-----------------:|--------------------:|:---------------|
@@ -93,51 +95,26 @@ holds latency bounded and where it doesn't:
9395
| `qwxzqwxzqwxz` | 7.9 | 191.5 | 0 / 0 |
9496
| long natural query | 7.1 | 2.9 | 20 / 20 |
9597

96-
### 1.0.0 results at 50,150 rows (`trigram_threshold = 0.3`, no adaptive)
97-
98-
| query | `pgsql` p50 (ms) | `database` p50 (ms) | hits (pg / db) | notes |
99-
|--------------------|-----------------:|--------------------:|:---------------|:------------------------------------------|
100-
| `world` | 8.9 | 2.3 | 20 / 20 | both indexed paths fast |
101-
| `modern history` | 185.7 | 196.0 | **20 / 0** | `database` cannot bridge token gaps |
102-
| `philosophical exposition` | 82.6 | 2.8 | 20 / 20 | window-aggregate cost on broad match set |
103-
| `phil` | 599.5 | 2.7 | 20 / 20 | short prefix → broad match → big sort |
104-
| `philosphy` | 9.2 | 186.7 | 0 / 0 | trigram correctly rejects, fast |
105-
| `qwxzqwxzqwxz` | 4.6 | 175.5 | 0 / 0 | GIN miss is instant; LIKE seq-scans |
106-
| `a comprehensive history of modern philosophical thought` | 31.3 | 2.6 | 20 / 20 | FTS handles long queries |
107-
108-
### Pre-1.0 default at 50,150 rows (`trigram_threshold = 0.15`) — for reference
109-
110-
| query | `pgsql` p50 (ms) | `database` p50 (ms) | hits (pg / db) |
111-
|--------------------|-----------------:|--------------------:|:---------------|
112-
| `world` | 180.2 | 2.4 | 20 / 20 |
113-
| `modern history` | 1263.9 | 181.3 | 20 / 0 |
114-
| `philosophical exposition` | 1584.4 | 3.0 | 20 / 20 |
115-
| `phil` | 1428.7 | 2.9 | 20 / 20 |
116-
| `philosphy` | 483.6 | 172.8 | 0 / 0 |
117-
| `qwxzqwxzqwxz` | 4.8 | 176.5 | 0 / 0 |
118-
| long natural query | 1393.7 | 3.0 | 20 / 20 |
119-
120-
The 0.15 default is preserved here as evidence for the threshold change in 1.0.0.
121-
12298
## Findings
12399

124100
1. **`scout-postgres` has measurably better recall.** Multi-token queries,
125101
prefix matches, and accent variants all return matches that the
126102
`database` driver's `LIKE %term%` strategy misses entirely.
127103
2. **The adaptive strategy + short-prefix fast path are the dominant wins.**
128-
`phil` drops from 599 ms (1.0.0) to 4 ms (current) because the fast path
129-
skips both `websearch_to_tsquery` and the trigram bitmap entirely. Multi-
130-
token FTS queries (e.g. `modern history`) drop from 186 ms to 5 ms
131-
because adaptive returns the FTS hits without running the trigram pass.
132-
3. **`total_count=false` removes the second-biggest cost.** The previous
133-
`COUNT(*) OVER()` window aggregate forced materialisation of the full
134-
match set before `LIMIT`. Latency now scales with page size, not
135-
match-set size. Opt back in per-query when an exact total is required.
136-
4. **The `trigram_threshold` setting is still the dominant trigram cost knob.**
137-
A 2× change in threshold can change p50 by 10–100× on the hybrid
138-
fallback path. The default `0.3` is chosen to be safe for typical
139-
mixed-length corpora; tune **higher** for long-text corpora, **lower**
140-
only for short titles where false negatives are the bigger concern.
104+
The fast path skips both `websearch_to_tsquery` and the trigram bitmap
105+
entirely on single short tokens, so `phil` runs in ~4 ms even on
106+
500k rows. Multi-token FTS queries (e.g. `modern history`) stay
107+
sub-10 ms because adaptive returns the FTS hits without running the
108+
trigram pass.
109+
3. **`total_count=false` removes a major p95 cost.** A `COUNT(*) OVER()`
110+
window aggregate forces materialisation of the full match set before
111+
`LIMIT`. With it off, latency scales with page size, not match-set
112+
size. Opt back in per-query when an exact total is required.
113+
4. **`trigram_threshold` is the dominant trigram cost knob.** A 2× change
114+
in threshold can change p50 by 10–100× on the hybrid fallback path.
115+
The default `0.3` is chosen to be safe for typical mixed-length
116+
corpora; tune **higher** for long-text corpora, **lower** only for
117+
short titles where false negatives are the bigger concern.
141118
5. **The `database` driver wins on small literal-substring queries** because
142119
`LIMIT 20` short-circuits its sequential scan. It loses badly on
143120
`no_match` (full table seq scan) and on multi-token queries (zero recall).

composer.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,8 @@
3030
],
3131
"require": {
3232
"php": "^8.3",
33+
"ext-pdo": "*",
34+
"ext-pdo_pgsql": "*",
3335
"illuminate/contracts": "^11.0 || ^12.0 || ^13.0",
3436
"illuminate/database": "^11.0 || ^12.0 || ^13.0",
3537
"illuminate/support": "^11.0 || ^12.0 || ^13.0",
@@ -79,6 +81,5 @@
7981
"providers": ["ScoutPostgres\\ScoutPostgresServiceProvider"]
8082
}
8183
},
82-
"minimum-stability": "dev",
8384
"prefer-stable": true
8485
}

config/scout-postgres.php

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -97,8 +97,8 @@
9797
| FTS+trigram query only when FTS recall is insufficient.
9898
| Cheapest on common queries; same recall as "hybrid" on
9999
| typo / fuzzy queries via fallback.
100-
| "hybrid" — always run the FTS+trigram query in a single pass. The
101-
| pre-1.0 behaviour. Use to reproduce historical timings.
100+
| "hybrid" — always run the FTS+trigram query in a single pass.
101+
| Higher floor latency, no two-query fallback overhead.
102102
| "fts_only" — never use trigram. Loses typo tolerance but cuts the
103103
| trigram-bitmap candidate-set cost on every query.
104104
*/

0 commit comments

Comments
 (0)