224 lines
6.0 KiB
Go
224 lines
6.0 KiB
Go
package meilisearch
|
|
|
|
import (
|
|
"bytes"
|
|
"encoding/json"
|
|
"fmt"
|
|
"io"
|
|
"net/http"
|
|
"time"
|
|
|
|
"github.com/felipemarinho97/torrent-indexer/schema"
|
|
)
|
|
|
|
// SearchIndexer integrates with Meilisearch to index and search torrent items.
|
|
type SearchIndexer struct {
|
|
Client *http.Client
|
|
BaseURL string
|
|
APIKey string
|
|
IndexName string
|
|
}
|
|
|
|
// IndexStats represents statistics about the Meilisearch index
|
|
type IndexStats struct {
|
|
NumberOfDocuments int64 `json:"numberOfDocuments"`
|
|
IsIndexing bool `json:"isIndexing"`
|
|
FieldDistribution map[string]int64 `json:"fieldDistribution"`
|
|
}
|
|
|
|
// HealthStatus represents the health status of Meilisearch
|
|
type HealthStatus struct {
|
|
Status string `json:"status"`
|
|
}
|
|
|
|
// NewSearchIndexer creates a new instance of SearchIndexer.
|
|
func NewSearchIndexer(baseURL, apiKey, indexName string) *SearchIndexer {
|
|
return &SearchIndexer{
|
|
Client: &http.Client{Timeout: 10 * time.Second},
|
|
BaseURL: baseURL,
|
|
APIKey: apiKey,
|
|
IndexName: indexName,
|
|
}
|
|
}
|
|
|
|
// IndexTorrent indexes a single torrent item in Meilisearch.
|
|
func (t *SearchIndexer) IndexTorrent(torrent schema.IndexedTorrent) error {
|
|
url := fmt.Sprintf("%s/indexes/%s/documents", t.BaseURL, t.IndexName)
|
|
|
|
torrentWithKey := struct {
|
|
Hash string `json:"id"`
|
|
schema.IndexedTorrent
|
|
}{
|
|
Hash: torrent.InfoHash,
|
|
IndexedTorrent: torrent,
|
|
}
|
|
|
|
jsonData, err := json.Marshal(torrentWithKey)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to marshal torrent data: %w", err)
|
|
}
|
|
|
|
req, err := http.NewRequest("POST", url, bytes.NewBuffer(jsonData))
|
|
if err != nil {
|
|
return fmt.Errorf("failed to create request: %w", err)
|
|
}
|
|
req.Header.Set("Content-Type", "application/json")
|
|
if t.APIKey != "" {
|
|
req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", t.APIKey))
|
|
}
|
|
|
|
resp, err := t.Client.Do(req)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to execute request: %w", err)
|
|
}
|
|
defer resp.Body.Close()
|
|
|
|
return nil
|
|
}
|
|
|
|
func (t *SearchIndexer) IndexTorrents(torrents []schema.IndexedTorrent) error {
|
|
url := fmt.Sprintf("%s/indexes/%s/documents", t.BaseURL, t.IndexName)
|
|
|
|
torrentsWithKey := make([]struct {
|
|
Hash string `json:"id"`
|
|
schema.IndexedTorrent
|
|
}, 0, len(torrents))
|
|
for _, torrent := range torrents {
|
|
torrentWithKey := struct {
|
|
Hash string `json:"id"`
|
|
schema.IndexedTorrent
|
|
}{
|
|
Hash: torrent.InfoHash,
|
|
IndexedTorrent: torrent,
|
|
}
|
|
torrentsWithKey = append(torrentsWithKey, torrentWithKey)
|
|
}
|
|
|
|
jsonData, err := json.Marshal(torrentsWithKey)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to marshal torrent data: %w", err)
|
|
}
|
|
|
|
req, err := http.NewRequest("POST", url, bytes.NewBuffer(jsonData))
|
|
if err != nil {
|
|
return fmt.Errorf("failed to create request: %w", err)
|
|
}
|
|
req.Header.Set("Content-Type", "application/json")
|
|
if t.APIKey != "" {
|
|
req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", t.APIKey))
|
|
}
|
|
|
|
resp, err := t.Client.Do(req)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to execute request: %w", err)
|
|
}
|
|
defer resp.Body.Close()
|
|
|
|
return nil
|
|
}
|
|
|
|
// SearchTorrent searches indexed torrents in Meilisearch based on the query.
|
|
func (t *SearchIndexer) SearchTorrent(query string, limit int) ([]schema.IndexedTorrent, error) {
|
|
url := fmt.Sprintf("%s/indexes/%s/search", t.BaseURL, t.IndexName)
|
|
requestBody := map[string]string{
|
|
"q": query,
|
|
}
|
|
jsonData, err := json.Marshal(requestBody)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to marshal search query: %w", err)
|
|
}
|
|
|
|
req, err := http.NewRequest("POST", url, bytes.NewBuffer(jsonData))
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to create request: %w", err)
|
|
}
|
|
req.Header.Set("Content-Type", "application/json")
|
|
if t.APIKey != "" {
|
|
req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", t.APIKey))
|
|
}
|
|
|
|
resp, err := t.Client.Do(req)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to execute request: %w", err)
|
|
}
|
|
defer resp.Body.Close()
|
|
|
|
if resp.StatusCode != http.StatusOK {
|
|
body, _ := io.ReadAll(resp.Body)
|
|
return nil, fmt.Errorf("search failed: %s", body)
|
|
}
|
|
|
|
var result struct {
|
|
Hits []schema.IndexedTorrent `json:"hits"`
|
|
}
|
|
if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
|
|
return nil, fmt.Errorf("failed to parse search response: %w", err)
|
|
}
|
|
|
|
return result.Hits, nil
|
|
}
|
|
|
|
// GetStats retrieves statistics about the Meilisearch index including document count.
|
|
// This method can be used for health checks and monitoring.
|
|
func (t *SearchIndexer) GetStats() (*IndexStats, error) {
|
|
url := fmt.Sprintf("%s/indexes/%s/stats", t.BaseURL, t.IndexName)
|
|
|
|
req, err := http.NewRequest("GET", url, nil)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to create request: %w", err)
|
|
}
|
|
|
|
if t.APIKey != "" {
|
|
req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", t.APIKey))
|
|
}
|
|
|
|
resp, err := t.Client.Do(req)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to execute request: %w", err)
|
|
}
|
|
defer resp.Body.Close()
|
|
|
|
if resp.StatusCode != http.StatusOK {
|
|
body, _ := io.ReadAll(resp.Body)
|
|
return nil, fmt.Errorf("failed to get stats: status %d, body: %s", resp.StatusCode, body)
|
|
}
|
|
|
|
var stats IndexStats
|
|
if err := json.NewDecoder(resp.Body).Decode(&stats); err != nil {
|
|
return nil, fmt.Errorf("failed to parse stats response: %w", err)
|
|
}
|
|
|
|
return &stats, nil
|
|
}
|
|
|
|
// IsHealthy checks if Meilisearch is available and responsive.
|
|
// Returns true if the service is healthy, false otherwise.
|
|
func (t *SearchIndexer) IsHealthy() bool {
|
|
url := fmt.Sprintf("%s/health", t.BaseURL)
|
|
|
|
req, err := http.NewRequest("GET", url, nil)
|
|
if err != nil {
|
|
return false
|
|
}
|
|
|
|
// Use a shorter timeout for health checks
|
|
client := &http.Client{Timeout: 5 * time.Second}
|
|
resp, err := client.Do(req)
|
|
if err != nil {
|
|
return false
|
|
}
|
|
defer resp.Body.Close()
|
|
|
|
return resp.StatusCode == http.StatusOK
|
|
}
|
|
|
|
// GetDocumentCount returns the number of indexed documents.
|
|
// This is a convenience method that extracts just the document count from stats.
|
|
func (t *SearchIndexer) GetDocumentCount() (int64, error) {
|
|
stats, err := t.GetStats()
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
return stats.NumberOfDocuments, nil
|
|
}
|