add fuzzy search

This commit is contained in:
2025-07-13 20:09:58 -07:00
parent 1d9c8fff73
commit 9b40c3bd8e
2 changed files with 188 additions and 7 deletions

View File

@@ -2,6 +2,7 @@ package app
import ( import (
"fmt" "fmt"
"sort"
"strings" "strings"
tea "github.com/charmbracelet/bubbletea" tea "github.com/charmbracelet/bubbletea"
@@ -176,15 +177,39 @@ func (m *TableDataModel) filterData() {
m.Shared.FilteredData = make([][]string, len(m.Shared.TableData)) m.Shared.FilteredData = make([][]string, len(m.Shared.TableData))
copy(m.Shared.FilteredData, m.Shared.TableData) copy(m.Shared.FilteredData, m.Shared.TableData)
} else { } else {
m.Shared.FilteredData = [][]string{} // Fuzzy search with scoring for rows
type rowMatch struct {
row []string
score int
}
var matches []rowMatch
searchLower := strings.ToLower(m.searchInput) searchLower := strings.ToLower(m.searchInput)
for _, row := range m.Shared.TableData { for _, row := range m.Shared.TableData {
bestScore := 0
// Check each cell in the row and take the best score
for _, cell := range row { for _, cell := range row {
if strings.Contains(strings.ToLower(cell), searchLower) { score := m.fuzzyScore(strings.ToLower(cell), searchLower)
m.Shared.FilteredData = append(m.Shared.FilteredData, row) if score > bestScore {
break bestScore = score
} }
} }
if bestScore > 0 {
matches = append(matches, rowMatch{row: row, score: bestScore})
}
}
// Sort by score (highest first)
sort.Slice(matches, func(i, j int) bool {
return matches[i].score > matches[j].score
})
// Extract sorted rows
m.Shared.FilteredData = make([][]string, len(matches))
for i, match := range matches {
m.Shared.FilteredData[i] = match.row
} }
} }
@@ -193,6 +218,74 @@ func (m *TableDataModel) filterData() {
} }
} }
// fuzzyScore calculates a fuzzy match score between text and pattern
// Returns 0 for no match, higher scores for better matches
func (m *TableDataModel) fuzzyScore(text, pattern string) int {
if pattern == "" {
return 1
}
textLen := len(text)
patternLen := len(pattern)
if patternLen > textLen {
return 0
}
// Exact match gets highest score
if text == pattern {
return 1000
}
// Prefix match gets high score
if strings.HasPrefix(text, pattern) {
return 900
}
// Contains match gets medium score
if strings.Contains(text, pattern) {
return 800
}
// Fuzzy character sequence matching
score := 0
textIdx := 0
patternIdx := 0
consecutiveMatches := 0
for textIdx < textLen && patternIdx < patternLen {
if text[textIdx] == pattern[patternIdx] {
score += 10
consecutiveMatches++
// Bonus for consecutive matches
if consecutiveMatches > 1 {
score += consecutiveMatches * 5
}
// Bonus for matches at word boundaries
if textIdx == 0 || text[textIdx-1] == '_' || text[textIdx-1] == '-' || text[textIdx-1] == ' ' {
score += 20
}
patternIdx++
} else {
consecutiveMatches = 0
}
textIdx++
}
// Must match all pattern characters
if patternIdx < patternLen {
return 0
}
// Bonus for shorter text (more precise match)
score += (100 - textLen)
return score
}
func (m *TableDataModel) View() string { func (m *TableDataModel) View() string {
var content strings.Builder var content strings.Builder

View File

@@ -2,6 +2,7 @@ package app
import ( import (
"fmt" "fmt"
"sort"
"strings" "strings"
tea "github.com/charmbracelet/bubbletea" tea "github.com/charmbracelet/bubbletea"
@@ -161,12 +162,31 @@ func (m *TableListModel) filterTables() {
m.Shared.FilteredTables = make([]string, len(m.Shared.Tables)) m.Shared.FilteredTables = make([]string, len(m.Shared.Tables))
copy(m.Shared.FilteredTables, m.Shared.Tables) copy(m.Shared.FilteredTables, m.Shared.Tables)
} else { } else {
m.Shared.FilteredTables = []string{} // Fuzzy search with scoring
searchLower := strings.ToLower(m.searchInput) type tableMatch struct {
for _, table := range m.Shared.Tables { name string
if strings.Contains(strings.ToLower(table), searchLower) { score int
m.Shared.FilteredTables = append(m.Shared.FilteredTables, table)
} }
var matches []tableMatch
searchLower := strings.ToLower(m.searchInput)
for _, table := range m.Shared.Tables {
score := m.fuzzyScore(strings.ToLower(table), searchLower)
if score > 0 {
matches = append(matches, tableMatch{name: table, score: score})
}
}
// Sort by score (highest first)
sort.Slice(matches, func(i, j int) bool {
return matches[i].score > matches[j].score
})
// Extract sorted table names
m.Shared.FilteredTables = make([]string, len(matches))
for i, match := range matches {
m.Shared.FilteredTables[i] = match.name
} }
} }
@@ -176,6 +196,74 @@ func (m *TableListModel) filterTables() {
} }
} }
// fuzzyScore calculates a fuzzy match score between text and pattern
// Returns 0 for no match, higher scores for better matches
func (m *TableListModel) fuzzyScore(text, pattern string) int {
if pattern == "" {
return 1
}
textLen := len(text)
patternLen := len(pattern)
if patternLen > textLen {
return 0
}
// Exact match gets highest score
if text == pattern {
return 1000
}
// Prefix match gets high score
if strings.HasPrefix(text, pattern) {
return 900
}
// Contains match gets medium score
if strings.Contains(text, pattern) {
return 800
}
// Fuzzy character sequence matching
score := 0
textIdx := 0
patternIdx := 0
consecutiveMatches := 0
for textIdx < textLen && patternIdx < patternLen {
if text[textIdx] == pattern[patternIdx] {
score += 10
consecutiveMatches++
// Bonus for consecutive matches
if consecutiveMatches > 1 {
score += consecutiveMatches * 5
}
// Bonus for matches at word boundaries
if textIdx == 0 || text[textIdx-1] == '_' || text[textIdx-1] == '-' {
score += 20
}
patternIdx++
} else {
consecutiveMatches = 0
}
textIdx++
}
// Must match all pattern characters
if patternIdx < patternLen {
return 0
}
// Bonus for shorter text (more precise match)
score += (100 - textLen)
return score
}
func (m *TableListModel) getVisibleCount() int { func (m *TableListModel) getVisibleCount() int {
reservedLines := 8 reservedLines := 8
if m.searching { if m.searching {