feat(search): нечёткий поиск по сообщениям — по началу слова и по подстроке
Поиск требовал ввести слово целиком. Теперь: - FTS5 ищет по началу слова: «пр» находит «Привет» (последнее слово запроса получает префикс), регистр не важен; - добавлена нормализованная копия текста (`messages.content_lower`, миграция 00007): подстрочные совпадения ищутся через LIKE без учёта регистра, включая кириллицу (SQLite lower() её не приводит — регистр опускает приложение); - при старте индекс дополняется у старых сообщений (порциями по 500); - результаты FTS и LIKE объединяются без дублей, сортировка по новизне; - тест: «пр» и «ивет» находят все три сообщения с «Привет/приветствие/ПРИВЕТ», посторонний текст не попадает.
This commit is contained in:
@@ -7,6 +7,7 @@ import (
|
||||
"mime/multipart"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"net/url"
|
||||
"testing"
|
||||
|
||||
"glchat/internal/permissions"
|
||||
@@ -568,3 +569,56 @@ func TestMentionsIncrementReadState(t *testing.T) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TestFuzzySearch проверяет нечёткий поиск: по началу слова, по подстроке и
|
||||
// без учёта регистра (запрос пользователя).
|
||||
func TestFuzzySearch(t *testing.T) {
|
||||
f := newMessagingFixture(t)
|
||||
for _, text := range []string{"Привет, мир", "приветствие другу", "ПРИВЕТ ВСЕМ"} {
|
||||
if rec := doJSON(t, f.srv, http.MethodPost, "/api/v1/channels/"+f.openChannel+"/messages",
|
||||
`{"content":"`+text+`"}`, f.ownerCookie); rec.Code != http.StatusOK {
|
||||
t.Fatalf("send %q = %d, body = %s", text, rec.Code, rec.Body.String())
|
||||
}
|
||||
}
|
||||
if rec := doJSON(t, f.srv, http.MethodPost, "/api/v1/channels/"+f.openChannel+"/messages",
|
||||
`{"content":"совсем другое"}`, f.ownerCookie); rec.Code != http.StatusOK {
|
||||
t.Fatalf("send other = %d", rec.Code)
|
||||
}
|
||||
|
||||
search := func(query string) []string {
|
||||
t.Helper()
|
||||
rec := doJSON(t, f.srv, http.MethodGet,
|
||||
"/api/v1/channels/"+f.openChannel+"/messages/search?q="+url.QueryEscape(query), "", f.memberCookie)
|
||||
if rec.Code != http.StatusOK {
|
||||
t.Fatalf("search %q = %d, body = %s", query, rec.Code, rec.Body.String())
|
||||
}
|
||||
payload := decodeResponse[struct {
|
||||
Messages []struct {
|
||||
Content string `json:"content"`
|
||||
} `json:"messages"`
|
||||
}](t, rec)
|
||||
contents := make([]string, 0, len(payload.Messages))
|
||||
for _, message := range payload.Messages {
|
||||
contents = append(contents, message.Content)
|
||||
}
|
||||
return contents
|
||||
}
|
||||
|
||||
// По началу слова в нижнем регистре: «пр» должно найти все три сообщения.
|
||||
prefix := search("пр")
|
||||
if len(prefix) != 3 {
|
||||
t.Fatalf("поиск «пр» вернул %d результатов: %v", len(prefix), prefix)
|
||||
}
|
||||
// Подстрока в середине слова тоже находится.
|
||||
substring := search("ивет")
|
||||
if len(substring) != 3 {
|
||||
t.Fatalf("поиск «ивет» вернул %d результатов: %v", len(substring), substring)
|
||||
}
|
||||
// Посторонний текст не попадает в выдачу.
|
||||
if results := search("привет всем"); len(results) != 1 {
|
||||
t.Fatalf("поиск по фразе вернул %d результатов: %v", len(results), results)
|
||||
}
|
||||
if results := search("другое"); len(results) != 1 {
|
||||
t.Fatalf("поиск «другое» вернул %d результатов: %v", len(results), results)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user