commits
replaces python bot with zig implementation:
- uses std.http.Client for bsky api
- websocket.zig for jetstream consumption
- same matching logic (phrase extraction, cooldown)
- reduces memory from 1GB to 256MB
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- 50% chance to quote-post as normal
- 50% chance to just post bufo with rkey reference
- configurable via QUOTE_CHANCE env var
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
filters out: what-have-you-done, what-have-i-done, sad, crying, cant-take
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
API returns names without extension, alt text converted back
should now match correctly.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
checks our recent posts before posting to avoid repetition.
stateless - fetches from bluesky API, survives restarts.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- add Embedder trait (providers.rs) for swappable embedding backends
- add VectorStore trait for swappable vector search backends
- extract scoring/fusion logic into scoring.rs module
- extract filter logic into filter.rs with composable Filter trait
- refactor VoyageEmbedder and TurbopufferStore to implement traits
- simplify search.rs using new abstractions
- add unit tests for scoring and filtering
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
exclude=^bigbufo_&include=bigbufo_0_0,bigbufo_2_1 now works as expected.
include acts as an allowlist - matching results bypass exclusion.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
- filter results by comma-separated regex patterns via `exclude` param
- filtering happens before truncation so you always get top_k results
- frontend UI exposes exclude input with links to regex101 and claude
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
adds apple-touch-icon meta tags and web app manifest to enable proper
icon display when users add the site to their iOS home screen. includes
theme color metadata for better PWA experience.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
the backend was correctly returning helpful error messages (like "search query is too long..."), but the frontend was only showing "search failed: Bad Request" because it only read response.statusText and never read the response body.
now when an API error occurs, the frontend:
1. reads the response body text
2. displays the actual error message from the backend
3. falls back to statusText if body reading fails
this means users will now see the full helpful message: "search query is too long (max 1024 characters for text search). try a shorter query."
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
when users submit search queries exceeding turbopuffer's 1024 character limit for BM25 text search, the application now returns a 400 Bad Request with a helpful error message instead of a generic 500 Internal Server Error.
changes:
- add TurbopufferError enum to categorize different error types
- parse turbopuffer API error responses to detect query length violations
- return 400 status with user-friendly message for query length errors
- maintain 500 status for genuine server errors
this fix ensures users understand the limitation and can adjust their queries accordingly, without falsely suggesting a server-side problem.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
switch from animated gif to static png version that displays properly across all browsers.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
replace static png favicon with animated gif version. firefox will display the animation, while other browsers will show the first frame as a static image.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
backend changes:
- add family_friendly parameter to SearchQuery (default: true)
- implement blocklist filtering for inappropriate bufos
- filter applied to search results before return
- update etag generation to include filter state
frontend changes:
- add family-friendly mode checkbox in search options
- wire up to search API and URL state management
- checkbox checked by default for safe browsing
developer experience:
- add 'just dev' command with cargo-watch hot reload
- auto-reloads on changes to src/ or static/ files
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
- redesign filter toggle with icon and glassmorphic styling
- center and enlarge 'no bufos found' shrugging buffo image
- improve alpha slider precision to 0.01 steps
- add hover states and better visual hierarchy
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
problem: when alpha=0.0 (pure keyword), results from vector search
that don't match the keyword were appearing with 0.0 scores, polluting
the result set with irrelevant bufos
solution: filter out results with score < 0.001 before sorting
- prevents vector-only results when alpha=0.0 (pure keyword)
- prevents keyword-only results when alpha=1.0 (pure semantic)
- keeps result set clean and relevant
tested:
- query=redis, alpha=0.0 → 1 result (was 5 with 4 zeros) ✓
- query=bufo, alpha=0.0 → 5 clean results (no zeros) ✓
- query=happy, alpha=0.5 → balanced hybrid mix ✓
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
problem: min-max normalization crushed BM25 scores to near-zero when:
- single result returned: (score - min) / (max - min) = 0 / 0.001 = 0
- similar scores: small range compressed relative differences
solution: BM25-max-scaled normalization (divide by max score)
- ensures top result always gets 1.0
- preserves relative spacing between results
- handles edge cases (single result, clustered scores)
references:
- https://opensourceconnections.com/blog/2023/02/27/hybrid-vigor-winning-at-hybrid-search/
- used in TREC 2021 winning hybrid search submission
tested:
- query=redis, alpha=0.0 → score=1.0 ✓ (was 0.0)
- query=bufo, alpha=0.0 → [1.0, 0.928, 0.812] ✓ (proportional)
- query=happy, alpha=0.5 → exact matches rise in rankings ✓
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
isolate all bufo peeking logic into separate bufo-peek.js file, keeping
the silly whimsical behavior cleanly separated from core search functionality.
behavior:
- bufo randomly appears from one of four edges (top, right, bottom, left)
- rotates 90° between edges to always peek perpendicular to edge
- animates: peeks in → holds → peeks back out (6s cycle)
- moves to new random edge after each cycle
- hides permanently after first search
technical:
- new static/bufo-peek.js handles all positioning and animation logic
- CSS variables (--peek-start, --peek-in) drive smooth animations
- event-based communication (bufo-hide) for clean separation
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
implements GET /api/search endpoint alongside existing POST endpoint,
enabling users to share search results via URL.
backend changes:
- refactor search logic into shared perform_search function
- add search_get handler for GET /api/search?query=...&top_k=...
- implement HTTP caching with ETag and Cache-Control headers
- ETag based on query hash enables 304 Not Modified responses
- 5-minute cache TTL balances freshness vs performance
frontend changes:
- update URL params when search is submitted (?q=...&top_k=...)
- auto-execute search on page load if URL contains query params
- support browser back/forward navigation with popstate
- migrate from POST to GET for all searches
caching strategy:
- browser caches results for 5 minutes using Cache-Control
- ETag validation prevents redundant data transfer
- results may be stale if new bufos ingested, but TTL is short
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
- switch to monospace font (SF Mono, Monaco, etc) site-wide
- change background to muted leaf green (#8ba888)
- add bufo-just-checking.gif randomly peeking from left or right
- bufo flips horizontally when on left side
- bufo fades out after first search, returns on page refresh
- mobile responsive sizing (200px -> 150px -> 100px)
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
- add input_type="query" to search embeddings (src/embedding.rs)
- add input_type="document" to document embeddings (scripts/ingest_bufos.py)
- create justfile with re-index, deploy, run, build recipes
- remove unused thiserror dependency from Cargo.toml
- remove debug logs from embedding client
- update README to document input_type optimization
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
- use .entered() on all spans to create proper parent-child relationships
- now all spans share same trace_id and properly nest under bufo_search parent
- before: all spans had parent_span_id: null (disconnected)
- after: child spans have parent_span_id pointing to bufo_search
verified locally with logfire MCP:
bufo_search (parent)
├─ voyage.embed_text
└─ turbopuffer.query
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
- add top-level 'bufo_search' span to group all search operations
- rename spans to use service.operation pattern (voyage.embed_text, turbopuffer.query)
- add comprehensive attributes to each span and log:
* query text visible in all operations
* top_k parameter tracking
* embedding dimensions
* vector search distance metrics (min/max)
* result quality metrics (count, top result, scores)
- remove noisy turbopuffer response debug log
- all child spans now properly nested under parent search span
this creates a clear hierarchy in logfire:
bufo_search (parent)
├─ voyage.embed_text
└─ turbopuffer.query
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
- fix turbopuffer QueryResponse to match actual API format (direct array,
not wrapped in {rows: ...})
- fix QueryRow to use non-optional dist field as returned by API
- correctly compute similarity scores from cosine distance: 1 - (dist / 2.0)
- remove placeholder score logic that was returning all 1.0s
- fix landing page link to use correct tangled.org URL
search results now show actual semantic similarity scores instead of
placeholder 1.0 values.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
- add comprehensive module documentation to search.rs explaining the
multimodal early fusion approach with references to voyage AI research
- update landing page to link "semantic search" directly to our search.rs
implementation on tangled
- remove outdated "multimodal hybrid search" link to turbopuffer docs
(no longer accurate since we removed BM25 hybrid search)
- simplify landing page header by removing the secondary info line
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
research findings:
- voyage-multimodal-3 uses unified transformer encoder for text + images
- 41.44% improvement on retrieval with combined modalities
- no "pollution" - this is the intended design for the model
changes:
1. ingestion script: prepend filename text to content array
- convert "bufo-jumping-on-bed.png" -> "bufo jumping on bed"
- send as {"type": "text"} + {"type": "image_base64"} in same request
- model creates single unified embedding capturing both modalities
2. search logic: simplify by removing BM25 and RRF fusion
- early fusion embeddings already contain semantic text meaning
- rely entirely on vector search with unified embeddings
- removed bm25_query method from turbopuffer client
- eliminated complex RRF score calculation
benefits:
- simpler codebase (removed ~80 lines of RRF fusion logic)
- better semantic understanding (text + visual unified)
- fewer api calls (no separate BM25 search)
- research-validated approach
next step: re-run ingestion to regenerate all embeddings
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
use logfire sdk's with_default_level_filter(LevelFilter::INFO) to exclude
trace and debug level spans from actix-http internals (timers, flags, etc).
only capture info-level spans like requests and our custom search spans.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
logfire 0.8.2 requires edition2024 which is only available in nightly rust
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
- replace env_logger with logfire for structured observability
- add opentelemetry middleware for automatic http request tracing
- instrument search operations with custom spans:
- embedding generation
- vector search
- bm25 search
- reciprocal rank fusion
- remove unused upsert method and struct from turbopuffer client
(ingestion script calls api directly, not used by main application)
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
- add width: 100%, min-width: 0, flex-shrink: 0 to prevent button overflow
- replace min-max normalization with absolute RRF score scaling
- scale by 25x so good matches approach 1.0, poor matches stay low
- this respects actual match quality instead of forcing top result to 95%
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
- add bufo favicon
- link to bufo.zone and turbopuffer hybrid search docs
- link to source code on tangled.sh
- mobile-first responsive layout (single column on mobile)
- larger images on mobile for better visibility
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
- 10 requests per minute per IP (burst of 10)
- returns 429 Too Many Requests after limit exceeded
- protects voyage API costs from query spam
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
- multimodal image embeddings via voyage-multimodal-3
- hybrid search combining vector (ANN) + text (BM25) with RRF
- normalized similarity scores (0-95%) for intuitive UI display
- ingestion pipeline for bufo.zone images
- clean web interface with image grid and relevance scores
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
replaces python bot with zig implementation:
- uses std.http.Client for bsky api
- websocket.zig for jetstream consumption
- same matching logic (phrase extraction, cooldown)
- reduces memory from 1GB to 256MB
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- add Embedder trait (providers.rs) for swappable embedding backends
- add VectorStore trait for swappable vector search backends
- extract scoring/fusion logic into scoring.rs module
- extract filter logic into filter.rs with composable Filter trait
- refactor VoyageEmbedder and TurbopufferStore to implement traits
- simplify search.rs using new abstractions
- add unit tests for scoring and filtering
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
- filter results by comma-separated regex patterns via `exclude` param
- filtering happens before truncation so you always get top_k results
- frontend UI exposes exclude input with links to regex101 and claude
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
the backend was correctly returning helpful error messages (like "search query is too long..."), but the frontend was only showing "search failed: Bad Request" because it only read response.statusText and never read the response body.
now when an API error occurs, the frontend:
1. reads the response body text
2. displays the actual error message from the backend
3. falls back to statusText if body reading fails
this means users will now see the full helpful message: "search query is too long (max 1024 characters for text search). try a shorter query."
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
when users submit search queries exceeding turbopuffer's 1024 character limit for BM25 text search, the application now returns a 400 Bad Request with a helpful error message instead of a generic 500 Internal Server Error.
changes:
- add TurbopufferError enum to categorize different error types
- parse turbopuffer API error responses to detect query length violations
- return 400 status with user-friendly message for query length errors
- maintain 500 status for genuine server errors
this fix ensures users understand the limitation and can adjust their queries accordingly, without falsely suggesting a server-side problem.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
backend changes:
- add family_friendly parameter to SearchQuery (default: true)
- implement blocklist filtering for inappropriate bufos
- filter applied to search results before return
- update etag generation to include filter state
frontend changes:
- add family-friendly mode checkbox in search options
- wire up to search API and URL state management
- checkbox checked by default for safe browsing
developer experience:
- add 'just dev' command with cargo-watch hot reload
- auto-reloads on changes to src/ or static/ files
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
- redesign filter toggle with icon and glassmorphic styling
- center and enlarge 'no bufos found' shrugging buffo image
- improve alpha slider precision to 0.01 steps
- add hover states and better visual hierarchy
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
problem: when alpha=0.0 (pure keyword), results from vector search
that don't match the keyword were appearing with 0.0 scores, polluting
the result set with irrelevant bufos
solution: filter out results with score < 0.001 before sorting
- prevents vector-only results when alpha=0.0 (pure keyword)
- prevents keyword-only results when alpha=1.0 (pure semantic)
- keeps result set clean and relevant
tested:
- query=redis, alpha=0.0 → 1 result (was 5 with 4 zeros) ✓
- query=bufo, alpha=0.0 → 5 clean results (no zeros) ✓
- query=happy, alpha=0.5 → balanced hybrid mix ✓
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
problem: min-max normalization crushed BM25 scores to near-zero when:
- single result returned: (score - min) / (max - min) = 0 / 0.001 = 0
- similar scores: small range compressed relative differences
solution: BM25-max-scaled normalization (divide by max score)
- ensures top result always gets 1.0
- preserves relative spacing between results
- handles edge cases (single result, clustered scores)
references:
- https://opensourceconnections.com/blog/2023/02/27/hybrid-vigor-winning-at-hybrid-search/
- used in TREC 2021 winning hybrid search submission
tested:
- query=redis, alpha=0.0 → score=1.0 ✓ (was 0.0)
- query=bufo, alpha=0.0 → [1.0, 0.928, 0.812] ✓ (proportional)
- query=happy, alpha=0.5 → exact matches rise in rankings ✓
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
isolate all bufo peeking logic into separate bufo-peek.js file, keeping
the silly whimsical behavior cleanly separated from core search functionality.
behavior:
- bufo randomly appears from one of four edges (top, right, bottom, left)
- rotates 90° between edges to always peek perpendicular to edge
- animates: peeks in → holds → peeks back out (6s cycle)
- moves to new random edge after each cycle
- hides permanently after first search
technical:
- new static/bufo-peek.js handles all positioning and animation logic
- CSS variables (--peek-start, --peek-in) drive smooth animations
- event-based communication (bufo-hide) for clean separation
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
implements GET /api/search endpoint alongside existing POST endpoint,
enabling users to share search results via URL.
backend changes:
- refactor search logic into shared perform_search function
- add search_get handler for GET /api/search?query=...&top_k=...
- implement HTTP caching with ETag and Cache-Control headers
- ETag based on query hash enables 304 Not Modified responses
- 5-minute cache TTL balances freshness vs performance
frontend changes:
- update URL params when search is submitted (?q=...&top_k=...)
- auto-execute search on page load if URL contains query params
- support browser back/forward navigation with popstate
- migrate from POST to GET for all searches
caching strategy:
- browser caches results for 5 minutes using Cache-Control
- ETag validation prevents redundant data transfer
- results may be stale if new bufos ingested, but TTL is short
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
- switch to monospace font (SF Mono, Monaco, etc) site-wide
- change background to muted leaf green (#8ba888)
- add bufo-just-checking.gif randomly peeking from left or right
- bufo flips horizontally when on left side
- bufo fades out after first search, returns on page refresh
- mobile responsive sizing (200px -> 150px -> 100px)
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
- add input_type="query" to search embeddings (src/embedding.rs)
- add input_type="document" to document embeddings (scripts/ingest_bufos.py)
- create justfile with re-index, deploy, run, build recipes
- remove unused thiserror dependency from Cargo.toml
- remove debug logs from embedding client
- update README to document input_type optimization
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
- use .entered() on all spans to create proper parent-child relationships
- now all spans share same trace_id and properly nest under bufo_search parent
- before: all spans had parent_span_id: null (disconnected)
- after: child spans have parent_span_id pointing to bufo_search
verified locally with logfire MCP:
bufo_search (parent)
├─ voyage.embed_text
└─ turbopuffer.query
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
- add top-level 'bufo_search' span to group all search operations
- rename spans to use service.operation pattern (voyage.embed_text, turbopuffer.query)
- add comprehensive attributes to each span and log:
* query text visible in all operations
* top_k parameter tracking
* embedding dimensions
* vector search distance metrics (min/max)
* result quality metrics (count, top result, scores)
- remove noisy turbopuffer response debug log
- all child spans now properly nested under parent search span
this creates a clear hierarchy in logfire:
bufo_search (parent)
├─ voyage.embed_text
└─ turbopuffer.query
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
- fix turbopuffer QueryResponse to match actual API format (direct array,
not wrapped in {rows: ...})
- fix QueryRow to use non-optional dist field as returned by API
- correctly compute similarity scores from cosine distance: 1 - (dist / 2.0)
- remove placeholder score logic that was returning all 1.0s
- fix landing page link to use correct tangled.org URL
search results now show actual semantic similarity scores instead of
placeholder 1.0 values.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
- add comprehensive module documentation to search.rs explaining the
multimodal early fusion approach with references to voyage AI research
- update landing page to link "semantic search" directly to our search.rs
implementation on tangled
- remove outdated "multimodal hybrid search" link to turbopuffer docs
(no longer accurate since we removed BM25 hybrid search)
- simplify landing page header by removing the secondary info line
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
research findings:
- voyage-multimodal-3 uses unified transformer encoder for text + images
- 41.44% improvement on retrieval with combined modalities
- no "pollution" - this is the intended design for the model
changes:
1. ingestion script: prepend filename text to content array
- convert "bufo-jumping-on-bed.png" -> "bufo jumping on bed"
- send as {"type": "text"} + {"type": "image_base64"} in same request
- model creates single unified embedding capturing both modalities
2. search logic: simplify by removing BM25 and RRF fusion
- early fusion embeddings already contain semantic text meaning
- rely entirely on vector search with unified embeddings
- removed bm25_query method from turbopuffer client
- eliminated complex RRF score calculation
benefits:
- simpler codebase (removed ~80 lines of RRF fusion logic)
- better semantic understanding (text + visual unified)
- fewer api calls (no separate BM25 search)
- research-validated approach
next step: re-run ingestion to regenerate all embeddings
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
use logfire sdk's with_default_level_filter(LevelFilter::INFO) to exclude
trace and debug level spans from actix-http internals (timers, flags, etc).
only capture info-level spans like requests and our custom search spans.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
- replace env_logger with logfire for structured observability
- add opentelemetry middleware for automatic http request tracing
- instrument search operations with custom spans:
- embedding generation
- vector search
- bm25 search
- reciprocal rank fusion
- remove unused upsert method and struct from turbopuffer client
(ingestion script calls api directly, not used by main application)
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
- add width: 100%, min-width: 0, flex-shrink: 0 to prevent button overflow
- replace min-max normalization with absolute RRF score scaling
- scale by 25x so good matches approach 1.0, poor matches stay low
- this respects actual match quality instead of forcing top result to 95%
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
- add bufo favicon
- link to bufo.zone and turbopuffer hybrid search docs
- link to source code on tangled.sh
- mobile-first responsive layout (single column on mobile)
- larger images on mobile for better visibility
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
- multimodal image embeddings via voyage-multimodal-3
- hybrid search combining vector (ANN) + text (BM25) with RRF
- normalized similarity scores (0-95%) for intuitive UI display
- ingestion pipeline for bufo.zone images
- clean web interface with image grid and relevance scores
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>