Skip to content

Search — Improvement & Growth Plan

Package: capell-app/search · Kind: package · Tier: premium · Product group: Capell Search & SEO · Bundle: search-seo · Status: Complete

capell-app/search adds a public frontend search surface (GET /search, autocomplete JSON, click-tracking beacon) plus an admin analytics layer (Top/Trending/Zero-result widgets, three overview stats, a settings page). Domain logic lives entirely in src/Actions behind a Capell\Search\Contracts\Search driver contract with three implementations: DatabaseSearch (LIKE/FULLTEXT over a flat table, default pages), SiteDiscoverySearch (canonical-URL registry metadata), and ScoutSearch (multi-source Scout). Key Actions: RunSearchAction (orchestrator), ResolveExpandedSearchQueriesAction (synonyms + Levenshtein/explicit typo correction), ApplySearchResultEnhancementsAction (promotions, source weighting, exact-match/recency/click boosts), SanitizeSearchResultAction (URL + meta scrubbing), RecordSearchAction (hashed query logging). One table search_logs (model SearchLog), one settings class SearchSettings, one command search:purge (scheduled monthly). Runtime deps are light: lorisleiva/laravel-actions, spatie/laravel-data, spatie/laravel-package-tools; Scout/Typesense and capell-app/site-discovery are suggest-only.

Current marketplace summary: “Production-grade site search for Capell — relevance-tuned results with synonyms, typo tolerance, and curated promotions, plus admin insights into what visitors search for and where you have no answers.” Manifest now declares the extension card, frontend search results, header search, settings, top-searches, trending-searches, zero-result widget captures in light and dark mode, plus an annotated curation screenshot for synonyms, typo corrections, and promoted results.

Prioritized; effort S/M/L.

  1. Done — Fix the outdated Search contract example in docsdocs/drivers-and-logging.md “Swap the Search Driver” now imports SearchFilterData, shows the full six-argument Search::search() signature ($siteId, $languageId, $filters), and demonstrates a public-safe escaping highlight() implementation. Evidence: docs/drivers-and-logging.md, src/Contracts/Search.php. — Shipped

  2. Shipped: expanded query search is bounded and first-page only. RunSearchAction now caps synonym/typo expansion breadth through capell-search.query_expansion.max_queries and uses the primary driver’s native paginator for page 2+ instead of refetching perPage * page rows for every expanded query. First-page expansion still merges and de-dupes the bounded result window; deeper pages keep accurate driver totals/current page state and avoid multiplicative DB/Scout round-trips. Evidence: config/capell-search.php, src/Actions/RunSearchAction.php, tests/Feature/Actions/SearchFeatureGapSliceTest.php. — M

  3. Shipped: click-count boosts use scoped cache and a collapsed enhancement pass. ApplySearchResultEnhancementsAction now caches clicked_result_url aggregates behind a short TTL scoped by site/language, invalidates affected scopes when clicks are recorded, and applies type labels, weights, exact-match, recency, and click boosts in one enhancement pass after promotion de-dupe/sanitization. Evidence: src/Actions/ApplySearchResultEnhancementsAction.php, src/Actions/RecordSearchResultClickAction.php, tests/Feature/Actions/SearchFeatureGapSliceTest.php. — M

  4. Shipped: public result Blade receives hydrated highlight data. SearchController resolves the configured Search driver, builds highlightedResults, and passes that hydrated collection into the public result view; resources/views/components/results.blade.php no longer resolves Search from Blade. Evidence: src/Http/Controllers/SearchController.php, resources/views/components/results.blade.php, tests/Feature/Http/SearchControllerTest.php. — M

  5. Shipped: DatabaseSearch::canUseFullText() memoizes schema detection per connection/table/column set. The information_schema.STATISTICS lookup now runs once per compatible driver/database/table/column combination and caches both positive and fallback results. Evidence: src/Drivers/DatabaseSearch.php, tests/Unit/Search/DatabaseSearchTest.php. — S/M

  6. Shipped: FULLTEXT index matching now accepts covering indexes regardless of order. Configured search columns only need to be present in one FULLTEXT index, so indexes with additional columns or different ordering no longer silently fall back to LIKE; incomplete indexes still fall back. Evidence: src/Drivers/DatabaseSearch.php, tests/Unit/Search/DatabaseSearchTest.php. — M

  7. Shipped: autocomplete uses a lightweight driver path. RunAutocompleteSearchAction now calls the configured Search driver directly with normalized query, filters, site, and language, then applies only cheap response mapping/type-label hydration. It skips promotions, click-count aggregates, recency boosts, and the rest of RunSearchAction’s enhancement pipeline for keystroke traffic. Evidence: src/Actions/RunAutocompleteSearchAction.php, tests/Feature/Http/SearchControllerTest.php. Query-suggestion UX remains a separate §3 roadmap item. — M

  8. Shipped: result type labels resolve through translations with config overrides. The default config no longer ships hardcoded English labels; ResolveSearchResultTypeLabelAction honors site-configured labels first, then capell-search::types.*, then a headline fallback for unknown types. Search results and autocomplete both use the same resolver. Evidence: src/Actions/ResolveSearchResultTypeLabelAction.php, resources/lang/en/types.php, src/Actions/ApplySearchResultEnhancementsAction.php, src/Actions/RunAutocompleteSearchAction.php, tests/Feature/Actions/ResolveSearchResultTypeLabelActionTest.php. — S/M

  9. Shipped: click tracking uses a token-first log match. GenerateSearchClickTokenAction emits an encrypted per-render token with normalized query/site/language context, result links echo it through the beacon, and RecordSearchResultClickAction resolves the latest matching log from the token before falling back to the legacy visitor tuple. Proxy IP or user-agent churn no longer drops result-page clicks, while old markup/autocomplete payloads remain compatible. Evidence: src/Actions/GenerateSearchClickTokenAction.php, src/Actions/RecordSearchResultClickAction.php, resources/views/components/results.blade.php, tests/Feature/Actions/RecordSearchActionTest.php, tests/Feature/Http/SearchControllerTest.php. — M

  10. Shipped curation UI; advanced ranking/driver tuning remains config-only. SearchSettingsSchema now exposes synonyms, typo corrections/terms, typo max distance, and promoted results as admin-editable runtime settings. Source weights, ranking boosts, database column mapping, and database column weights still live in config/capell-search.php; those lower-level tuning knobs can remain a later admin polish item. Evidence: src/Filament/Settings/SearchSettingsSchema.php, src/Settings/SearchSettings.php, config/capell-search.php, tests/Unit/Filament/SearchSettingsSchemaTest.php, tests/Feature/Actions/SearchCurationSettingsTest.php. — L

Tied to capabilities[] (search, search-admin/console/frontend, search-synonyms, search-promoted-results, search-typo-corrections, search-source-weighting, search-zero-result-reporting, search-site-discovery-indexing) and search-category norms.

  • Shipped: faceted filtering UI with live counts. BuildSearchFacetGroupsAction builds type/source facet groups from enabled searchable sources, computes counts through the configured Search driver with the active site/language/filter context, and pages/search.blade.php renders neutral public filter links before results. Evidence: src/Actions/BuildSearchFacetGroupsAction.php, src/Data/SearchFacetGroupData.php, src/Data/SearchFacetOptionData.php, resources/views/components/facets.blade.php, resources/views/pages/search.blade.php, tests/Feature/Actions/SearchFacetGroupsActionTest.php, tests/Feature/Http/SearchControllerTest.php.
  • Shipped: admin settings manage synonyms, promoted results, and typo dictionaries. SearchSettingsSchema now exposes curation controls for synonyms, explicit typo corrections, typo dictionary terms/max distance, and promoted best-bet results. The curation actions read persisted SearchSettings through ResolveSearchSettingAction before config fallbacks, so admin changes affect runtime behavior. Evidence: src/Filament/Settings/SearchSettingsSchema.php, src/Settings/SearchSettings.php, src/Actions/ResolveExpandedSearchQueriesAction.php, src/Actions/ResolveCorrectedSearchQueryAction.php, src/Actions/ResolvePromotedSearchResultsAction.php, tests/Unit/Filament/SearchSettingsSchemaTest.php, tests/Feature/Actions/SearchCurationSettingsTest.php.
  • Shipped: zero-result terms can become persisted curation rules. CreateSynonymFromZeroResultSearchAction turns a zero-result query into a synonym alias for an existing target query, and CreatePromotedResultFromZeroResultSearchAction creates or replaces a promoted best-bet result for that zero-result query. Both update SearchSettings, so the existing expansion/promotion runtime consumes the fix immediately. Evidence: src/Actions/CreateSynonymFromZeroResultSearchAction.php, src/Actions/CreatePromotedResultFromZeroResultSearchAction.php, tests/Feature/Actions/ZeroResultCurationActionsTest.php.
  • Shipped: autocomplete includes query suggestions from search logs. BuildAutocompleteQuerySuggestionsAction returns popular normalized-query completions by prefix, scoped by site/language, and RunAutocompleteSearchAction exposes them as querySuggestions alongside result hits. The header autocomplete renders query suggestions before result rows. Evidence: src/Actions/BuildAutocompleteQuerySuggestionsAction.php, src/Data/AutocompleteQuerySuggestionData.php, src/Actions/RunAutocompleteSearchAction.php, resources/views/components/header/search-dialog.blade.php, tests/Feature/Actions/SearchSuggestionActionsTest.php, tests/Feature/Http/SearchControllerTest.php.
  • Shipped: did-you-mean correction metadata is surfaced. ResolveCorrectedSearchQueryAction resolves explicit and dictionary typo corrections, RunAutocompleteSearchAction exposes metadata.corrected, and the header autocomplete renders the corrected query as a selectable suggestion. Evidence: src/Actions/ResolveCorrectedSearchQueryAction.php, src/Data/SearchQueryMetadataData.php, resources/views/components/header/autocomplete-results.blade.php, tests/Feature/Actions/SearchSuggestionActionsTest.php, tests/Feature/Http/SearchControllerTest.php.
  • Shipped: Scout honors engine scores and totals where available. ScoutSearch now reads common engine relevance fields (_rankingScore, _score, scout_score, text_match) before falling back to its local substring score, and preserves Scout paginator totals when the engine reports more matches than the fetched window. Public visibility filtering still shrinks totals when hidden payloads are dropped locally. Evidence: src/Drivers/ScoutSearch.php, tests/Unit/Search/ScoutSearchTest.php.
  • Shipped: Scout index maintenance commands and freshness ownership docs. search:index imports enabled Scout-backed searchable sources via makeAllSearchable() using the configured/default chunk size, and search:flush removes configured sources via removeAllFromSearch(). Both commands support --source for targeted maintenance, are listed in capell.json, and docs/drivers-and-logging.md now documents that source packages own public payloads, observers, queue setup, and external index freshness. Evidence: src/Actions/IndexScoutSearchSourcesAction.php, src/Actions/FlushScoutSearchSourcesAction.php, src/Console/Commands/IndexSearchCommand.php, src/Console/Commands/FlushSearchCommand.php, docs/drivers-and-logging.md, tests/Feature/Actions/SearchScoutMaintenanceActionsTest.php, tests/Feature/Console/SearchScoutMaintenanceCommandsTest.php.
  • Shipped: Database fallback ranking supports field weights. DatabaseSearch now accepts capell-search.database.column_weights and orders LIKE fallback results by a generated search_score, so title/excerpt/body relevance can be tuned without a custom driver. Global boosts (exact, recency, click) remain config scalars, and Scout delegates field relevance to the engine/source config. Evidence: config/capell-search.php, src/Drivers/DatabaseSearch.php, src/Providers/SearchServiceProvider.php, tests/Unit/Search/DatabaseSearchTest.php.
  • Shipped: runtime health probes cover driver resolution and search-log writes. SearchHealthCheck verifies the configured Search driver resolves, the search log table exists, SearchLog is registered, a synthetic query-log write/delete succeeds, and logging configuration is sane. Evidence: src/Health/SearchHealthCheck.php, tests/Feature/Health/SearchHealthCheckTest.php.
  • Runtime health probe now covers local failures. SearchHealthCheck now runs diagnostics for storage table presence, model registration, configured driver resolution, synthetic log write/delete, and logging configuration. — src/Health/SearchHealthCheck.php
  • Shipped guardrail — Site Discovery is now the safe default driver. New installs default to site_discovery through config, settings defaults, settings migration, and provider fallback, so the package searches Capell’s canonical public URL registry instead of assuming public page content lives in flat pages.title/excerpt/body columns. The database driver remains available as an explicit opt-in for flat index tables/views. Evidence: config/capell-search.php, src/Settings/SearchSettings.php, database/settings/2026_05_10_190869_01_add_search_settings.php, src/Providers/SearchServiceProvider.php, tests/Feature/Providers/SearchServiceProviderTest.php, docs/drivers-and-logging.md.
  • Shipped guardrail — Scout drops common non-public payload markers. ScoutSearch now filters indexed payloads marked draft, unpublished, private, restricted, or non-public before mapping public results, and focused tests cover those leak paths. Model-specific toSearchableArray() implementations should still avoid indexing sensitive fields, and site/language scoping remains best-effort through Scout where() support. Evidence: src/Drivers/ScoutSearch.php, tests/Unit/Search/ScoutSearchTest.php.
  • Shipped guardrail — public result meta is now explicit allow-list output. SanitizeSearchResultAction::safeMeta() only preserves keys listed in capell-search.public_urls.allowed_meta_keys and drops nested object/array payloads, so internal IDs, owner references, admin URLs, signed preview tokens, and arbitrary model metadata cannot ride through SearchResultData->meta. URL sanitization blocks configured private path prefixes and strips signed/preview query keys. Evidence: config/capell-search.php, src/Actions/SanitizeSearchResultAction.php, tests/Feature/Actions/SanitizeSearchResultActionTest.php.
  • Shipped guardrail — highlight() must escape before adding markup. results.blade.php:53-57 prints {!! $search->highlight(...) !!}. All three built-in drivers htmlspecialchars/e() the text before wrapping <mark>, tests assert escaping, and src/Contracts/Search.php now documents that custom implementations must escape the full input text and only add trusted <mark> markup. Evidence: src/Contracts/Search.php, docs/drivers-and-logging.md, tests/Unit/Search/DatabaseSearchTest.php, tests/Unit/Search/ScoutSearchTest.php, tests/Unit/Search/SiteDiscoverySearchTest.php.
  • Shipped: click-tracking beacon no longer depends on cached-page CSRF state. search/click is now explicitly CSRF-exempt and throttled through capell-search.click_tracking.rate_limiter; the header and result click beacons post without a meta CSRF token, and the result component owns a guarded beacon listener so search-result pages still track clicks when the header search modal is absent. Evidence: routes/web.php, config/capell-search.php, resources/views/components/header/search-dialog.blade.php, resources/views/components/results.blade.php, tests/Feature/Http/SearchControllerTest.php.
  • Shipped guardrail — search log writes are deferred after response. capell.json sets frontendRenderBudgetMs: 20 and adminQueryBudget: 40. The frontend path still does driver query work, but expanded queries are bounded/first-page only, click-count aggregates are cached per site/language, and the analytics insert is no longer inline: SearchController now calls RecordSearchAction::dispatchAfterResponse() with scalar visitor metadata, and RecordSearchAction still owns the actual write. Evidence: src/Http/Controllers/SearchController.php, src/Actions/RecordSearchAction.php, tests/Feature/Http/SearchControllerTest.php, tests/Feature/Actions/RecordSearchActionTest.php, tests/Feature/Actions/SearchFeatureGapSliceTest.php. cacheSafety.cacheable=false is correct (per-query).
  • Shipped i18n guardrail — Site Discovery titles flow into Search. Result type labels resolve through translations/config overrides, root URL titles translate through capell-search::generic.site_discovery_home_title, and SiteDiscoverySearch now prefers PublicUrlRegistryEntryData::title before deriving a slug title. The core CMS Site Discovery contributor passes localized discoverable page titles into the registry, and non-CMS Blog, Events, Knowledge Base, and Campaign Studio public URL contributors now populate PublicUrlData::title from their localized/model titles. Evidence: packages/site-discovery/src/Data/PublicUrlData.php, packages/site-discovery/src/Data/PublicUrlRegistryEntryData.php, packages/site-discovery/src/Support/PublicUrls/CmsPagePublicUrlContributor.php, packages/blog/src/Support/PublicUrls/BlogPublicUrlContributor.php, packages/events/src/Support/PublicUrls/EventsPublicUrlContributor.php, packages/knowledge-base/src/Support/PublicUrls/KnowledgeBasePublicUrlContributor.php, packages/campaign-studio/src/Support/PublicUrls/CampaignLandingPagePublicUrlContributor.php, src/Drivers/SiteDiscoverySearch.php, tests/Unit/Search/SiteDiscoverySearchTest.php, packages/site-discovery/tests/Integration/Discovery/PublicUrlRegistryActionTest.php.
  • Test coverage — covered: DatabaseSearch (escape, clamp, site/lang/status filter, pagination, highlight, FULLTEXT index compatibility), ScoutSearch (registry merge, weight, absolute URL, highlight) via in-memory fakes, SiteDiscoverySearch (indexability, metadata match, paginate/highlight), RecordSearchAction/RecordSearchResultClickAction, PurgeSearchLogsAction, ResolveExpandedSearchQueries (synonyms + typos), ApplySearchResultEnhancements (promotions + weights + dedupe), RunSearchAction (synonym dedupe), SearchController (autocomplete limits, blank query, normalized passthrough, configured-page view, frontend-shell view, click POST endpoint, click rate limiter, “no package identifiers in markup”), click-beacon route CSRF exemption and tokenless header/results markup, provider binding/driver selection, settings defaults, manifest requirements, generated-output coverage source, insights actions, widgets, and non-CMS Site Discovery title propagation.
  • Manifest alignment shipped. BuildTopClickedResultsQueryAction is now listed in capell.json actions[] and covered by ManifestRequirementsTest; drivers-and-logging.md now lists site_discovery as a first-class driver.

Shipped copy refresh. composer.json, capell.json, and resources/lang/en/package.php now lead with the production search value proposition instead of the old feature-dump copy. Evidence: composer description “Production-grade site search for Capell with relevance-tuned results, synonyms, typo tolerance, curated promotions, and admin search insights.” Marketplace summary “Production-grade site search for Capell — relevance-tuned results with synonyms, typo tolerance, and curated promotions, plus admin insights into what visitors search for and where you have no answers.”

Improved one-sentence summary:

Production-grade site search for Capell — relevance-tuned results with synonyms, typo tolerance, and curated promotions, plus admin insights into what visitors search for and where you have no answers.

Improved 3–4 sentence description:

Search gives every Capell site a fast, themeable search experience: a results page, header search field, and type-ahead autocomplete, backed by a pluggable driver contract (database, Site Discovery URL registry, or Laravel Scout/Meilisearch/Typesense). Curate results with synonyms, typo corrections, and promoted “best bet” answers, and tune ranking with source weighting plus exact-match, recency, and click-through boosts. The admin dashboard surfaces top searches, trending terms, and — critically — zero-result queries, so editors see exactly where content is missing. Privacy-first by default: visitor identifiers are hashed and query logs auto-purge on a configurable retention window.

Shipped screenshot/media promotion. capell.json.marketplace.screenshots now promotes the extension card plus frontend search, header search, search settings, top-searches, trending-searches, zero-result widget captures in light and dark mode, and an annotated curation screenshot for synonyms, typo corrections, and promoted results.

Pricing / tier / bundle positioning. premium tier in the search-seo bundle is right: search + SEO is a natural “growth” pairing. Position Search as the demand-side half (what visitors look for) against seo-suite as the supply-side half (how content is found by engines). Cross-sell is already wired: suggest lists capell-app/site-discovery (URL-registry indexing + generated-output coverage) and docs/README.md “Read Next” points at seo-suite and site-discovery. Make the bundle explicit: Search + SEO Suite + Site Discovery = “Findability”. Zero-result analytics is the upsell hook into content strategy because zero-result terms can become synonyms or promoted best-bet results.

Differentiators / value props / target buyer. Differentiators vs. table-stakes CMS search: (1) editable result curation (synonyms, promotions, typo dictionaries) — most CMS search can’t do this; (2) zero-result + trending analytics that turn search into a content-strategy signal; (3) driver-agnostic contract so a site starts on DB search and graduates to Typesense without touching the frontend. Target buyer: agencies and content teams running multi-site Capell installs who treat on-site search as a conversion/retention surface, not a checkbox.

Keywords/tags (8–12): site search, full-text search, laravel scout, meilisearch, typesense, autocomplete, synonyms, typo tolerance, search analytics, zero-result tracking, promoted results, faceted search.

ItemBucketEffortImpactSection ref
Shipped: Fix outdated 3-arg Search contract example in driver docsDoneSMed (prevents broken integrations)§2.1
Shipped: Promote real Search frontend/header/widget screenshots and an annotated curation screenshot in marketplace media.DoneSMed (free conversion lift)§5
Shipped: Rewrite composer description + marketplace summaryDoneSMed§5
Shipped: Document/guarantee highlight() must escapeDoneSHigh (XSS guardrail)§4
Shipped: Add Scout unpublished/private-exclusion test + DB no-status-column leak test. Evidence: ScoutSearch drops indexed payloads marked draft/private/restricted; DatabaseSearch fails closed when the configured published-status guard column is missing; driver tests cover both leak paths.DoneMHigh (result-visibility safety)§4
Shipped: Cache clickCounts() aggregate + collapse enhancement map chain. Evidence: click-count boosts use a short TTL cache scoped by site/language, click recording invalidates the affected scopes, promoted-result conversion no longer remaps promotions twice, and focused action tests prove cached aggregates preserve scoped ranking.DoneMHigh (frontend budget)§2.3
Shipped: Move RecordSearchAction write to after-response dispatch. Evidence: SearchController dispatches after response; RecordSearchAction receives scalar visitor metadata; controller/action tests cover deferred dispatch and write behavior.DoneMHigh (20ms render budget)§4
Shipped: Fix click beacon CSRF on cached pages. Evidence: search/click is CSRF-exempt + throttled, header/results beacons post without CSRF meta state, and controller tests cover the route middleware plus tokenless markup.DoneMHigh (analytics actually works)§4
Shipped: Memoize FULLTEXT detection and accept covering indexes regardless of column order. Evidence: DatabaseSearch caches schema compatibility by connection/database/table/columns and unit tests cover covering vs incomplete index sets.DoneMMed (DB driver perf + correctness)§2.5, §2.6
Shipped: Resolve driver in controller and pass hydrated highlight data into Blade. Evidence: SearchController now supplies highlightedResults; public result Blade no longer resolves Search; controller tests assert highlighted view data/rendered markup.DoneMMed (convention compliance)§2.4
Shipped: Lightweight autocomplete path skips enhancement pipeline. Evidence: autocomplete calls the driver directly and focused controller coverage proves promoted results are not injected into type-ahead responses.DoneMMed (public endpoint load)§2.7, §3
Shipped: Translate result type labels with config overrides. Evidence: both search results and autocomplete use ResolveSearchResultTypeLabelAction; tests cover configured, translated, and fallback labels.DoneS/MMed (localized result chips)§2.8
Shipped: Did-you-mean surface + query-completion suggestions from logs. Evidence: autocomplete exposes metadata.corrected and querySuggestions; header autocomplete renders corrected/popular query rows; focused action/controller tests cover corrections and scoped log-backed suggestions.DoneMHigh (search norm + UX)§3
Shipped: Admin UI for synonyms / promoted results / typo dictionaries. Evidence: settings schema exposes editable curation fields and curation actions read persisted settings before config fallback; focused tests cover schema fields and runtime overrides.DoneLHigh (premium differentiator)§2.10, §3
Shipped: Faceted-filter frontend with live counts. Evidence: type/source facet groups compute counts through the active search driver and render as public-safe filter links on the search page; action/controller tests cover counts, selected toggles, URLs, and markup safety.DoneLHigh (table-stakes surface)§3
Shipped: Honor Scout engine scores/total where available, and document Scout source freshness ownership.DoneLHigh (premium Scout story)§3
Shipped: Runtime health probe for driver resolution and search-log writes. Evidence: health checks now resolve the configured Search driver and perform a synthetic query-log write/delete; tests cover successful probes and failure cases.DoneMMed (priority-support SKU)§3, §4
Shipped: Zero-result → create synonym/promotion curation actions. Evidence: actions persist zero-result terms into synonym aliases or promoted best-bet results consumed by runtime expansion/promotion logic; focused tests cover dedupe and runtime effects.DoneMHigh (cross-sell + stickiness)§3, §5