Skip to content

Conversation

@hmans
Copy link
Owner

@hmans hmans commented Dec 9, 2025

Summary

  • Adds full-text search capability to beans using Bleve
  • Uses BM25 ranking algorithm for better relevance scoring
  • Search is exposed via GraphQL BeanFilter.search and CLI beans list --search
  • Index stored in .beans/.index/ (gitignored automatically)

Changes

  • New internal/search/ package - Bleve wrapper for indexing and searching beans
  • GraphQL API - Added search: String to BeanFilter input type
  • CLI - Added -S/--search flag to beans list command
  • Index lifecycle - Lazy init on first search, auto-rebuild on file changes
  • beans init - Now creates .beans/.gitignore to ignore the search index

Search Syntax

Supports Bleve query string syntax:

Query Meaning
login Exact term match
login~ Fuzzy match (1 edit distance)
login~2 Fuzzy match (2 edit distance)
log* Wildcard prefix
"user login" Exact phrase
user AND login Both terms required
user OR login Either term
title:login Search only title field
body:auth Search only body field

Usage

# CLI
beans list --search "authentication"
beans list -S "login~" -s todo        # fuzzy + status filter
beans list -S "auth*" --type bug      # wildcard + type filter

# GraphQL
beans query '{ beans(filter: { search: "authentication" }) { id title } }'
beans query '{ beans(filter: { search: "login", status: ["todo"] }) { id title } }'

Test plan

  • Unit tests for internal/search/ package
  • Integration tests for Core + search in internal/beancore/
  • All existing tests pass
  • Manual testing with project's own beans

hmans added 2 commits December 9, 2025 14:46
- Add internal/search package wrapping Bleve for full-text indexing
- Index bean title and body fields for search
- Expose search via `search` field in GraphQL BeanFilter
- Search is composable with existing filters (status, type, tags, etc.)
- Lazy index initialization on first search or mutation
- Synchronous index updates on Create/Update/Delete
- Auto-rebuild index on Load() for file watcher mode
- Store index in .beans/.index/ (auto-created, gitignored)
- beans init now creates .beans/.gitignore to ignore .index/
- Add internal/search package wrapping Bleve for full-text indexing
- Use BM25 scoring algorithm for better relevance ranking
- Index bean title and body fields for search
- Expose search via `search` field in GraphQL BeanFilter
- Search is composable with existing filters (status, type, tags, etc.)
- Lazy index initialization on first search or mutation
- Synchronous index updates on Create/Update/Delete
- Auto-rebuild index on Load() for file watcher mode
- Store index in .beans/.index/ (auto-created, gitignored)
- beans init now creates .beans/.gitignore to ignore .index/
hmans added 2 commits December 9, 2025 14:54
Add -S/--search flag for full-text search in title and body.
Composable with all other filters (status, type, tags, etc.).

Examples:
  beans list --search "authentication"
  beans list -S "login" -s todo
  beans list --search "auth*" --type bug
Add documentation for search syntax in:
- beans list command help (--search/-S flag)
- GraphQL schema (BeanFilter.search field)

Documents fuzzy matching (~), wildcards (*), phrases, boolean
operators, and field-specific searches.
hmans added 10 commits December 9, 2025 15:36
- Remove unused pendingReload variable in watcher.go
- Add DefaultSearchLimit constant to document the 1000 result limit
- Remove redundant `combined` field from search index (Bleve searches all fields by default)
- Add warning logging for non-fatal search index errors instead of silently discarding them
- Add SetWarnWriter() to configure or disable warning output
- Suppress warnings in tests via SetWarnWriter(nil)
- Hold write lock for entire Search() operation using defer
- Eliminates brief unlocked window between lock transitions
- Simpler to reason about with negligible performance impact
- Use write lock only for lazy index initialization
- Release lock before performing Bleve search (thread-safe)
- Use read lock only when reading from beans map

This allows concurrent searches to run in parallel and prevents
searches from blocking writes during the actual search operation.
- NewIndex now returns a boolean indicating if the index was rebuilt
- Core logs a warning when search index is rebuilt (corruption or first use)
- Helps users understand when their search index was regenerated
- Search errors are now propagated to the caller via GraphQL
- Users will see actual errors rather than empty results on failure
- Slug is now indexed alongside title and body for full-text search
- Users can search by slug content or use field-specific queries (slug:auth)
- Updated CLI help and GraphQL schema documentation
- Replace boolean 'rebuilt' with IndexStatus enum (Opened, Created, Recovered)
- Differentiate between new index and corrupted index recovery
- Only rebuild index when newly created or recovered from corruption
- Skip unnecessary rebuild when opening existing valid index
- Warning now only shown for actual corruption recovery
- Switch from disk-based Bleve index to in-memory using NewMemOnly()
- Remove IndexStatus enum and corruption recovery logic
- Remove .gitignore creation for .index/ directory
- Replace RebuildFromBeans() with simpler IndexBeans() batch method
- Simplify ensureSearchIndexLocked() in Core

The index is rebuilt on each invocation, which is fast for typical
project sizes (milliseconds for ~100 beans). This eliminates disk I/O,
corruption handling, and the need to gitignore the index directory.
Change "failed to rebuild search index" to "failed to reinitialize
search index after reload" to better describe when this warning occurs.
@hmans hmans merged commit bf738c4 into main Dec 11, 2025
1 check passed
@hmans hmans deleted the feat/text-search branch December 11, 2025 16:50
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants