From eb84c1f90f923aaaa6505e963e41ad6ce4f66c2a Mon Sep 17 00:00:00 2001 From: Daniel <845765@qq.com> Date: Sat, 21 Sep 2024 17:29:01 +0800 Subject: [PATCH] :art: Highlight regular expression search results https://github.com/siyuan-note/siyuan/issues/11112 --- kernel/model/file.go | 8 ++++-- kernel/model/search.go | 22 ++++++++++++++--- kernel/sql/block_query.go | 52 +++++++++++++++++++++++++++++++++++++++ 3 files changed, 77 insertions(+), 5 deletions(-) diff --git a/kernel/model/file.go b/kernel/model/file.go index 320eba633..c316786f1 100644 --- a/kernel/model/file.go +++ b/kernel/model/file.go @@ -804,12 +804,16 @@ func GetDoc(startID, endID, id string, index int, query string, queryTypes map[s subTree := &parse.Tree{ID: rootID, Root: &ast.Node{Type: ast.NodeDocument}, Marks: tree.Marks} var keywords []string - if "" != query && (0 == queryMethod || 1 == queryMethod) { // 只有关键字搜索和查询语法搜索才支持高亮 + if "" != query && (0 == queryMethod || 1 == queryMethod || 3 == queryMethod) { // 只有关键字、查询语法和正则表达式搜索支持高亮 if 0 == queryMethod { query = stringQuery(query) } typeFilter := buildTypeFilter(queryTypes) - keywords = highlightByQuery(query, typeFilter, rootID) + if 0 == queryMethod || 1 == queryMethod { + keywords = highlightByFTS(query, typeFilter, rootID) + } else { + keywords = highlightByRegexp(query, typeFilter, rootID) + } } for _, n := range nodes { diff --git a/kernel/model/search.go b/kernel/model/search.go index 7a0949ae1..5a60cff96 100644 --- a/kernel/model/search.go +++ b/kernel/model/search.go @@ -1290,8 +1290,8 @@ func fullTextSearchByRegexp(exp, boxFilter, pathFilter, typeFilter, orderBy stri stmt := "SELECT * FROM `blocks` WHERE " + fieldFilter + " AND type IN " + typeFilter stmt += boxFilter + pathFilter stmt += " " + orderBy - stmt += " LIMIT " + strconv.Itoa(pageSize) + " OFFSET " + strconv.Itoa((page-1)*pageSize) - blocks := sql.SelectBlocksRawStmtNoParse(stmt, Conf.Search.Limit) + regex := regexp.MustCompile(exp) + blocks := sql.SelectBlocksRegex(stmt, regex, Conf.Search.Name, Conf.Search.Alias, Conf.Search.Memo, Conf.Search.IAL, page, pageSize) ret = fromSQLBlocks(&blocks, "", beforeLen) if 1 > len(ret) { ret = []*Block{} @@ -1354,7 +1354,7 @@ func fullTextSearchByFTS(query, boxFilter, pathFilter, typeFilter, orderBy strin return } -func highlightByQuery(query, typeFilter, id string) (ret []string) { +func highlightByFTS(query, typeFilter, id string) (ret []string) { const limit = 256 table := "blocks_fts" if !Conf.Search.CaseSensitive { @@ -1383,6 +1383,22 @@ func highlightByQuery(query, typeFilter, id string) (ret []string) { return } +func highlightByRegexp(query, typeFilter, id string) (ret []string) { + fieldFilter := fieldRegexp(query) + stmt := "SELECT * FROM `blocks` WHERE " + fieldFilter + " AND type IN " + typeFilter + stmt += " AND root_id = '" + id + "'" + regex := regexp.MustCompile(query) + sqlBlocks := sql.SelectBlocksRegex(stmt, regex, Conf.Search.Name, Conf.Search.Alias, Conf.Search.Memo, Conf.Search.IAL, 1, 256) + for _, block := range sqlBlocks { + keyword := gulu.Str.SubstringsBetween(block.Content, search.SearchMarkLeft, search.SearchMarkRight) + if 0 < len(keyword) { + ret = append(ret, keyword...) + } + } + ret = gulu.Str.RemoveDuplicatedElem(ret) + return +} + func fullTextSearchCount(query, boxFilter, pathFilter, typeFilter string) (matchedBlockCount, matchedRootCount int) { query = filterQueryInvisibleChars(query) if ast.IsNodeIDPattern(query) { diff --git a/kernel/sql/block_query.go b/kernel/sql/block_query.go index e6d461f0b..d267395c5 100644 --- a/kernel/sql/block_query.go +++ b/kernel/sql/block_query.go @@ -20,6 +20,7 @@ import ( "bytes" "database/sql" "math" + "regexp" "sort" "strconv" "strings" @@ -619,6 +620,57 @@ func SelectBlocksRawStmt(stmt string, page, limit int) (ret []*Block) { return } +func SelectBlocksRegex(stmt string, exp *regexp.Regexp, name, alias, memo, ial bool, page, pageSize int) (ret []*Block) { + rows, err := query(stmt) + if err != nil { + logging.LogErrorf("sql query [%s] failed: %s", stmt, err) + return + } + defer rows.Close() + count := 0 + for rows.Next() { + count++ + if count <= (page-1)*pageSize { + continue + } + + var block Block + if err := rows.Scan(&block.ID, &block.ParentID, &block.RootID, &block.Hash, &block.Box, &block.Path, &block.HPath, &block.Name, &block.Alias, &block.Memo, &block.Tag, &block.Content, &block.FContent, &block.Markdown, &block.Length, &block.Type, &block.SubType, &block.IAL, &block.Sort, &block.Created, &block.Updated); err != nil { + logging.LogErrorf("query scan field failed: %s\n%s", err, logging.ShortStack()) + return + } + + hitContent := exp.MatchString(block.Content) + hitName := name && exp.MatchString(block.Name) + hitAlias := alias && exp.MatchString(block.Alias) + hitMemo := memo && exp.MatchString(block.Memo) + hitIAL := ial && exp.MatchString(block.IAL) + if hitContent || hitName || hitAlias || hitMemo || hitIAL { + if hitContent { + block.Content = exp.ReplaceAllString(block.Content, "__@mark__${0}__mark@__") + } + if hitName { + block.Name = exp.ReplaceAllString(block.Name, "__@mark__${0}__mark@__") + } + if hitAlias { + block.Alias = exp.ReplaceAllString(block.Alias, "__@mark__${0}__mark@__") + } + if hitMemo { + block.Memo = exp.ReplaceAllString(block.Memo, "__@mark__${0}__mark@__") + } + if hitIAL { + block.IAL = exp.ReplaceAllString(block.IAL, "__@mark__${0}__mark@__") + } + + ret = append(ret, &block) + if len(ret) >= pageSize { + break + } + } + } + return +} + func selectBlocksRawStmt(stmt string, limit int) (ret []*Block) { rows, err := query(stmt) if err != nil {