package engines import ( "context" "errors" "fmt" "io" "net/http" "net/url" "strings" "github.com/ashie/gosearch/internal/contracts" ) // BingEngine searches Bing via the public search endpoint. // Uses Bing's web search results page and extracts results from the HTML. type BingEngine struct { client *http.Client } func (e *BingEngine) Name() string { return "bing" } func (e *BingEngine) Search(ctx context.Context, req contracts.SearchRequest) (contracts.SearchResponse, error) { if strings.TrimSpace(req.Query) == "" { return contracts.SearchResponse{Query: req.Query}, nil } if e == nil || e.client == nil { return contracts.SearchResponse{}, errors.New("bing engine not initialized") } endpoint := fmt.Sprintf( "https://www.bing.com/search?q=%s&count=10&offset=%d", url.QueryEscape(req.Query), (req.Pageno-1)*10, ) httpReq, err := http.NewRequestWithContext(ctx, http.MethodGet, endpoint, nil) if err != nil { return contracts.SearchResponse{}, err } httpReq.Header.Set("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36") resp, err := e.client.Do(httpReq) if err != nil { return contracts.SearchResponse{}, err } defer resp.Body.Close() if resp.StatusCode != http.StatusOK { body, _ := io.ReadAll(io.LimitReader(resp.Body, 4096)) return contracts.SearchResponse{}, fmt.Errorf("bing upstream error: status=%d body=%q", resp.StatusCode, string(body)) } results, err := parseBingHTML(resp.Body) if err != nil { return contracts.SearchResponse{}, err } return contracts.SearchResponse{ Query: req.Query, NumberOfResults: len(results), Results: results, Answers: []map[string]any{}, Corrections: []string{}, Infoboxes: []map[string]any{}, Suggestions: []string{}, UnresponsiveEngines: [][2]string{}, }, nil } // parseBingHTML extracts search results from Bing's HTML response. // Bing results are in
or
or
snippet := extractBingSnippet(block) results = append(results, contracts.MainResult{ Template: "default.html", Title: title, Content: snippet, URL: &href, Engine: "bing", Score: 0, Category: "general", Engines: []string{"bing"}, }) } return results, nil } func extractBingLink(block string) (title, href string) { // Find hrefStart := strings.Index(block, `href="`) if hrefStart == -1 { return "", "" } hrefStart += 6 hrefEnd := strings.Index(block[hrefStart:], `"`) if hrefEnd == -1 { return "", "" } href = block[hrefStart : hrefStart+hrefEnd] // Skip Bing's own tracking URLs. if strings.Contains(href, "bing.com") && strings.Contains(href, "search?") { // Try to extract the real URL from u= parameter. if uIdx := strings.Index(href, "&u="); uIdx != -1 { encodedURL := href[uIdx+3:] if decoded, err := url.QueryUnescape(encodedURL); err == nil { href = decoded } } } // Title is between > and after the href. titleStart := strings.Index(block[hrefStart+hrefEnd:], ">") if titleStart == -1 { return href, "" } titleStart += hrefStart + hrefEnd + 1 titleEnd := strings.Index(block[titleStart:], "") if titleEnd == -1 { return href, "" } title = stripHTML(block[titleStart : titleStart+titleEnd]) title = strings.TrimSpace(title) return title, href } func extractBingSnippet(block string) string { // Try
first. if idx := strings.Index(block, `class="b_caption"`); idx != -1 { caption := block[idx:] if pStart := strings.Index(caption, "
"); pEnd != -1 { return stripHTML(snippet[:pEnd+4]) } } } // Fallback: any
tag. if pStart := strings.Index(block, "
"); pEnd != -1 { return stripHTML(snippet[:pEnd+4]) } } return "" }