scope/search/web/search.go

181 lines
3.7 KiB
Go

package web
import (
"net/http"
"sort"
"sync"
"time"
"golang.org/x/sync/errgroup"
)
func init() {
http.DefaultClient.Timeout = 5 * time.Second
}
// Result represents a search result
type Result struct {
Title string
Link string
Desc string
Engines []string
Rank int
}
// Engine represents a search engine for web results (not images, shopping, erc.)
type Engine interface {
// Set search keyword for engine
SetKeyword(string)
// Set User Agent. If string is empty,
// an acceptable will should be used.
SetUserAgent(string)
// Set page number to search
SetPage(int)
// Initialize engine (make requests, set variables, etc.)
Init() error
// Run function for each search result,
// inputting index
Each(func(int) error) error
// Get title from index given by Each()
Title(int) (string, error)
// Get link from index given by Each()
Link(int) (string, error)
// Get description from index given by Each()
Desc(int) (string, error)
// Return shortened name of search engine.
// Should be lowercase (e.g. google, ddg, bing)
Name() string
}
// Options represents search options
type Options struct {
Keyword string
UserAgent string
Page int
}
// Search searches the given engines concurrently and returns the results
func Search(opts Options, engines ...Engine) ([]*Result, error) {
var outMtx sync.Mutex
var out []*Result
// Create new error group
wg := errgroup.Group{}
// For every engine
for index, engine := range engines {
// Copy index and engine (for goroutine)
curIndex, curEngine := index, engine
wg.Go(func() error {
// Set options
curEngine.SetKeyword(opts.Keyword)
curEngine.SetUserAgent(opts.UserAgent)
curEngine.SetPage(opts.Page)
// Attempt to init engine
if err := curEngine.Init(); err != nil {
return err
}
// For each result
err := curEngine.Each(func(i int) error {
// Get result link
link, err := curEngine.Link(i)
if err != nil {
return err
}
// Calculate result rank
rank := (curIndex * 100) + i
// Check if result exists
index, exists := linkExists(out, link)
// If result already exists
if exists {
// Add engine to the existing result
out[index].Engines = append(out[index].Engines, curEngine.Name())
// If the rank is higher than the old one, update it
if rank < out[index].Rank {
out[index].Rank = rank
}
return nil
}
// Get result title
title, err := curEngine.Title(i)
if err != nil {
return err
}
// Get result description
desc, err := curEngine.Desc(i)
if err != nil {
return err
}
// If title, link, or description empty, ignore
if title == "" || link == "" || desc == "" {
return nil
}
// If length of description, truncate
if len(desc) > 500 {
desc = desc[:500] + "..."
}
// Create result struct
result := &Result{
Title: title,
Link: link,
Desc: desc,
Rank: rank,
Engines: []string{curEngine.Name()},
}
// Lock out mutex
outMtx.Lock()
// Add result to slice
out = append(out, result)
// Unlock out mutex
outMtx.Unlock()
return nil
})
if err != nil {
return err
}
// Sort slice by rank
sort.Slice(out, func(i, j int) bool {
return out[i].Rank < out[j].Rank
})
return nil
})
}
// Wait for error group
if err := wg.Wait(); err != nil {
return out, err
}
return out, nil
}
// linkExists checks if a link exists in the results
func linkExists(results []*Result, link string) (int, bool) {
// For every result
for index, result := range results {
// If link is the same as provided
if result.Link == link {
// Return index with true
return index, true
}
}
return -1, false
}