fork of https://github.com/sourcegraph/zoekt
1package json
2
3import (
4 "context"
5 "encoding/json"
6 "net/http"
7 "time"
8
9 "github.com/sourcegraph/zoekt"
10 "github.com/sourcegraph/zoekt/query"
11)
12
13// defaultTimeout is the maximum amount of time a search request should
14// take. This is the same default used by Sourcegraph.
15const defaultTimeout = 20 * time.Second
16
17func JSONServer(searcher zoekt.Searcher) http.Handler {
18 s := jsonSearcher{searcher}
19 mux := http.NewServeMux()
20 mux.HandleFunc("/search", s.jsonSearch)
21 mux.HandleFunc("/list", s.jsonList)
22 return mux
23}
24
25type jsonSearcher struct {
26 Searcher zoekt.Searcher
27}
28
29type jsonSearchArgs struct {
30 Q string
31 RepoIDs *[]uint32
32 Opts *zoekt.SearchOptions
33}
34
35type jsonSearchReply struct {
36 Result *zoekt.SearchResult
37}
38
39type jsonListArgs struct {
40 Q string
41 Opts *zoekt.ListOptions
42}
43
44type jsonListReply struct {
45 List *zoekt.RepoList
46}
47
48func (s *jsonSearcher) jsonSearch(w http.ResponseWriter, req *http.Request) {
49 ctx := req.Context()
50 w.Header().Add("Content-Type", "application/json")
51
52 if req.Method != "POST" {
53 jsonError(w, http.StatusMethodNotAllowed, "Only POST is supported")
54 return
55 }
56
57 searchArgs := jsonSearchArgs{}
58 err := json.NewDecoder(req.Body).Decode(&searchArgs)
59 if err != nil {
60 jsonError(w, http.StatusBadRequest, err.Error())
61 return
62 }
63 if searchArgs.Q == "" {
64 jsonError(w, http.StatusBadRequest, "missing query")
65 return
66 }
67 if searchArgs.Opts == nil {
68 searchArgs.Opts = &zoekt.SearchOptions{}
69 }
70
71 q, err := query.Parse(searchArgs.Q)
72 if err != nil {
73 jsonError(w, http.StatusBadRequest, err.Error())
74 return
75 }
76
77 if searchArgs.RepoIDs != nil {
78 q = query.NewAnd(q, query.NewRepoIDs(*searchArgs.RepoIDs...))
79 }
80
81 // Set a timeout if the user hasn't specified one.
82 if searchArgs.Opts.MaxWallTime == 0 {
83 var cancel context.CancelFunc
84 ctx, cancel = context.WithTimeout(ctx, defaultTimeout)
85 defer cancel()
86 }
87
88 if err := CalculateDefaultSearchLimits(ctx, q, s.Searcher, searchArgs.Opts); err != nil {
89 jsonError(w, http.StatusInternalServerError, err.Error())
90 return
91 }
92
93 searchResult, err := s.Searcher.Search(ctx, q, searchArgs.Opts)
94 if err != nil {
95 jsonError(w, http.StatusInternalServerError, err.Error())
96 return
97 }
98
99 err = json.NewEncoder(w).Encode(jsonSearchReply{searchResult})
100 if err != nil {
101 jsonError(w, http.StatusInternalServerError, err.Error())
102 return
103 }
104}
105
106func jsonError(w http.ResponseWriter, statusCode int, err string) {
107 w.WriteHeader(statusCode)
108 json.NewEncoder(w).Encode(struct{ Error string }{Error: err})
109}
110
111// Calculates and sets heuristic defaults on opts for various upper bounds on
112// the number of matches when searching, if none are already specified. The
113// defaults are derived from opts.MaxDocDisplayCount, so if none is set, there
114// is no calculation to do.
115func CalculateDefaultSearchLimits(ctx context.Context,
116 q query.Q,
117 searcher zoekt.Searcher,
118 opts *zoekt.SearchOptions,
119) error {
120 if opts.MaxDocDisplayCount == 0 || opts.ShardMaxMatchCount != 0 {
121 return nil
122 }
123
124 maxResultDocs := opts.MaxDocDisplayCount
125 // This is a special mode of Search that _only_ calculates ShardFilesConsidered and bails ASAP.
126 if result, err := searcher.Search(ctx, q, &zoekt.SearchOptions{EstimateDocCount: true}); err != nil {
127 return err
128 } else if numdocs := result.ShardFilesConsidered; numdocs > 10000 {
129 // If the search touches many shards and many files, we
130 // have to limit the number of matches. This setting
131 // is based on the number of documents eligible after
132 // considering reponames, so large repos (both
133 // android, chromium are about 500k files) aren't
134 // covered fairly.
135
136 // 10k docs, 50 maxResultDocs -> max match = (250 + 250 / 10)
137 opts.ShardMaxMatchCount = maxResultDocs*5 + (5*maxResultDocs)/(numdocs/1000)
138 } else {
139 // Virtually no limits for a small corpus.
140 n := numdocs + maxResultDocs*100
141 opts.ShardMaxMatchCount = n
142 opts.TotalMaxMatchCount = n
143 }
144
145 return nil
146}
147
148func (s *jsonSearcher) jsonList(w http.ResponseWriter, req *http.Request) {
149 w.Header().Add("Content-Type", "application/json")
150
151 if req.Method != "POST" {
152 jsonError(w, http.StatusMethodNotAllowed, "Only POST is supported")
153 return
154 }
155
156 listArgs := jsonListArgs{}
157 err := json.NewDecoder(req.Body).Decode(&listArgs)
158 if err != nil {
159 jsonError(w, http.StatusBadRequest, err.Error())
160 return
161 }
162
163 query, err := query.Parse(listArgs.Q)
164 if err != nil {
165 jsonError(w, http.StatusBadRequest, err.Error())
166 return
167 }
168
169 listResult, err := s.Searcher.List(req.Context(), query, listArgs.Opts)
170 if err != nil {
171 jsonError(w, http.StatusInternalServerError, err.Error())
172 return
173 }
174
175 err = json.NewEncoder(w).Encode(jsonListReply{listResult})
176 if err != nil {
177 jsonError(w, http.StatusInternalServerError, err.Error())
178 return
179 }
180}