-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathsearch_engine.go
More file actions
228 lines (208 loc) · 5.98 KB
/
Copy pathsearch_engine.go
File metadata and controls
228 lines (208 loc) · 5.98 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
package main
import (
"fmt"
"math"
"net/http"
"net/url"
"regexp"
"sort"
"time"
)
type Ranking struct {
title string
url string
score float64
}
type SearchEngine struct {
data_handler DataHandler
stop_words map[string]struct{}
doc_num uint
}
// Constructs a search engine for a local corpus and starts a server with a search page containing the corpus
func NewLocalSearchEngine(
data_handler DataHandler,
stop_words map[string]struct{},
default_delay float32,
crawl_workers struct {
clean_href int
download int
extract int
},
server_dir string,
port string,
relative_start_url string,
verbose bool,
) (*SearchEngine, error) {
// Create search engine
this := &SearchEngine{
data_handler: data_handler,
stop_words: stop_words,
doc_num: 0,
}
// Start file server, get url, and wait to start before crawling
main_url := this.StartServer(server_dir, port)
start_url, err := url.JoinPath(main_url, relative_start_url)
if err != nil {
return nil, err
}
time.Sleep(500 * time.Millisecond)
// Crawl corpus
if verbose {
fmt.Printf("Starting crawl of %s.\n", start_url)
}
crawler := NewCrawler(this.data_handler, this.stop_words, default_delay)
download_errors, err := crawler.Crawl(start_url, crawl_workers)
if err != nil {
return nil, err
}
// Set document count
this.doc_num, err = this.data_handler.Size()
if err != nil {
return nil, err
}
if verbose {
fmt.Printf("Crawl of %d documents complete with %d download errors.\n", this.doc_num, download_errors)
}
return this, nil
}
// Constructs a search engine for a preexisting corpus. Does not start a server with a search page
func NewSearchEngine(
data_handler DataHandler,
stop_words map[string]struct{},
default_delay float32,
crawl_workers struct {
clean_href int
download int
extract int
},
start_url string,
verbose bool,
) (*SearchEngine, error) {
// Create search engine
this := &SearchEngine{
data_handler: data_handler,
stop_words: stop_words,
doc_num: 0,
}
// Crawl corpus
if verbose {
fmt.Printf("Starting crawl of %s.\n", start_url)
}
crawler := NewCrawler(data_handler, stop_words, default_delay)
t_start := time.Now()
download_errors, err := crawler.Crawl(start_url, crawl_workers)
delta_t := time.Since(t_start)
if err != nil {
return nil, err
}
// Set document count
this.doc_num, err = this.data_handler.Size()
if err != nil {
return nil, err
}
if verbose {
fmt.Printf(
"Crawl of %d documents complete with %d download errors. [%s]\n",
this.doc_num,
download_errors,
delta_t.Round(time.Millisecond),
)
}
return this, nil
}
// Starts a server with a search page for the contents of the search engine.
func (se *SearchEngine) StartServer(server_dir string, port string) string {
// Create file server
mux := http.NewServeMux()
mux.Handle("/", http.FileServer(http.Dir(server_dir)))
// Create search page
mux.Handle("/search", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// Get user inputted query, then find results. Abort if database fails
query := r.URL.Query().Get("query")
results, err := se.Rank(query)
if err != nil {
fmt.Println("Error: database query failed")
return
}
// Create header
w.Header().Set("Content-Type", "text/html; charset=utf-8")
fmt.Fprintf(w, "<html><body>")
fmt.Fprintf(w, "<h3>Search term: %s</h3>", query)
// Create search box
fmt.Fprintf(w, "<form action=\"/search\" method=\"GET\">")
fmt.Fprintf(w, "<input type=\"text\" name=\"query\" placeholder=\"Search...\">")
fmt.Fprintf(w, "<button type=\"submit\">Search</button>")
fmt.Fprintf(w, "</form>")
// List results
fmt.Fprintf(w, "<ol>")
for _, ranking := range results {
fmt.Fprintf(w, "<li><a href=\"%s\">%s</a></li>\n", ranking.url, ranking.title)
}
fmt.Fprintf(w, "</ol>")
fmt.Fprintf(w, "</body></html>")
}))
go http.ListenAndServe(":"+port, mux)
return "http://localhost:" + port + "/"
}
// Returns a sorted slice of rankings based on TfIdf scores in relation to a multi-word query
func (se *SearchEngine) Rank(query string) ([]Ranking, error) {
// Clean query
word_re := regexp.MustCompile(`[a-zA-Z]+(['’][a-zA-Z]+)*`)
q_words := NormalizeWords(word_re.FindAllString(query, -1))
cleaned_query := []string{}
for _, q_word := range q_words {
if _, ok := se.stop_words[q_word]; !ok {
cleaned_query = append(cleaned_query, q_word)
}
}
// Create TfIdf score map
unsorted_ranking_map := make(map[string]Ranking)
for _, search_term := range cleaned_query {
docs, err := se.Search(search_term)
if err != nil {
return nil, err
}
docs_with_word := uint(len(docs))
for url, doc := range docs {
score := se.TfIdf(search_term, url, doc.freq, doc.word_count, docs_with_word)
ranking, ok := unsorted_ranking_map[url]
if !ok {
unsorted_ranking_map[url] = Ranking{doc.title, url, score}
} else {
unsorted_ranking_map[url] = Ranking{doc.title, url, ranking.score + score}
}
}
}
// Convert map to slice
rankings := []Ranking{}
for _, ranking := range unsorted_ranking_map {
rankings = append(rankings, ranking)
}
// Sort slice by score
sort.Slice(rankings, func(a, b int) bool {
if rankings[a].score == rankings[b].score {
return rankings[a].url > rankings[b].url
}
return rankings[a].score > rankings[b].score
})
return rankings, nil
}
// Returns a frequency map linking documents to frequency of the given search term
func (se *SearchEngine) Search(search_term string) (map[string]struct {
title string
word_count uint
freq uint
}, error) {
clean_search_term := NormalizeWords([]string{search_term})[0]
doc_map, err := se.data_handler.GetDocsWithWord(clean_search_term)
if err != nil {
return nil, err
}
return doc_map, nil
}
// Returns a TfIdf score for a given url and search term
func (se *SearchEngine) TfIdf(search_term string, url string, freq uint, word_count uint, docs_with_word uint) float64 {
tf := float64(freq) / float64(word_count+1)
idf := math.Log(float64(se.doc_num) / (float64(docs_with_word + 1)))
return tf * idf
}