使用Golang实现站点搜索功能有哪些方案?

使用Golang实现站点搜索功能有哪些方案? 几乎所有网站都有一个"搜索"按钮,用于在网站内搜索内容。

作为临时解决方案,我目前正在使用Google自定义搜索,但我想知道是否有一种使用Go实现此功能的简单方法。比如:

  1. 抓取所有HTML网站(是否已在使用Golang Web服务器时将其加载到内存中?)
  2. 根据表单输入搜索和分析标签内的内容。
  3. 将结果以列表形式呈现在网页上。

我已经进行了大量搜索,但一无所获。

对于如何寻找思路或具体方法,您有什么建议或线索吗?


更多关于使用Golang实现站点搜索功能有哪些方案?的实战教程也可以访问 https://www.itying.com/category-94-b0.html

11 回复

通常我会直接查询数据库,如果你正确设置了索引,即使是全文搜索也相当高效。

但一般来说,解决方案很大程度上取决于服务器本身的实现方式……如果你没有在数据库中存储某些内容,就无法真正查询它 😉

更多关于使用Golang实现站点搜索功能有哪些方案?的实战系列教程也可以访问 https://www.itying.com/category-94-b0.html


NobbZ:

通常人们想要搜索的是填充到模板中的动态数据,而不是围绕它的静态内容。

如果我没有理解错的话,我需要构建一个类似CMS的网站?

acim:

如果你有大量数据,可以考虑使用 ElasticSearch 或 Solr。还有两个 Golang 项目:

这对于大约 100 个 HTML 页面来说是不是太夸张了?

NobbZ:

但通常解决方案很大程度上取决于服务器本身的实现方式……如果你没有在数据库中存储某些内容,就无法真正查询它

有一堆HTML模板。可以将其视为包含两列的数据库:URL和文本。难道没有办法在HTML模板中进行搜索吗?

Sibert:

这对于大约100个HTML页面来说是不是有点小题大做了?

在这种情况下确实如此,但你也应该考虑结果的质量。不过根据你的情况,我会选择Bleeve或Riot,因为你的数据量较小。就像有人说的,你应该只索引内容,或者在索引前先去除HTML标签。

从长远来看,这种方法会增加维护难度。使用包含内容的数据库可能会更简单。但正如我之前所说,如果坚持当前的方法,您需要手动维护页面可搜索内容的索引,因为当您搜索"article"这个词时,可能不希望找到包含<article>标签的文章。

// 代码示例保留原样

嗯,我以为你已经构建了一些东西。

但我不知道你构建了什么以及它是如何工作的,因此我告诉你通常的做法。

你说你有"模板",这些模板本身通常不包含有用的可搜索数据,而是页面框架。内容来自其他地方。你仍然没有说明这个"其他地方"是什么。

// 代码示例保留原样
func main() {
    fmt.Println("hello world")
}

1 个赞

如果你有大量数据,可以考虑使用 ElasticSearch 或 Solr。还有两个 Golang 项目:

GitHub go-ego/riot

Go 开源、分布式、简单高效的搜索引擎 - go-ego/riot

GitHub blevesearch/bleve

一个现代化的 Go 文本索引库。通过在 GitHub 上创建账户来为 blevesearch/bleve 的开发做贡献。

Sibert:

有一堆HTML模板。

通常人们希望搜索填充到模板中的动态数据,而不是围绕它的静态内容。

所以从我的角度来看,搜索模板没有意义。但如果你真的想这样做,那么你需要为模板的可搜索区域构建一个索引,并使用该索引进行搜索。你可能需要研究数据库实现来正确处理这部分。

// 示例代码:构建模板索引
func buildTemplateIndex(templates []Template) map[string]int {
    index := make(map[string]int)
    for i, template := range templates {
        index[template.Name] = i
    }
    return index
}

NobbZ:

通常这些内容本身不包含有用的可搜索数据,而是页面骨架。实际内容来自其他地方。

嗯,你可以通过至少两种方式使用模板。数据驱动或"纯HTML"。我决定从"纯HTML"开始。所以我想我确实选择了一种传统方式。数据就在模板本身中。

<div class="container">

<div class="submenu">
 {{template "sub_form"}}
</div>

  <div class="content">
    <h1>Lorem ipsum</h1>
    <p>Lorem ipsum etc Lorem ipsum etc Lorem ipsum etc Lorem ipsum etc </p>

  <br><br>
</div></div>

在Golang中实现站点搜索功能有多种方案,以下是几种实用的方法:

方案一:使用Bleve全文搜索引擎

Bleve是Go语言中最流行的全文搜索引擎库,特别适合站内搜索:

package main

import (
    "fmt"
    "log"
    "net/http"
    "html/template"
    
    "github.com/blevesearch/bleve/v2"
)

var index bleve.Index

func init() {
    // 创建或打开索引
    mapping := bleve.NewIndexMapping()
    var err error
    index, err = bleve.New("site.bleve", mapping)
    if err != nil {
        index, err = bleve.Open("site.bleve")
        if err != nil {
            log.Fatal(err)
        }
    }
}

// 索引页面内容
func indexPage(id, title, content, url string) error {
    doc := map[string]interface{}{
        "title":   title,
        "content": content,
        "url":     url,
    }
    return index.Index(id, doc)
}

// 搜索处理
func searchHandler(w http.ResponseWriter, r *http.Request) {
    query := r.URL.Query().Get("q")
    if query == "" {
        http.ServeFile(w, r, "search.html")
        return
    }
    
    searchQuery := bleve.NewMatchQuery(query)
    search := bleve.NewSearchRequest(searchQuery)
    search.Fields = []string{"title", "content", "url"}
    searchResults, err := index.Search(search)
    if err != nil {
        http.Error(w, err.Error(), http.StatusInternalServerError)
        return
    }
    
    tmpl := template.Must(template.ParseFiles("results.html"))
    tmpl.Execute(w, searchResults)
}

func main() {
    http.HandleFunc("/search", searchHandler)
    http.ListenAndServe(":8080", nil)
}

对应的HTML模板示例:

<!-- search.html -->
<form action="/search" method="get">
    <input type="text" name="q" placeholder="搜索网站内容...">
    <button type="submit">搜索</button>
</form>

<!-- results.html -->
<h2>搜索结果 ({{.Total}})</h2>
{{range .Hits}}
<div class="result">
    <h3><a href="{{.Fields.url}}">{{.Fields.title}}</a></h3>
    <p>{{.Fields.content}}</p>
    <small>相关度: {{.Score}}</small>
</div>
{{end}}

方案二:使用GoQuery进行HTML解析和内存搜索

对于小型网站,可以使用内存中的简单搜索:

package main

import (
    "bytes"
    "fmt"
    "log"
    "net/http"
    "strings"
    "sync"
    
    "github.com/PuerkitoBio/goquery"
)

type Page struct {
    URL     string
    Title   string
    Content string
}

var (
    pages []Page
    mutex sync.RWMutex
)

// 抓取和解析页面
func crawlPage(url string) error {
    resp, err := http.Get(url)
    if err != nil {
        return err
    }
    defer resp.Body.Close()
    
    doc, err := goquery.NewDocumentFromReader(resp.Body)
    if err != nil {
        return err
    }
    
    title := doc.Find("title").Text()
    content := doc.Find("body").Text()
    
    // 清理内容,移除多余空白
    content = strings.Join(strings.Fields(content), " ")
    
    mutex.Lock()
    pages = append(pages, Page{
        URL:     url,
        Title:   title,
        Content: content,
    })
    mutex.Unlock()
    
    return nil
}

// 简单文本搜索
func searchPages(query string) []Page {
    var results []Page
    query = strings.ToLower(query)
    
    mutex.RLock()
    defer mutex.RUnlock()
    
    for _, page := range pages {
        if strings.Contains(strings.ToLower(page.Title), query) || 
           strings.Contains(strings.ToLower(page.Content), query) {
            results = append(results, page)
        }
    }
    return results
}

func searchHandler(w http.ResponseWriter, r *http.Request) {
    query := r.URL.Query().Get("q")
    if query == "" {
        fmt.Fprint(w, `
            <form method="get">
                <input type="text" name="q">
                <button type="submit">搜索</button>
            </form>
        `)
        return
    }
    
    results := searchPages(query)
    
    var buf bytes.Buffer
    buf.WriteString(fmt.Sprintf("<h2>找到 %d 个结果</h2>", len(results)))
    
    for _, result := range results {
        buf.WriteString(fmt.Sprintf(`
            <div style="margin: 20px 0;">
                <h3><a href="%s">%s</a></h3>
                <p>%s...</p>
            </div>
        `, result.URL, result.Title, truncateText(result.Content, 200)))
    }
    
    w.Header().Set("Content-Type", "text/html")
    w.Write(buf.Bytes())
}

func truncateText(text string, length int) string {
    if len(text) <= length {
        return text
    }
    return text[:length] + "..."
}

func main() {
    // 预先抓取网站页面
    urls := []string{
        "https://yoursite.com/page1",
        "https://yoursite.com/page2",
        // 添加更多URL
    }
    
    for _, url := range urls {
        go crawlPage(url)
    }
    
    http.HandleFunc("/search", searchHandler)
    log.Fatal(http.ListenAndServe(":8080", nil))
}

方案三:集成SQLite的FTS5扩展

对于需要持久化存储的场景:

package main

import (
    "database/sql"
    "fmt"
    "log"
    "net/http"
    
    _ "github.com/mattn/go-sqlite3"
)

func setupSearchDB() *sql.DB {
    db, err := sql.Open("sqlite3", "./search.db")
    if err != nil {
        log.Fatal(err)
    }
    
    // 创建FTS5虚拟表
    _, err = db.Exec(`
        CREATE VIRTUAL TABLE IF NOT EXISTS pages USING fts5(
            url, 
            title, 
            content,
            tokenize = 'porter unicode61'
        )
    `)
    if err != nil {
        log.Fatal(err)
    }
    
    return db
}

func searchSQLite(db *sql.DB, query string) ([]map[string]string, error) {
    rows, err := db.Query(`
        SELECT url, title, snippet(pages, 2, '<b>', '</b>', '...', 10) 
        FROM pages 
        WHERE pages MATCH ? 
        ORDER BY rank
    `, query)
    if err != nil {
        return nil, err
    }
    defer rows.Close()
    
    var results []map[string]string
    for rows.Next() {
        var url, title, snippet string
        if err := rows.Scan(&url, &title, &snippet); err != nil {
            return nil, err
        }
        results = append(results, map[string]string{
            "url":     url,
            "title":   title,
            "snippet": snippet,
        })
    }
    
    return results, nil
}

这些方案从简单到复杂,可以根据网站规模和性能需求选择合适的实现方式。Bleve方案提供了最完整的搜索功能,包括相关性排序和高亮显示,适合大多数应用场景。

回到顶部