// handleContent fetches the content of a document from the Bigtable and returns it. func handleContent(w http.ResponseWriter, r *http.Request, table *bigtable.Table) { ctx, _ := context.WithTimeout(context.Background(), 10*time.Second) name := r.FormValue("name") if len(name) == 0 { http.Error(w, "No document name supplied.", http.StatusBadRequest) return } row, err := table.ReadRow(ctx, name) if err != nil { http.Error(w, "Error reading content: "+err.Error(), http.StatusInternalServerError) return } content := row[contentColumnFamily] if len(content) == 0 { http.Error(w, "Document not found.", http.StatusNotFound) return } var buf bytes.Buffer if err := contentTemplate.ExecuteTemplate(&buf, "", struct{ Title, Content string }{name, string(content[0].Value)}); err != nil { http.Error(w, "Error executing HTML template: "+err.Error(), http.StatusInternalServerError) return } io.Copy(w, &buf) }
// handleAddDoc adds a document to the index. func handleAddDoc(w http.ResponseWriter, r *http.Request, table *bigtable.Table) { if r.Method != "POST" { http.Error(w, "POST requests only", http.StatusMethodNotAllowed) return } ctx, _ := context.WithTimeout(context.Background(), time.Minute) name := r.FormValue("name") if len(name) == 0 { http.Error(w, "Empty document name!", http.StatusBadRequest) return } content := r.FormValue("content") if len(content) == 0 { http.Error(w, "Empty document content!", http.StatusBadRequest) return } var ( writeErr error // Set if any write fails. mu sync.Mutex // Protects writeErr wg sync.WaitGroup // Used to wait for all writes to finish. ) // writeOneColumn writes one column in one row, updates err if there is an error, // and signals wg that one operation has finished. writeOneColumn := func(row, family, column, value string, ts bigtable.Timestamp) { mut := bigtable.NewMutation() mut.Set(family, column, ts, []byte(value)) err := table.Apply(ctx, row, mut) if err != nil { mu.Lock() writeErr = err mu.Unlock() } } // Start a write to store the document content. wg.Add(1) go func() { writeOneColumn(name, contentColumnFamily, "", content, bigtable.Now()) wg.Done() }() // Start writes to store the document name in the index for each word in the document. words := tokenize(content) for _, word := range words { var ( row = word family = indexColumnFamily column = name value = "" ts = bigtable.Now() ) wg.Add(1) go func() { // TODO: should use a semaphore to limit the number of concurrent writes. writeOneColumn(row, family, column, value, ts) wg.Done() }() } wg.Wait() if writeErr != nil { http.Error(w, "Error writing to Bigtable: "+writeErr.Error(), http.StatusInternalServerError) return } var buf bytes.Buffer if err := addTemplate.ExecuteTemplate(&buf, "", struct{ Title string }{name}); err != nil { http.Error(w, "Error executing HTML template: "+err.Error(), http.StatusInternalServerError) return } io.Copy(w, &buf) }
// handleSearch responds to search queries, returning links and snippets for matching documents. func handleSearch(w http.ResponseWriter, r *http.Request, table *bigtable.Table) { ctx, _ := context.WithTimeout(context.Background(), 10*time.Second) query := r.FormValue("q") // Split the query into words. words := tokenize(query) if len(words) == 0 { http.Error(w, "Empty query.", http.StatusBadRequest) return } // readRows reads from many rows concurrently. readRows := func(rows []string) ([]bigtable.Row, error) { results := make([]bigtable.Row, len(rows)) errors := make([]error, len(rows)) var wg sync.WaitGroup for i, row := range rows { wg.Add(1) go func(i int, row string) { defer wg.Done() results[i], errors[i] = table.ReadRow(ctx, row, bigtable.RowFilter(bigtable.LatestNFilter(1))) }(i, row) } wg.Wait() for _, err := range errors { if err != nil { return nil, err } } return results, nil } // For each query word, get the list of documents containing it. results, err := readRows(words) if err != nil { http.Error(w, "Error reading index: "+err.Error(), http.StatusInternalServerError) return } // Count how many of the query words each result contained. hits := make(map[string]int) for _, r := range results { for _, r := range r[indexColumnFamily] { hits[r.Column]++ } } // Build a slice of all the documents that matched every query word. var matches []string for doc, count := range hits { if count == len(words) { matches = append(matches, doc[len(indexColumnFamily+":"):]) } } // Fetch the content of those documents from the Bigtable. content, err := readRows(matches) if err != nil { http.Error(w, "Error reading results: "+err.Error(), http.StatusInternalServerError) return } type result struct{ Title, Snippet string } data := struct { Query string Results []result }{query, nil} // Output links and snippets. for i, doc := range matches { var text string c := content[i][contentColumnFamily] if len(c) > 0 { text = string(c[0].Value) } if len(text) > 100 { text = text[:100] + "..." } data.Results = append(data.Results, result{doc, text}) } var buf bytes.Buffer if err := searchTemplate.ExecuteTemplate(&buf, "", data); err != nil { http.Error(w, "Error executing HTML template: "+err.Error(), http.StatusInternalServerError) return } io.Copy(w, &buf) }