diff --git a/docs/examples.md b/docs/examples.md
index 692cd7e39..c7476a722 100644
--- a/docs/examples.md
+++ b/docs/examples.md
@@ -83,10 +83,10 @@ lfm := pdk.LinearFloatMapper{
`Min` and `Max` define the linear function, and `Res` determines the maximum allowed value for the output row ID - we chose these values to produce a “round to nearest integer” behavior. Other predefined mappers have their own specific parameters, usually two or three.
-This mapper function is the core operation, but we need a few other pieces to define the overall process, which is encapsulated in the BitMapper object. This object defines which field(s) of the input data source to use (`Fields`), how to parse them (`Parsers`), what mapping to use (`Mapper`), and the name of the field to use (`Frame`). TODO update so this makes sense
+This mapper function is the core operation, but we need a few other pieces to define the overall process, which is encapsulated in the ColumnMapper object. This object defines which field(s) of the input data source to use (`Fields`), how to parse them (`Parsers`), what mapping to use (`Mapper`), and the name of the field to use (`Field`).
```go
-pdk.BitMapper{
- Frame: "dist_miles",
+pdk.ColumnMapper{
+ Field: "dist_miles",
Mapper: lfm,
Parsers: []pdk.Parser{pdk.FloatParser{}},
Fields: []int{fields["trip_distance"]},
@@ -107,9 +107,9 @@ These same objects are represented in the JSON definition file:
"Res": 3600
}
],
- "BitMappers": [
+ "ColumnMappers": [
{
- "Frame": "dist_miles",
+ "Field": "dist_miles",
"Mapper": {
"Name": "lfm0"
},
@@ -122,9 +122,9 @@ These same objects are represented in the JSON definition file:
}
```
-Here, we define a list of Mappers, each including a name, which we use to refer to the mapper later, in the list of BitMappers. We can also do this with Parsers, but a few simple Parsers that need no configuration are available by default. We also have a list of Fields, which is simply a map of field names (in the source data) to column indices (in Pilosa). We use these names in the BitMapper definitions to keep things human-readable.
+Here, we define a list of Mappers, each including a name, which we use to refer to the mapper later, in the list of ColumnMappers. We can also do this with Parsers, but a few simple Parsers that need no configuration are available by default. We also have a list of Fields, which is simply a map of field names (in the source data) to column indices (in Pilosa). We use these names in the ColumnMapper definitions to keep things human-readable.
-**total_amount_dollars:** Here we use the rounding mapping again, so each row represents rides with a total cost that rounds to the row's ID. The BitMapper definition is very similar to the previous one.
+**total_amount_dollars:** Here we use the rounding mapping again, so each row represents rides with a total cost that rounds to the row's ID. The ColumnMapper definition is very similar to the previous one.
**passenger_count:** This column contains small integers, so we use one of the simplest possible mappings: the column value is the row ID.
@@ -132,7 +132,7 @@ Here, we define a list of Mappers, each including a name, which we use to refer
When working with a composite data type like a timestamp, there are plenty of mapping options. In this case, we expect to see interesting periodic trends, so we want to encode the cyclic components of time in a way that allows us to look at them independently during analysis.
-We do this by storing time data in four separate fields for each timestamp: one each for the year, month, day, and time of day. The first three are mapped directly. For example, a ride with a date of 2015/06/24 will have a bit set in row 2015 of field "year", row 6 of field "month", and row 24 of field "day".
+We do this by storing time data in four separate fields for each timestamp: one each for the year, month, day, and time of day. The first three are mapped directly. For example, a ride with a date of 2015/06/24 will have a bit set in row 2015 of field "year", row 6 of field "month", and row 24 of field "day".
We might continue this pattern with hours, minutes, and seconds, but we don't have much use for that level of precision here, so instead we use a "bucketing" approach. That is, we pick a resolution (30 minutes), divide the day into buckets of that size, and create a row for each one. So a ride with a time of 6:45AM has a bit set in row 13 of field "time_of_day".
@@ -189,16 +189,26 @@ TopN(pickup_grid_id)
Average of `total_amount` per `passenger_count` can be computed with some postprocessing. We use a small number of `TopN` calls to retrieve counts of rides by passenger_count, then use those counts to compute an average.
```python
-queries = ''
+import pilosa
+
+client = pilosa.Client()
+schema = client.schema()
+taxi = schema.index("taxi")
+passenger_count = taxi.field("passenger_count")
+total_amount_dollars = taxi.field("total_amount_dollars")
+
+queries = []
pcounts = range(10)
for i in pcounts:
- queries += "TopN(Row(passenger_count=%d), total_amount_dollars)" % i
+ queries.append(total_amount_dollars.topn(passenger_count.row(i))
+query = taxi.batch_query(**queries)
+results = client.query(query)
resp = requests.post(qurl, data=queries)
average_amounts = []
-for pcount, topn in zip(pcounts, resp.json()['results']):
- wsum = sum([r['count'] * r['key'] for r in topn])
- count = sum([r['count'] for r in topn])
+for pcount, result in zip(pcounts, resp.results):
+ wsum = sum([r.count * r.id for r in result.count_items])
+ count = sum([r.count for r in result.count_items])
average_amounts.append(float(wsum)/count)
```
@@ -206,127 +216,6 @@ for pcount, topn in zip(pcounts, resp.json()['results']):
Note that the BSI-powered Sum query now provides an alternative approach to this kind of query.
+
diff --git a/handler.go b/handler.go
index 988435a07..5a452ce96 100644
--- a/handler.go
+++ b/handler.go
@@ -45,18 +45,19 @@ type QueryResponse struct {
// MarshalJSON marshals QueryResponse into a JSON-encoded byte slice
func (resp *QueryResponse) MarshalJSON() ([]byte, error) {
- var output struct {
- Results []interface{} `json:"results,omitempty"`
- ColumnAttrSets []*ColumnAttrSet `json:"columnAttrs,omitempty"`
- Err string `json:"error,omitempty"`
- }
- output.Results = resp.Results
- output.ColumnAttrSets = resp.ColumnAttrSets
-
if resp.Err != nil {
- output.Err = resp.Err.Error()
+ return json.Marshal(struct {
+ Err string `json:"error"`
+ }{Err: resp.Err.Error()})
}
- return json.Marshal(output)
+
+ return json.Marshal(struct {
+ Results []interface{} `json:"results"`
+ ColumnAttrSets []*ColumnAttrSet `json:"columnAttrs,omitempty"`
+ }{
+ Results: resp.Results,
+ ColumnAttrSets: resp.ColumnAttrSets,
+ })
}
type Handler interface {
diff --git a/http/handler.go b/http/handler.go
index 8cc059035..3226eee50 100644
--- a/http/handler.go
+++ b/http/handler.go
@@ -964,10 +964,12 @@ func (h *Handler) readURLQueryRequest(r *http.Request) (*pilosa.QueryRequest, er
}
// writeQueryResponse writes the response from the executor to w.
-func (h *Handler) writeQueryResponse(w io.Writer, r *http.Request, resp *pilosa.QueryResponse) error {
+func (h *Handler) writeQueryResponse(w http.ResponseWriter, r *http.Request, resp *pilosa.QueryResponse) error {
if !validHeaderAcceptJSON(r.Header) {
+ w.Header().Set("Content-Type", "application/protobuf")
return h.writeProtobufQueryResponse(w, resp)
}
+ w.Header().Set("Content-Type", "application/json")
return h.writeJSONQueryResponse(w, resp)
}
diff --git a/server/handler_test.go b/server/handler_test.go
index 701d019ae..f45aa2422 100644
--- a/server/handler_test.go
+++ b/server/handler_test.go
@@ -248,6 +248,8 @@ func TestHandler_Endpoints(t *testing.T) {
t.Fatalf("unexpected status code: %d", w.Code)
} else if body := w.Body.String(); body != `{"results":[2]}`+"\n" {
t.Fatalf("unexpected body: %q", body)
+ } else if w.Header().Get("Content-Type") != "application/json" {
+ t.Fatalf("unexpected header: %q", w.Header().Get("Content-Type"))
}
})
@@ -286,6 +288,8 @@ func TestHandler_Endpoints(t *testing.T) {
t.Fatal(err)
} else if rt, ok := resp.Results[0].(uint64); !ok || rt != 3 {
t.Fatalf("unexpected response type: %#v", resp.Results[0])
+ } else if w.Header().Get("Content-Type") != "application/protobuf" {
+ t.Fatalf("unexpected header: %q", w.Header().Get("Content-Type"))
}
})
@@ -445,6 +449,14 @@ func TestHandler_Endpoints(t *testing.T) {
}
})
+ t.Run("Query empty", func(t *testing.T) {
+ w := httptest.NewRecorder()
+ h.ServeHTTP(w, test.MustNewHTTPRequest("POST", "/index/i0/query", strings.NewReader("")))
+ if body := w.Body.String(); body != `{"results":[]}`+"\n" {
+ t.Fatalf("unexpected body: %q", body)
+ }
+ })
+
t.Run("Method not allowed", func(t *testing.T) {
w := httptest.NewRecorder()
h.ServeHTTP(w, test.MustNewHTTPRequest("GET", "/index/i0/query", nil))