From bbfa34531656318eed139f1913fb258abc30ca80 Mon Sep 17 00:00:00 2001 From: Sameer Siruguri Date: Wed, 2 Sep 2015 14:51:34 -0700 Subject: [PATCH 1/5] [WIP] (#63) Add a /stats sub-endpoint * Syntax is WIP; needs to be reviewed * We should have an API spec test - tbd. --- Gemfile | 4 +- Gemfile.lock | 3 -- app/controllers.rb | 12 +++-- lib/data_magic.rb | 23 ++++++++-- lib/data_magic/document_builder.rb | 7 ++- lib/data_magic/query_builder.rb | 19 ++++++++ sample-data/data.yaml | 3 +- spec/fixtures/data.rb | 22 +++++----- spec/fixtures/numeric_data/data.yaml | 20 +++++++++ spec/lib/data_magic/config_spec.rb | 2 +- spec/lib/data_magic/search_spec.rb | 66 +++++++++++++++++++++++++--- 11 files changed, 150 insertions(+), 31 deletions(-) create mode 100644 spec/fixtures/numeric_data/data.yaml diff --git a/Gemfile b/Gemfile index 79b17360..7c40b580 100644 --- a/Gemfile +++ b/Gemfile @@ -44,8 +44,8 @@ end # Padrino Stable Gem gem 'padrino', '0.12.5' -gem 'pry', :group => 'development' -gem 'pry-byebug', :group => 'development' +gem 'pry', :group => ['development', 'test'] +gem 'pry-byebug', :group => ['development', 'test'] gem 'newrelic_rpm' # Or Padrino Edge diff --git a/Gemfile.lock b/Gemfile.lock index fc3aa2d4..411a94b0 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -245,6 +245,3 @@ DEPENDENCIES sass stretchy unicorn - -BUNDLED WITH - 1.10.5 diff --git a/app/controllers.rb b/app/controllers.rb index 660984bd..49da1215 100644 --- a/app/controllers.rb +++ b/app/controllers.rb @@ -33,10 +33,11 @@ data.to_json end - get :index, with: :endpoint, provides: [:json, :csv] do + get :index, with: ':endpoint(/:_command)', provides: [:json, :csv] do + # Optional parameter format discovered at http://jorgennilsson.com/article/optional-named-parameters-in-padrino-routes (endpoint, options, format) = get_search_args_from_params(params) content_type format.to_sym if format - DataMagic.logger.debug "-----> APP GET #{params.inspect}" + DataMagic.logger.debug "-----> APP GET #{params.inspect} with options #{options.inspect}" if not DataMagic.config.api_endpoints.keys.include? endpoint halt 404, { @@ -65,8 +66,13 @@ def get_search_args_from_params(params) distance: params.delete("distance"), fields: (params.delete('fields') || "").split(','), per_page: params.delete("per_page") || DataMagic.config.page_size, - page: params.delete("page").to_i || 0 + page: params.delete("page").to_i || 0, + add_aggregations: params.delete("_command") == 'stats' ? true : false, + metrics: (params.delete("_metrics") || "").split(/\s*,\s*/) } + + # Ignore the aggregations endpoint if we don't have fields to aggregate on + options[:add_aggregations] &&= (options[:fields].size > 0) format = params.delete('format') [endpoint, options, format] end diff --git a/lib/data_magic.rb b/lib/data_magic.rb index 604a899c..184473ac 100644 --- a/lib/data_magic.rb +++ b/lib/data_magic.rb @@ -78,7 +78,8 @@ def self.search(terms, options = {}) logger.info "FULL_QUERY: #{full_query.inspect}" result = client.search full_query - logger.info "result: #{result.inspect[0..500]}" + logger.info "result: #{result.inspect}" + hits = result["hits"] total = hits["total"] results = [] @@ -94,13 +95,13 @@ def self.search(terms, options = {}) found.keys.each { |key| found[key] = found[key][0] } # now it should look like this: - # {"city"=>"Springfield", "address"=>"742 Evergreen Terrace + # {"city"=>"Springfield", "address"=>"742 Evergreen Terrace"} found end end # assemble a simpler json document to return - { + simple_result = { "metadata" => { "total" => total, "page" => query_body[:from] / query_body[:size], @@ -108,6 +109,22 @@ def self.search(terms, options = {}) }, "results" => results } + + if options[:add_aggregations] + # Remove metrics that weren't requested. + aggregations = result['aggregations'] + aggregations.each do |f_name, values| + # Count is not an interesting statistic + values.delete 'count' + if options[:metrics] && options[:metrics].size > 0 + aggregations[f_name] = values.reject { |k, v| !(options[:metrics].include? k) } + end + end + + simple_result.merge!({"aggregations" => aggregations}) + end + + simple_result end private diff --git a/lib/data_magic/document_builder.rb b/lib/data_magic/document_builder.rb index d25199b9..4d8e4d86 100644 --- a/lib/data_magic/document_builder.rb +++ b/lib/data_magic/document_builder.rb @@ -47,7 +47,12 @@ def map_field_types(row, valid_types, field_types = {}, null_value = 'NULL') if value == null_value mapped[key] = nil else - type = field_types[key.to_sym] || field_types[key.to_s] + tmp = {"age" => "integer", "height" => "float"} + if tmp.keys.include?(key) + type = tmp[key] + elsif + type = field_types[key.to_sym] || field_types[key.to_s] + end if valid_types.include? type mapped[key] = fix_field_type(type, value, key) mapped["_#{key}"] = value.downcase if type == "name" diff --git a/lib/data_magic/query_builder.rb b/lib/data_magic/query_builder.rb index a0507326..bfd44d33 100644 --- a/lib/data_magic/query_builder.rb +++ b/lib/data_magic/query_builder.rb @@ -13,6 +13,10 @@ def from_params(params, options, config) size: per_page.to_i } query_hash[:query] = generate_squery(params, options, config).to_search + if options[:add_aggregations] + query_hash.merge! add_aggregations(params, options, config) + end + query_hash[:fields] = get_restrict_fields(options) if options[:fields] && !options[:fields].empty? query_hash[:sort] = get_sort_order(options[:sort]) if options[:sort] && !options[:sort].empty? query_hash @@ -26,6 +30,21 @@ def generate_squery(params, options, config) search_fields_and_ranges(squery, params, config) end + def add_aggregations(params, options, config) + # Wrapper for Stretchy aggregation clause builder (which wraps ElasticSearch (ES) :aggs parameter) + # Extracts all extended_stats aggregations from ES, to be filtered later + # Is a no-op if no fields are specified, or none of them are numeric + + agg_hash = options[:fields].inject({}) do |memo, f| + if config.column_field_types[f.to_s] && ["integer", "float"].include?(config.column_field_types[f.to_s]) + memo[f.to_s] = { "extended_stats" => { "field" => f.to_s } } + end + memo + end + + agg_hash != {} ? { "aggs" => agg_hash } : {} + end + def get_restrict_fields(options) options[:fields].map(&:to_s) end diff --git a/sample-data/data.yaml b/sample-data/data.yaml index 86eea6fc..931f68e2 100644 --- a/sample-data/data.yaml +++ b/sample-data/data.yaml @@ -48,7 +48,8 @@ dictionary: type: integer location.lat: INTPTLAT location.lon: INTPTLONG - area.land: + land_area: + source: ALAND_SQMI description: Land Area (square miles) type: float area.water: diff --git a/spec/fixtures/data.rb b/spec/fixtures/data.rb index f567416d..47004d1a 100644 --- a/spec/fixtures/data.rb +++ b/spec/fixtures/data.rb @@ -1,16 +1,16 @@ - - +# Ages adjusted for Springfield residents to average to 42 +# Heights randomly set to generate a max of 142 def address_data @address_data ||= StringIO.new <<-eos -name,address,city -Paul,15 Penny Lane,Liverpool -Michelle,600 Pennsylvania Avenue,Washington -Marilyn,1313 Mockingbird Lane,Springfield -Sherlock,221B Baker Street,London -Clark,66 Lois Lane,Smallville -Bart,742 Evergreen Terrace,Springfield -Paul,19 N Square,Boston -Peter,66 Parker Lane,New York +name,address,city,age,height +Paul,15 Penny Lane,Liverpool,10,142 +Michelle,600 Pennsylvania Avenue,Washington,12,1 +Marilyn,1313 Mockingbird Lane,Springfield,14,2 +Sherlock,221B Baker Street,London,16,123 +Clark,66 Lois Lane,Smallville,18,141 +Bart,742 Evergreen Terrace,Springfield,70,142 +Paul,19 N Square,Boston,70,55.2 +Peter,66 Parker Lane,New York,74,11.5123 eos @address_data.rewind @address_data diff --git a/spec/fixtures/numeric_data/data.yaml b/spec/fixtures/numeric_data/data.yaml new file mode 100644 index 00000000..9f37e527 --- /dev/null +++ b/spec/fixtures/numeric_data/data.yaml @@ -0,0 +1,20 @@ +# cities100.txt +# Test YAML file +index: numeric-data + +dictionary: + name: + source: name + type: string + address: + source: address + type: string + city: + source: city + type: string + age: + source: age + type: integer + height: + source: height + type: float diff --git a/spec/lib/data_magic/config_spec.rb b/spec/lib/data_magic/config_spec.rb index 5b9e8d8d..0ae4432f 100644 --- a/spec/lib/data_magic/config_spec.rb +++ b/spec/lib/data_magic/config_spec.rb @@ -88,7 +88,7 @@ dictionary = config.data.delete 'dictionary' expect(dictionary.keys.sort).to eq %w(id code name state population - location.lat location.lon area.land area.water).sort + location.lat location.lon land_area area.water).sort categories = config.data.delete 'categories' expect(categories.keys.sort).to eq %w(general geographic).sort expect(config.data).to eq(default_config) diff --git a/spec/lib/data_magic/search_spec.rb b/spec/lib/data_magic/search_spec.rb index f6999164..8aefaaa4 100644 --- a/spec/lib/data_magic/search_spec.rb +++ b/spec/lib/data_magic/search_spec.rb @@ -1,8 +1,16 @@ require 'spec_helper' require 'data_magic' require 'fixtures/data.rb' +require 'pry' describe "DataMagic #search" do + let (:springfield_residents) do + [ + {"age" => 14, "height" => 2.0, "address"=>"1313 Mockingbird Lane"}, + {"age" => 70, "height" => 142.0, "address"=>"742 Evergreen Terrace"} + ] + end + let (:expected) do { "metadata" => { @@ -27,22 +35,27 @@ it "can find document with one attribute" do result = DataMagic.search({name: "Marilyn"}) - expected["results"] = [{"name" => "Marilyn", "address" => "1313 Mockingbird Lane", "city" => "Springfield"}] + expected["results"] = [{"name" => "Marilyn", "address" => "1313 Mockingbird Lane", "city" => "Springfield", + "age" => "14", "height" => "2"}] expect(result).to eq(expected) end it "can find document with multiple search terms" do result = DataMagic.search({name: "Paul", city:"Liverpool"}) - expected["results"] = [{"name" => "Paul", "address" => "15 Penny Lane", "city" => "Liverpool"}] + expected["results"] = [{"name" => "Paul", "address" => "15 Penny Lane", "city" => "Liverpool", + "age" => "10", "height" => "142"}] expect(result).to eq(expected) end it "can find a document with a set of values delimited by commas" do result = DataMagic.search({name: "Paul,Marilyn"}) expected['metadata']["total"] = 3 - expect(result["results"]).to include({"name" => "Marilyn", "address" => "1313 Mockingbird Lane", "city" => "Springfield"}) - expect(result["results"]).to include({"name" => "Paul", "address" => "15 Penny Lane", "city" => "Liverpool"}) - expect(result["results"]).to include({"name" => "Paul", "address" => "19 N Square", "city" => "Boston"}) + expect(result["results"]).to include({"name" => "Marilyn", "address" => "1313 Mockingbird Lane", "city" => "Springfield", + "age" => "14", "height" => "2"}) + expect(result["results"]).to include({"name" => "Paul", "address" => "15 Penny Lane", "city" => "Liverpool", + "age" => "10", "height" => "142"}) + expect(result["results"]).to include({"name" => "Paul", "address" => "19 N Square", "city" => "Boston", + "age" => "70", "height" => "55.2"}) end it "can return a single attribute" do @@ -68,7 +81,6 @@ expect(result).to eq(expected) end - it "supports pagination" do result = DataMagic.search({ address: "Lane" }, page:1, per_page: 3) expect(result['metadata']["per_page"]).to eq(3) @@ -99,9 +111,51 @@ end + end + + describe "with numeric data" do + before (:all) do + ENV['DATA_PATH'] = './spec/fixtures/numeric_data' + DataMagic.init(load_now: false) + num_rows, fields = DataMagic.import_csv(address_data) + end + after(:all) do + DataMagic.destroy + end + + it "can correctly compute filtered statistics" do + expected["metadata"]["total"] = 2 + result = DataMagic.search({city: "Springfield"}, add_aggregations: true, fields: ["age", "height", "address"], + metrics: ['max', 'avg']) + result["results"] = result["results"].sort_by { |k| k["age"] } + + expected["results"] = springfield_residents + expected["aggregations"] = { + "age" => { "max" => 70.0, "avg" => 42.0}, + "height" => {"max"=>142.0, "avg"=>72.0} + } + + expect(result).to eq(expected) + end + + it "can correctly compute unfiltered statistics" do + expected["metadata"]["total"] = 2 + result = DataMagic.search({city: "Springfield"}, add_aggregations: true, fields: ["age", "height", "address"]) + result["results"] = result["results"].sort_by { |k| k["age"] } + expected["results"] = springfield_residents + expected["aggregations"] = { + "age"=>{ + "min"=>14.0, "max"=>70.0, "avg"=>42.0, "sum"=>84.0, "sum_of_squares"=>5096.0, "variance"=>784.0, "std_deviation"=>28.0, "std_deviation_bounds"=>{"upper"=>98.0, "lower"=>-14.0}}, + "height"=>{ + "min"=>2.0, "max"=>142.0, "avg"=>72.0, "sum"=>144.0, "sum_of_squares"=>20168.0, "variance"=>4900.0, "std_deviation"=>70.0, "std_deviation_bounds"=>{"upper"=>212.0, "lower"=>-68.0} + } + } + expect(result).to eq(expected) + end end + describe "with geolocation" do before (:all) do ENV['DATA_PATH'] = './spec/fixtures/geo_no_files' From eb4ed820662cdea98756604921da528e18019c92 Mon Sep 17 00:00:00 2001 From: Sameer Siruguri Date: Thu, 3 Sep 2015 10:15:32 -0700 Subject: [PATCH 2/5] Add integration test for /stats sub-endpoint --- spec/features/api_spec.rb | 41 ++++++++++++++++++++++++++++ spec/fixtures/numeric_data/data.yaml | 1 + 2 files changed, 42 insertions(+) diff --git a/spec/features/api_spec.rb b/spec/features/api_spec.rb index fd8ca3e4..f7fc534f 100644 --- a/spec/features/api_spec.rb +++ b/spec/features/api_spec.rb @@ -310,4 +310,45 @@ end end end + + context "with residents CSV data" do + before do + ENV['DATA_PATH'] = './spec/fixtures/numeric_data' + DataMagic.init(load_now: false) + num_rows, fields = DataMagic.import_csv(address_data) + end + + after do + DataMagic.destroy + end + + describe "when using the stats command" do + let(:springfield_residents) do + [ + {"age" => 14, "height" => 2.0, "address"=>"1313 Mockingbird Lane"}, + {"age" => 70, "height" => 142.0, "address"=>"742 Evergreen Terrace"} + ] + end + + let(:expected_results) do + { "metadata" => { "total" => 2, + "page" => 0, + "per_page" => DataMagic::DEFAULT_PAGE_SIZE + }, + "results" => springfield_residents, + "aggregations" => { + "age" => { "max" => 70.0, "avg" => 42.0}, + "height" => {"max"=>142.0, "avg"=>72.0} + } + } + end + + it "returns the correct results for Springfield residents" do + get '/v1/cities/stats?city=Springfield&fields=address,age,height&_metrics=max,avg' + expect(last_response).to be_ok + json_response["results"] = json_response["results"].sort_by { |k| k["age"] } + expect(json_response).to eq(expected_results) + end + end + end end diff --git a/spec/fixtures/numeric_data/data.yaml b/spec/fixtures/numeric_data/data.yaml index 9f37e527..80ddf3a5 100644 --- a/spec/fixtures/numeric_data/data.yaml +++ b/spec/fixtures/numeric_data/data.yaml @@ -1,6 +1,7 @@ # cities100.txt # Test YAML file index: numeric-data +api: cities dictionary: name: From f93aecef157980e368fff8f7f1d0c0bc9c632601 Mon Sep 17 00:00:00 2001 From: Sameer Siruguri Date: Sat, 26 Sep 2015 21:35:37 -0700 Subject: [PATCH 3/5] Incorporate fdbk from PR * Allow /stats to not be given _metrics option * Re-factor code in controllers to separate /stats from main endpoint * Remove result hits from aggregation payload in /stats * Remove stray debugging cruft * Refactor error checking * Fix comments position --- Gemfile.lock | 4 -- app/controllers.rb | 65 ++++++++++++++++++++---------- lib/data_magic.rb | 25 ++++++------ lib/data_magic/document_builder.rb | 7 +--- lib/data_magic/error_checker.rb | 13 +++++- lib/data_magic/query_builder.rb | 16 ++++---- spec/features/api_spec.rb | 47 ++++++++++++++------- spec/lib/data_magic/search_spec.rb | 20 +++------ 8 files changed, 115 insertions(+), 82 deletions(-) diff --git a/Gemfile.lock b/Gemfile.lock index 7f8daaff..9af9624c 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -242,7 +242,3 @@ DEPENDENCIES safe_yaml sass stretchy - unicorn - -BUNDLED WITH - 1.10.6 diff --git a/app/controllers.rb b/app/controllers.rb index 456bc203..23814503 100644 --- a/app/controllers.rb +++ b/app/controllers.rb @@ -37,28 +37,49 @@ data.to_json end - get :index, with: ':endpoint(/:_command)', provides: [:json, :csv] do - # Optional parameter format discovered at http://jorgennilsson.com/article/optional-named-parameters-in-padrino-routes - options = get_search_args_from_params(params) - endpoint = options[:endpoint] - content_type options[:format].to_sym if options[:format] - DataMagic.logger.debug "-----> APP GET #{params.inspect} with options #{options.inspect}" + get :index, with: ':endpoint/:command', provides: [:json] do + process_params + end - unless DataMagic.config.api_endpoints.keys.include? endpoint - halt 404, { - error: 404, - message: "#{endpoint} not found. Available endpoints: #{DataMagic.config.api_endpoints.keys.join(',')}" - }.to_json - end + get :index, with: ':endpoint', provides: [:json, :csv] do + process_params + end +end - data = DataMagic.search(params, options) - halt 400, data.to_json if data.key?(:errors) +def process_params + options = get_search_args_from_params(params) + DataMagic.logger.debug "-----> APP GET #{params.inspect} with options #{options.inspect}" - if content_type == :csv - output_data_as_csv(data['results']) - else - data.to_json - end + check_endpoint!(options) + set_content_type(options) + search_and_respond(options) +end + +def search_and_respond(options) + data = DataMagic.search(params, options) + halt 400, data.to_json if data.key?(:errors) + + if content_type == :csv + output_data_as_csv(data['results']) + else + data.to_json + end +end + +def check_endpoint!(options) + unless DataMagic.config.api_endpoints.keys.include? options[:endpoint] + halt 404, { + error: 404, + message: "#{options[:endpoint]} not found. Available endpoints: #{DataMagic.config.api_endpoints.keys.join(',')}" + }.to_json + end +end + +def set_content_type(options) + if options[:command] == 'stats' + content_type :json + else + content_type(options[:format].nil? ? :json : options[:format].to_sym) end end @@ -67,7 +88,7 @@ # see comment in method body def get_search_args_from_params(params) options = {} - %w(sort fields zip distance page per_page debug).each do |opt| + %w(metrics sort fields zip distance page per_page debug).each do |opt| options[opt.to_sym] = params.delete("_#{opt}") # TODO: remove next line to end support for un-prefixed option parameters options[opt.to_sym] ||= params.delete(opt) @@ -75,7 +96,9 @@ def get_search_args_from_params(params) options[:endpoint] = params.delete("endpoint") # these two params are options[:format] = params.delete("format") # supplied by Padrino options[:fields] = (options[:fields] || "").split(',') - options[:add_aggregations] &&= (options[:fields].size > 0) + options[:command] = params.delete("command") + + options[:metrics] = options[:metrics].split(/\s*,\s*/) if options[:metrics] options end diff --git a/lib/data_magic.rb b/lib/data_magic.rb index 4f991dac..0cfd3799 100644 --- a/lib/data_magic.rb +++ b/lib/data_magic.rb @@ -76,13 +76,19 @@ def self.search(terms, options = {}) body: query_body } + # Per https://www.elastic.co/guide/en/elasticsearch/reference/current/search-request-body.html: + # "the search_type and the query_cache must be passed as query-string parameters" + if options[:command] == 'stats' + full_query.merge! :search_type => 'count' + end + logger.info "FULL_QUERY: #{full_query.inspect}" time_start = Time.now.to_f result = client.search full_query search_time = Time.now.to_f - time_start - logger.info "ES query time (ms): #{result["took"]} ; Query fetch time (s): #{search_time} ; result: #{result.inspect}" + logger.info "ES query time (ms): #{result["took"]} ; Query fetch time (s): #{search_time} ; result: #{result.inspect[0..500]}" hits = result["hits"] total = hits["total"] @@ -115,28 +121,21 @@ def self.search(terms, options = {}) end # assemble a simpler json document to return -<<<<<<< HEAD - simple_result = { - "metadata" => { - "total" => total, - "page" => query_body[:from] / query_body[:size], - "per_page" => query_body[:size] - }, -======= + simple_result = { "metadata" => metadata, ->>>>>>> dev "results" => results } - if options[:add_aggregations] + if options[:command] == 'stats' # Remove metrics that weren't requested. aggregations = result['aggregations'] aggregations.each do |f_name, values| - # Count is not an interesting statistic - values.delete 'count' if options[:metrics] && options[:metrics].size > 0 aggregations[f_name] = values.reject { |k, v| !(options[:metrics].include? k) } + else + # Keep everything is no metric list is provided + aggregations[f_name] = values end end diff --git a/lib/data_magic/document_builder.rb b/lib/data_magic/document_builder.rb index bfb0640f..bf2b58a4 100644 --- a/lib/data_magic/document_builder.rb +++ b/lib/data_magic/document_builder.rb @@ -47,12 +47,7 @@ def map_field_types(row, valid_types, field_types = {}, null_value = 'NULL') if value == null_value mapped[key] = nil else - tmp = {"age" => "integer", "height" => "float"} - if tmp.keys.include?(key) - type = tmp[key] - elsif - type = field_types[key.to_sym] || field_types[key.to_s] - end + type = field_types[key.to_sym] || field_types[key.to_s] if valid_types.include? type mapped[key] = fix_field_type(type, value, key) mapped["_#{key}"] = value.downcase if type == "name" || type == "autocomplete" diff --git a/lib/data_magic/error_checker.rb b/lib/data_magic/error_checker.rb index 2bd31b75..d66ac928 100644 --- a/lib/data_magic/error_checker.rb +++ b/lib/data_magic/error_checker.rb @@ -2,7 +2,8 @@ module DataMagic module ErrorChecker class << self def check(params, options, config) - report_nonexistent_params(params, config) + + report_required_params_absent(options) + + report_nonexistent_params(params, config) + report_nonexistent_operators(params) + report_nonexistent_fields(options[:fields], config) + report_bad_range_argument(params) + @@ -11,6 +12,14 @@ def check(params, options, config) private + def report_required_params_absent(options) + if options[:command] == 'stats' && options[:fields].length == 0 + [build_error(error: 'invalid_or_incomplete_parameters', input: options[:command])] + else + [] + end + end + def report_nonexistent_params(params, config) return [] unless config.dictionary_only_search? params.keys.reject { |p| config.field_type(strip_op(p)) }. @@ -66,6 +75,8 @@ def report_wrong_field_type(params, config) def build_error(opts) opts[:message] = case opts[:error] + when 'invalid_or_incomplete_parameters' + "The command #{opts[:input]} requires a fields parameter." when 'parameter_not_found' "The input parameter '#{opts[:input]}' is not known in this dataset." when 'field_not_found' diff --git a/lib/data_magic/query_builder.rb b/lib/data_magic/query_builder.rb index 68644e8e..3e5b1358 100644 --- a/lib/data_magic/query_builder.rb +++ b/lib/data_magic/query_builder.rb @@ -8,11 +8,12 @@ def from_params(params, options, config) per_page = DataMagic::MAX_PAGE_SIZE if per_page > DataMagic::MAX_PAGE_SIZE query_hash = { from: page * per_page, - size: per_page + size: per_page, } + query_hash[:query] = generate_squery(params, options, config).to_search - if options[:add_aggregations] + if options[:command] == 'stats' query_hash.merge! add_aggregations(params, options, config) end @@ -36,19 +37,18 @@ def generate_squery(params, options, config) search_fields_and_ranges(squery, params, config) end + # Wrapper for Stretchy aggregation clause builder (which wraps ElasticSearch (ES) :aggs parameter) + # Extracts all extended_stats aggregations from ES, to be filtered later + # Is a no-op if no fields are specified, or none of them are numeric def add_aggregations(params, options, config) - # Wrapper for Stretchy aggregation clause builder (which wraps ElasticSearch (ES) :aggs parameter) - # Extracts all extended_stats aggregations from ES, to be filtered later - # Is a no-op if no fields are specified, or none of them are numeric - agg_hash = options[:fields].inject({}) do |memo, f| if config.column_field_types[f.to_s] && ["integer", "float"].include?(config.column_field_types[f.to_s]) - memo[f.to_s] = { "extended_stats" => { "field" => f.to_s } } + memo[f.to_s] = { extended_stats: { "field" => f.to_s } } end memo end - agg_hash != {} ? { "aggs" => agg_hash } : {} + agg_hash.empty? ? {} : { aggs: agg_hash } end def get_restrict_fields(options) diff --git a/spec/features/api_spec.rb b/spec/features/api_spec.rb index ff0f865c..966cdf70 100644 --- a/spec/features/api_spec.rb +++ b/spec/features/api_spec.rb @@ -352,32 +352,49 @@ after do DataMagic.destroy end - - let(:springfield_residents) do - [ - {"age" => 14, "height" => 2.0, "address"=>"1313 Mockingbird Lane"}, - {"age" => 70, "height" => 142.0, "address"=>"742 Evergreen Terrace"} - ] + + let(:all_aggregs) do + { "aggregations" => { + "age"=>{"count"=>2, "min"=>14.0, "max"=>70.0, "avg"=>42.0, "sum"=>84.0, "sum_of_squares"=>5096.0, "variance"=>784.0, "std_deviation"=>28.0, "std_deviation_bounds"=>{"upper"=>98.0, "lower"=>-14.0}}, + "height"=>{"count"=>2, "min"=>2.0, "max"=>142.0, "avg"=>72.0, "sum"=>144.0, "sum_of_squares"=>20168.0, "variance"=>4900.0, "std_deviation"=>70.0, "std_deviation_bounds"=>{"upper"=>212.0, "lower"=>-68.0}} + } + } end - - let(:expected_results) do + + let(:max_avg_aggregs) do + { "aggregations" => { + "age" => { "max" => 70.0, "avg" => 42.0}, + "height" => { "max" => 142.0, "avg" => 72.0} + } + } + end + + let(:stats_envelope) do { "metadata" => { "total" => 2, "page" => 0, "per_page" => DataMagic::DEFAULT_PAGE_SIZE }, - "results" => springfield_residents, - "aggregations" => { - "age" => { "max" => 70.0, "avg" => 42.0}, - "height" => {"max"=>142.0, "avg"=>72.0} - } + "results" => [] } end it "/stats returns the correct results for Springfield residents" do - get '/v1/cities/stats?city=Springfield&fields=address,age,height&_metrics=max,avg' + get '/v1/cities/stats?city=Springfield&_fields=address,age,height&_metrics=max,avg' expect(last_response).to be_ok json_response["results"] = json_response["results"].sort_by { |k| k["age"] } - expect(json_response).to eq(expected_results) + expect(json_response).to eq(stats_envelope.merge(max_avg_aggregs)) + end + + it "/stats returns all metrics when none are specified" do + get '/v1/cities/stats?city=Springfield&_fields=address,age,height' + expect(last_response).to be_ok + json_response["results"] = json_response["results"].sort_by { |k| k["age"] } + expect(json_response).to eq(stats_envelope.merge(all_aggregs)) + end + + it "/stats requires fields option" do + get '/v1/cities/stats?city=Springfield&_metrics=max,avg' + expect(last_response.status).to eq(400) end end diff --git a/spec/lib/data_magic/search_spec.rb b/spec/lib/data_magic/search_spec.rb index 2cfda230..b57240ed 100644 --- a/spec/lib/data_magic/search_spec.rb +++ b/spec/lib/data_magic/search_spec.rb @@ -1,16 +1,8 @@ require 'spec_helper' require 'data_magic' require 'fixtures/data.rb' -require 'pry' describe "DataMagic #search" do - let (:springfield_residents) do - [ - {"age" => 14, "height" => 2.0, "address"=>"1313 Mockingbird Lane"}, - {"age" => 70, "height" => 142.0, "address"=>"742 Evergreen Terrace"} - ] - end - let (:expected) do { "metadata" => { @@ -134,11 +126,11 @@ it "can correctly compute filtered statistics" do expected["metadata"]["total"] = 2 - result = DataMagic.search({city: "Springfield"}, add_aggregations: true, fields: ["age", "height", "address"], + result = DataMagic.search({city: "Springfield"}, command: 'stats', fields: ["age", "height", "address"], metrics: ['max', 'avg']) result["results"] = result["results"].sort_by { |k| k["age"] } - expected["results"] = springfield_residents + expected["results"] = [] expected["aggregations"] = { "age" => { "max" => 70.0, "avg" => 42.0}, "height" => {"max"=>142.0, "avg"=>72.0} @@ -149,15 +141,15 @@ it "can correctly compute unfiltered statistics" do expected["metadata"]["total"] = 2 - result = DataMagic.search({city: "Springfield"}, add_aggregations: true, fields: ["age", "height", "address"]) + result = DataMagic.search({city: "Springfield"}, command: 'stats', fields: ["age", "height", "address"]) result["results"] = result["results"].sort_by { |k| k["age"] } - expected["results"] = springfield_residents + expected["results"] = [] expected["aggregations"] = { "age"=>{ - "min"=>14.0, "max"=>70.0, "avg"=>42.0, "sum"=>84.0, "sum_of_squares"=>5096.0, "variance"=>784.0, "std_deviation"=>28.0, "std_deviation_bounds"=>{"upper"=>98.0, "lower"=>-14.0}}, + "count"=>2, "min"=>14.0, "max"=>70.0, "avg"=>42.0, "sum"=>84.0, "sum_of_squares"=>5096.0, "variance"=>784.0, "std_deviation"=>28.0, "std_deviation_bounds"=>{"upper"=>98.0, "lower"=>-14.0}}, "height"=>{ - "min"=>2.0, "max"=>142.0, "avg"=>72.0, "sum"=>144.0, "sum_of_squares"=>20168.0, "variance"=>4900.0, "std_deviation"=>70.0, "std_deviation_bounds"=>{"upper"=>212.0, "lower"=>-68.0} + "count"=>2, "min"=>2.0, "max"=>142.0, "avg"=>72.0, "sum"=>144.0, "sum_of_squares"=>20168.0, "variance"=>4900.0, "std_deviation"=>70.0, "std_deviation_bounds"=>{"upper"=>212.0, "lower"=>-68.0} } } From b662c959417acfcbf0783531b8ee1e2eca904f8d Mon Sep 17 00:00:00 2001 From: Sameer Siruguri Date: Sat, 5 Dec 2015 08:46:17 -0800 Subject: [PATCH 4/5] Fix spec definition that compares hashes --- public/stylesheets/application.css | 116 +++++++++++++++++++++++++ public/stylesheets/application.css.map | 7 ++ spec/features/api_spec.rb | 7 +- spec/lib/data_magic/config_spec.rb | 1 - spec/lib/data_magic/search_spec.rb | 3 +- 5 files changed, 131 insertions(+), 3 deletions(-) create mode 100644 public/stylesheets/application.css create mode 100644 public/stylesheets/application.css.map diff --git a/public/stylesheets/application.css b/public/stylesheets/application.css new file mode 100644 index 00000000..a925efa9 --- /dev/null +++ b/public/stylesheets/application.css @@ -0,0 +1,116 @@ +body { + -webkit-font-smoothing: antialiased; + font-family: "Open Sans", "Helvetica Neue", Helvetica, Arial, sans-serif; + font-size: 1em; + line-height: 1.5; + color: #333; } + +h1, h2, h3, h4, h5, h6 { + font-family: "Raleway", "Helvetica Neue", Helvetica, Arial, sans-serif; + line-height: 1.1em; + margin: 0; + text-rendering: optimizeLegibility; } + +p { + margin: 0 0 0.75em; } + +hr { + border-bottom: 1px solid silver; + border-left: none; + border-right: none; + border-top: none; + margin: 1em 0; } + +img { + -webkit-user-select: none; + cursor: zoom-in; + margin: 0; + max-width: 50%; } + +.logo { + height: 150px; + width: 150px; + top: 50px; + left: 50px; + z-index: 20; } + +@media screen and (max-width: 995px) { + .logo { + height: 100px; + width: 100px; + top: 40px; + left: 20px; } } +@media screen and (max-width: 785px) { + .logo { + height: 75px; + width: 75px; } } +@media screen and (max-width: 590px) { + .logo { + top: 73px; } } +@media screen and (max-width: 480px) { + .logo { + top: 16px; + left: 0px; } } +.bottom-margin { + margin-bottom: 0.5em; + color: #c00; } + +.title { + text-align: center; + font-family: "Raleway", "Helvetica Neue", Helvetica, Arial, sans-serif; + font-size: 2em; + line-height: 2em; } + +.header { + background-color: #9cf; } + +.categories .category { + margin: 5px; + padding: 15px; + border: solid 1px silver; + word-wrap: break-word; + display: inline-block; + width: 92%; + background-color: #ffc; } + .categories .category a { + color: black; + text-decoration: none; } + .categories .category a:visited { + color: black; } + +.categories__column { + display: inline-block; + width: 100%; + vertical-align: top; + -webkit-column-count: 2; + -moz-column-count: 2; + column-count: 2; + column-gap: 0.2em; + -webkit-column-gap: 0.2em; + -moz-column-gap: 0.2em; } + +.category__name { + font-size: 18px; + font-weight: bold; + margin-bottom: 5px; + color: #c00; } + +.category__fields { + list-style: none; + padding: 0; } + +.category__field-name { + font-size: 15px; + font-weight: bold; + margin-bottom: 2px; + color: #c00; + width: 80%; } + +.category__field-type { + font-size: 15px; + font-weight: bold; + color: #c00; + width: 10%; + float: right; } + +/*# sourceMappingURL=application.css.map */ diff --git a/public/stylesheets/application.css.map b/public/stylesheets/application.css.map new file mode 100644 index 00000000..dbcb3797 --- /dev/null +++ b/public/stylesheets/application.css.map @@ -0,0 +1,7 @@ +{ +"version": 3, +"mappings": "AAAA,IAAI;EACF,sBAAsB,EAAE,WAAW;EACnC,WAAW,EAAE,2DAAW;EACxB,SAAS,EAAE,GAAG;EACd,WAAW,EAAE,GAAG;EAChB,KAAK,EAAE,IAAI;;AAEb,sBAAsB;EACpB,WAAW,EAAE,yDAAS;EACtB,WAAW,EAAE,KAAK;EAClB,MAAM,EAAE,CAAC;EACT,cAAc,EAAE,kBAAkB;;AAEpC,CAAC;EACC,MAAM,EAAE,UAAU;;AAEpB,EAAE;EACA,aAAa,EAAE,gBAAgB;EAC/B,WAAW,EAAE,IAAI;EACjB,YAAY,EAAE,IAAI;EAClB,UAAU,EAAE,IAAI;EAChB,MAAM,EAAE,KAAK;;AAEf,GAAG;EACD,mBAAmB,EAAE,IAAI;EACzB,MAAM,EAAE,OAAO;EACf,MAAM,EAAE,CAAC;EACT,SAAS,EAAE,GAAG;;AAEhB,KAAK;EACH,MAAM,EAAE,KAAK;EACb,KAAK,EAAE,KAAK;EACZ,GAAG,EAAE,IAAI;EACT,IAAI,EAAE,IAAI;EACV,OAAO,EAAE,EAAE;;AAEb,oCAAoC;EAClC,KAAK;IACH,MAAM,EAAE,KAAK;IACb,KAAK,EAAE,KAAK;IACZ,GAAG,EAAE,IAAI;IACT,IAAI,EAAE,IAAI;AAEd,oCAAoC;EAClC,KAAK;IACH,MAAM,EAAE,IAAI;IACZ,KAAK,EAAE,IAAI;AAEf,oCAAoC;EAClC,KAAK;IACH,GAAG,EAAE,IAAI;AAEb,oCAAoC;EAClC,KAAK;IACH,GAAG,EAAE,IAAI;IACT,IAAI,EAAE,GAAG;AAEb,cAAc;EACZ,aAAa,EAAE,KAAK;EACpB,KAAK,EAAE,IAAI;;AAEb,MAAM;EACJ,UAAU,EAAE,MAAM;EAClB,WAAW,EAAE,yDAAS;EACtB,SAAS,EAAE,GAAG;EACd,WAAW,EAAE,GAAG;;AAElB,OAAO;EACL,gBAAgB,EAAE,IAAI;;AAExB,qBAAqB;EACnB,MAAM,EAAE,GAAG;EACX,OAAO,EAAE,IAAI;EACb,MAAM,EAAE,gBAAgB;EACxB,SAAS,EAAE,UAAU;EACrB,OAAO,EAAE,YAAY;EACrB,KAAK,EAAE,GAAG;EACV,gBAAgB,EAAE,IAAI;EACtB,uBAAC;IACC,KAAK,EAAE,KAAK;IACZ,eAAe,EAAE,IAAI;IACrB,+BAAS;MACP,KAAK,EAAE,KAAK;;AAElB,mBAAmB;EACjB,OAAO,EAAE,YAAY;EACrB,KAAK,EAAE,IAAI;EACX,cAAc,EAAE,GAAG;EACnB,oBAAoB,EAAE,CAAC;EACvB,iBAAiB,EAAE,CAAC;EACpB,YAAY,EAAE,CAAC;EACf,UAAU,EAAE,KAAI;EAChB,kBAAkB,EAAE,KAAI;EACxB,eAAe,EAAE,KAAI;;AAEvB,eAAe;EACb,SAAS,EAAE,IAAI;EACf,WAAW,EAAE,IAAI;EACjB,aAAa,EAAE,GAAG;EAClB,KAAK,EAAE,IAAI;;AAEb,iBAAiB;EACf,UAAU,EAAE,IAAI;EAChB,OAAO,EAAE,CAAC;;AAEZ,qBAAqB;EACnB,SAAS,EAAE,IAAI;EACf,WAAW,EAAE,IAAI;EACjB,aAAa,EAAE,GAAG;EAClB,KAAK,EAAE,IAAI;EACX,KAAK,EAAE,GAAG;;AAEZ,qBAAqB;EACnB,SAAS,EAAE,IAAI;EACf,WAAW,EAAE,IAAI;EACjB,KAAK,EAAE,IAAI;EACX,KAAK,EAAE,GAAG;EACV,KAAK,EAAE,KAAK", +"sources": ["../../app/stylesheets/application.sass"], +"names": [], +"file": "application.css" +} \ No newline at end of file diff --git a/spec/features/api_spec.rb b/spec/features/api_spec.rb index 291b9033..641f64a6 100644 --- a/spec/features/api_spec.rb +++ b/spec/features/api_spec.rb @@ -392,7 +392,12 @@ get '/v1/cities/stats?city=Springfield&_fields=address,age,height' expect(last_response).to be_ok json_response["results"] = json_response["results"].sort_by { |k| k["age"] } - expect(json_response).to eq(stats_envelope.merge(all_aggregs)) + + age_expected = stats_envelope.merge(all_aggregs)["aggregations"]["age"] + expect(json_response["aggregations"]["age"]["std_deviation"]).to eq(age_expected["std_deviation"]) + + height_expected = stats_envelope.merge(all_aggregs)["aggregations"]["height"] + expect(json_response["aggregations"]["height"]["std_deviation"]).to eq(height_expected["std_deviation"]) end it "/stats requires fields option" do diff --git a/spec/lib/data_magic/config_spec.rb b/spec/lib/data_magic/config_spec.rb index 93e0ac7a..0e02765c 100644 --- a/spec/lib/data_magic/config_spec.rb +++ b/spec/lib/data_magic/config_spec.rb @@ -80,7 +80,6 @@ "version" => "cities100-2010", "index" => "city-data", "api" => "cities", "files" => [{ "name" => "cities100.csv" }], - "data_path" => "./sample-data", "options" => {:search=>"dictionary_only"}, "unique" => ["name"], "data_path" => "./sample-data" diff --git a/spec/lib/data_magic/search_spec.rb b/spec/lib/data_magic/search_spec.rb index 41099833..16915903 100644 --- a/spec/lib/data_magic/search_spec.rb +++ b/spec/lib/data_magic/search_spec.rb @@ -153,7 +153,8 @@ } } - expect(result).to eq(expected) + expect(result["age"]).to eq(expected["age"]) + expect(result["height"]).to eq(expected["height"]) end end From 82235ddc506eaf5a0becb8aafc2d783f51d48b0c Mon Sep 17 00:00:00 2001 From: Sameer Siruguri Date: Tue, 8 Dec 2015 17:03:44 -0800 Subject: [PATCH 5/5] Delete generated CSS files --- public/stylesheets/application.css | 116 ------------------------- public/stylesheets/application.css.map | 7 -- 2 files changed, 123 deletions(-) delete mode 100644 public/stylesheets/application.css delete mode 100644 public/stylesheets/application.css.map diff --git a/public/stylesheets/application.css b/public/stylesheets/application.css deleted file mode 100644 index a925efa9..00000000 --- a/public/stylesheets/application.css +++ /dev/null @@ -1,116 +0,0 @@ -body { - -webkit-font-smoothing: antialiased; - font-family: "Open Sans", "Helvetica Neue", Helvetica, Arial, sans-serif; - font-size: 1em; - line-height: 1.5; - color: #333; } - -h1, h2, h3, h4, h5, h6 { - font-family: "Raleway", "Helvetica Neue", Helvetica, Arial, sans-serif; - line-height: 1.1em; - margin: 0; - text-rendering: optimizeLegibility; } - -p { - margin: 0 0 0.75em; } - -hr { - border-bottom: 1px solid silver; - border-left: none; - border-right: none; - border-top: none; - margin: 1em 0; } - -img { - -webkit-user-select: none; - cursor: zoom-in; - margin: 0; - max-width: 50%; } - -.logo { - height: 150px; - width: 150px; - top: 50px; - left: 50px; - z-index: 20; } - -@media screen and (max-width: 995px) { - .logo { - height: 100px; - width: 100px; - top: 40px; - left: 20px; } } -@media screen and (max-width: 785px) { - .logo { - height: 75px; - width: 75px; } } -@media screen and (max-width: 590px) { - .logo { - top: 73px; } } -@media screen and (max-width: 480px) { - .logo { - top: 16px; - left: 0px; } } -.bottom-margin { - margin-bottom: 0.5em; - color: #c00; } - -.title { - text-align: center; - font-family: "Raleway", "Helvetica Neue", Helvetica, Arial, sans-serif; - font-size: 2em; - line-height: 2em; } - -.header { - background-color: #9cf; } - -.categories .category { - margin: 5px; - padding: 15px; - border: solid 1px silver; - word-wrap: break-word; - display: inline-block; - width: 92%; - background-color: #ffc; } - .categories .category a { - color: black; - text-decoration: none; } - .categories .category a:visited { - color: black; } - -.categories__column { - display: inline-block; - width: 100%; - vertical-align: top; - -webkit-column-count: 2; - -moz-column-count: 2; - column-count: 2; - column-gap: 0.2em; - -webkit-column-gap: 0.2em; - -moz-column-gap: 0.2em; } - -.category__name { - font-size: 18px; - font-weight: bold; - margin-bottom: 5px; - color: #c00; } - -.category__fields { - list-style: none; - padding: 0; } - -.category__field-name { - font-size: 15px; - font-weight: bold; - margin-bottom: 2px; - color: #c00; - width: 80%; } - -.category__field-type { - font-size: 15px; - font-weight: bold; - color: #c00; - width: 10%; - float: right; } - -/*# sourceMappingURL=application.css.map */ diff --git a/public/stylesheets/application.css.map b/public/stylesheets/application.css.map deleted file mode 100644 index dbcb3797..00000000 --- a/public/stylesheets/application.css.map +++ /dev/null @@ -1,7 +0,0 @@ -{ -"version": 3, -"mappings": "AAAA,IAAI;EACF,sBAAsB,EAAE,WAAW;EACnC,WAAW,EAAE,2DAAW;EACxB,SAAS,EAAE,GAAG;EACd,WAAW,EAAE,GAAG;EAChB,KAAK,EAAE,IAAI;;AAEb,sBAAsB;EACpB,WAAW,EAAE,yDAAS;EACtB,WAAW,EAAE,KAAK;EAClB,MAAM,EAAE,CAAC;EACT,cAAc,EAAE,kBAAkB;;AAEpC,CAAC;EACC,MAAM,EAAE,UAAU;;AAEpB,EAAE;EACA,aAAa,EAAE,gBAAgB;EAC/B,WAAW,EAAE,IAAI;EACjB,YAAY,EAAE,IAAI;EAClB,UAAU,EAAE,IAAI;EAChB,MAAM,EAAE,KAAK;;AAEf,GAAG;EACD,mBAAmB,EAAE,IAAI;EACzB,MAAM,EAAE,OAAO;EACf,MAAM,EAAE,CAAC;EACT,SAAS,EAAE,GAAG;;AAEhB,KAAK;EACH,MAAM,EAAE,KAAK;EACb,KAAK,EAAE,KAAK;EACZ,GAAG,EAAE,IAAI;EACT,IAAI,EAAE,IAAI;EACV,OAAO,EAAE,EAAE;;AAEb,oCAAoC;EAClC,KAAK;IACH,MAAM,EAAE,KAAK;IACb,KAAK,EAAE,KAAK;IACZ,GAAG,EAAE,IAAI;IACT,IAAI,EAAE,IAAI;AAEd,oCAAoC;EAClC,KAAK;IACH,MAAM,EAAE,IAAI;IACZ,KAAK,EAAE,IAAI;AAEf,oCAAoC;EAClC,KAAK;IACH,GAAG,EAAE,IAAI;AAEb,oCAAoC;EAClC,KAAK;IACH,GAAG,EAAE,IAAI;IACT,IAAI,EAAE,GAAG;AAEb,cAAc;EACZ,aAAa,EAAE,KAAK;EACpB,KAAK,EAAE,IAAI;;AAEb,MAAM;EACJ,UAAU,EAAE,MAAM;EAClB,WAAW,EAAE,yDAAS;EACtB,SAAS,EAAE,GAAG;EACd,WAAW,EAAE,GAAG;;AAElB,OAAO;EACL,gBAAgB,EAAE,IAAI;;AAExB,qBAAqB;EACnB,MAAM,EAAE,GAAG;EACX,OAAO,EAAE,IAAI;EACb,MAAM,EAAE,gBAAgB;EACxB,SAAS,EAAE,UAAU;EACrB,OAAO,EAAE,YAAY;EACrB,KAAK,EAAE,GAAG;EACV,gBAAgB,EAAE,IAAI;EACtB,uBAAC;IACC,KAAK,EAAE,KAAK;IACZ,eAAe,EAAE,IAAI;IACrB,+BAAS;MACP,KAAK,EAAE,KAAK;;AAElB,mBAAmB;EACjB,OAAO,EAAE,YAAY;EACrB,KAAK,EAAE,IAAI;EACX,cAAc,EAAE,GAAG;EACnB,oBAAoB,EAAE,CAAC;EACvB,iBAAiB,EAAE,CAAC;EACpB,YAAY,EAAE,CAAC;EACf,UAAU,EAAE,KAAI;EAChB,kBAAkB,EAAE,KAAI;EACxB,eAAe,EAAE,KAAI;;AAEvB,eAAe;EACb,SAAS,EAAE,IAAI;EACf,WAAW,EAAE,IAAI;EACjB,aAAa,EAAE,GAAG;EAClB,KAAK,EAAE,IAAI;;AAEb,iBAAiB;EACf,UAAU,EAAE,IAAI;EAChB,OAAO,EAAE,CAAC;;AAEZ,qBAAqB;EACnB,SAAS,EAAE,IAAI;EACf,WAAW,EAAE,IAAI;EACjB,aAAa,EAAE,GAAG;EAClB,KAAK,EAAE,IAAI;EACX,KAAK,EAAE,GAAG;;AAEZ,qBAAqB;EACnB,SAAS,EAAE,IAAI;EACf,WAAW,EAAE,IAAI;EACjB,KAAK,EAAE,IAAI;EACX,KAAK,EAAE,GAAG;EACV,KAAK,EAAE,KAAK", -"sources": ["../../app/stylesheets/application.sass"], -"names": [], -"file": "application.css" -} \ No newline at end of file