mirror of
https://github.com/warmbly/warmbly.git
synced 2026-10-04 16:02:03 +00:00
feat: preserve web-search operators and escape participant patterns in unibox search
This commit is contained in:
@@ -519,7 +519,7 @@ func (r *uniboxRepository) Search(ctx context.Context, orgID uuid.UUID, params *
|
||||
// declares it, or the index is not used.
|
||||
q := *params.Subject
|
||||
webPos, addrPos := argPos, argPos+1
|
||||
args = append(args, q, q)
|
||||
args = append(args, q, escapeLikePattern(q))
|
||||
argPos += 2
|
||||
|
||||
match := fmt.Sprintf(`ue.search_tsv @@ websearch_to_tsquery('english', $%d)
|
||||
@@ -536,9 +536,9 @@ func (r *uniboxRepository) Search(ctx context.Context, orgID uuid.UUID, params *
|
||||
// Participants. The stored header is "Display Name (addr)", so one
|
||||
// substring match covers searching by either.
|
||||
match += fmt.Sprintf(`
|
||||
OR EXISTS (SELECT 1 FROM unnest(ue.from_addr) AS s(addr) WHERE s.addr ILIKE '%%' || $%d || '%%')
|
||||
OR EXISTS (SELECT 1 FROM unnest(ue.to_addr) AS s(addr) WHERE s.addr ILIKE '%%' || $%d || '%%')
|
||||
OR EXISTS (SELECT 1 FROM unnest(ue.cc) AS s(addr) WHERE s.addr ILIKE '%%' || $%d || '%%')`,
|
||||
OR EXISTS (SELECT 1 FROM unnest(ue.from_addr) AS s(addr) WHERE s.addr ILIKE '%%' || $%d || '%%' ESCAPE '\')
|
||||
OR EXISTS (SELECT 1 FROM unnest(ue.to_addr) AS s(addr) WHERE s.addr ILIKE '%%' || $%d || '%%' ESCAPE '\')
|
||||
OR EXISTS (SELECT 1 FROM unnest(ue.cc) AS s(addr) WHERE s.addr ILIKE '%%' || $%d || '%%' ESCAPE '\')`,
|
||||
addrPos, addrPos, addrPos)
|
||||
|
||||
inner += ` AND (` + match + `)`
|
||||
@@ -1494,6 +1494,9 @@ func (r *uniboxRepository) scanGrounding(ctx context.Context, query string, orgI
|
||||
// so adding a word narrows rather than widens.
|
||||
func prefixTSQuery(input string) string {
|
||||
const maxTerms = 8
|
||||
if hasWebSearchSyntax(input) {
|
||||
return ""
|
||||
}
|
||||
|
||||
cleaned := strings.Map(func(r rune) rune {
|
||||
switch {
|
||||
@@ -1509,10 +1512,26 @@ func prefixTSQuery(input string) string {
|
||||
return ""
|
||||
}
|
||||
if len(terms) > maxTerms {
|
||||
terms = terms[:maxTerms]
|
||||
return ""
|
||||
}
|
||||
for i, t := range terms {
|
||||
terms[i] = t + ":*"
|
||||
}
|
||||
return strings.Join(terms, " & ")
|
||||
}
|
||||
|
||||
func hasWebSearchSyntax(input string) bool {
|
||||
if strings.Contains(input, `"`) {
|
||||
return true
|
||||
}
|
||||
for _, term := range strings.Fields(input) {
|
||||
if strings.HasPrefix(term, "-") || strings.EqualFold(term, "OR") {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func escapeLikePattern(input string) string {
|
||||
return strings.NewReplacer(`\`, `\\`, `%`, `\%`, `_`, `\_`).Replace(input)
|
||||
}
|
||||
|
||||
@@ -19,8 +19,10 @@ func TestPrefixTSQuery(t *testing.T) {
|
||||
// Every one of these makes to_tsquery raise when passed through raw.
|
||||
{"operators are stripped", "a & b | c", "a:* & b:* & c:*"},
|
||||
{"unbalanced bracket", "re: (urgent", "re:* & urgent:*"},
|
||||
{"negation and colons", "!foo:bar", "foo:* & bar:*"},
|
||||
{"quotes", `"exact phrase"`, "exact:* & phrase:*"},
|
||||
{"punctuation and colons", "!foo:bar", "foo:* & bar:*"},
|
||||
{"negation keeps web-search semantics", "foo -bar", ""},
|
||||
{"quotes keep web-search semantics", `"exact phrase"`, ""},
|
||||
{"OR keeps web-search semantics", "foo OR bar", ""},
|
||||
{"a lone operator has nothing to search", "&&&", ""},
|
||||
|
||||
{"empty", "", ""},
|
||||
@@ -37,11 +39,17 @@ func TestPrefixTSQuery(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// A pasted paragraph must not become a 200-term query the planner has to walk.
|
||||
// Long input stays on websearch_to_tsquery instead of being broadened by a truncated prefix query.
|
||||
func TestPrefixTSQueryCapsTerms(t *testing.T) {
|
||||
got := prefixTSQuery("one two three four five six seven eight nine ten eleven")
|
||||
want := "one:* & two:* & three:* & four:* & five:* & six:* & seven:* & eight:*"
|
||||
want := ""
|
||||
if got != want {
|
||||
t.Fatalf("prefixTSQuery capped = %q, want %q", got, want)
|
||||
t.Fatalf("prefixTSQuery long input = %q, want %q", got, want)
|
||||
}
|
||||
}
|
||||
|
||||
func TestEscapeLikePattern(t *testing.T) {
|
||||
if got, want := escapeLikePattern(`100%_done\later`), `100\%\_done\\later`; got != want {
|
||||
t.Fatalf("escapeLikePattern = %q, want %q", got, want)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -304,7 +304,7 @@ export function ConversationList({
|
||||
// The box searches people, subject and message body, and naming
|
||||
// that is the difference between it looking broken and looking
|
||||
// useful: nobody tries an address in a box labelled "Search inbox".
|
||||
placeholder={`Search ${scopeLabel.toLowerCase()} — name, address, or any word`}
|
||||
placeholder={`Search ${scopeLabel.toLowerCase()}: name, address, or any word`}
|
||||
title={'Searches the sender, recipients, subject and message body. "quoted phrases", OR and -exclude work.'}
|
||||
className="flex-1 min-w-0 h-full bg-transparent text-[12.5px] text-slate-900 placeholder:text-slate-400 outline-none"
|
||||
/>
|
||||
|
||||
Reference in New Issue
Block a user