diff --git a/.dir-locals.el b/.dir-locals.el index 22c744974495735a0ddd5caa2bb06da629fe2013..e7b3155e06aaea3b222ccac6274c1d90c8c4615b 100644 --- a/.dir-locals.el +++ b/.dir-locals.el @@ -10,6 +10,7 @@ ;; This list isn't complete; add more forms as we come across them. (define-clojure-indent (api-let 2) + (assert 1) (assoc* 1) (auto-parse 1) (catch-api-exceptions 0) diff --git a/project.clj b/project.clj index 3454e3ac94e875bff0876ab9ef96d05ed67d7080..890036e7ce1db63406caa74476e69293c882a488 100644 --- a/project.clj +++ b/project.clj @@ -59,7 +59,8 @@ :init metabase.core/init} :eastwood {:exclude-namespaces [:test-paths] :add-linters [:unused-private-vars] - :exclude-linters [:constant-test]} ; korma macros generate some formats with if statements that are always logically true or false + :exclude-linters [:constant-test ; korma macros generate some forms with if statements that are always logically true or false + :suspicious-expression]} ; core.match macros generate some forms like (and expr) which is "suspicious" :profiles {:dev {:dependencies [[org.clojure/tools.nrepl "0.2.10"] ; REPL <3 [expectations "2.1.1"] ; unit tests [marginalia "0.8.0"] ; for documentation @@ -85,6 +86,7 @@ :jvm-opts ["-Dmb.db.file=target/metabase-test" "-Dmb.jetty.join=false" "-Dmb.jetty.port=3001" - "-Dmb.api.key=test-api-key"]} + "-Dmb.api.key=test-api-key" + "-Xverify:none"]} ; disable bytecode verification when running tests so they start slightly faster :uberjar {:aot :all :prep-tasks ^:replace ["npm" "webpack" "javac" "compile"]}}) diff --git a/resources/migrations/007_add_field_parent_id.json b/resources/migrations/007_add_field_parent_id.json new file mode 100644 index 0000000000000000000000000000000000000000..d2fd5df82bb57753795ffcf4da312627c444771e --- /dev/null +++ b/resources/migrations/007_add_field_parent_id.json @@ -0,0 +1,30 @@ +{ + "databaseChangeLog": [ + { + "changeSet": { + "id": "7", + "author": "cammsaul", + "changes": [ + { + "addColumn": { + "tableName": "metabase_field", + "columns": [ + { + "column": { + "name": "parent_id", + "type": "int", + "constraints": { + "nullable": true, + "references": "metabase_field(id)", + "foreignKeyName": "fk_field_parent_ref_field_id" + } + } + } + ] + } + } + ] + } + } + ] +} diff --git a/resources/migrations/liquibase.json b/resources/migrations/liquibase.json index ba2d99753d644777803e605526db1a1f82a2e165..de127f9b52a8b07a2a4d08da0ed6c46470cef4cf 100644 --- a/resources/migrations/liquibase.json +++ b/resources/migrations/liquibase.json @@ -4,6 +4,7 @@ {"include": {"file": "migrations/002_add_session_table.json"}}, {"include": {"file": "migrations/004_add_setting_table.json"}}, {"include": {"file": "migrations/005_add_org_report_tz_column.json"}}, - {"include": {"file": "migrations/006_disconnect_orgs.json"}} + {"include": {"file": "migrations/006_disconnect_orgs.json"}}, + {"include": {"file": "migrations/007_add_field_parent_id.json"}} ] } diff --git a/src/metabase/api/meta/field.clj b/src/metabase/api/meta/field.clj index eb37aa20e4e387b55f20050122bd62f6a133b84e..274b8aabece45dbedca08fdfd788602cc03c8975 100644 --- a/src/metabase/api/meta/field.clj +++ b/src/metabase/api/meta/field.clj @@ -39,10 +39,10 @@ {field_type FieldType special_type FieldSpecialType} (write-check Field id) - (check-500 (m/mapply upd Field id (merge {:description description ; you're allowed to unset description and special_type - :special_type special_type} ; but field_type and preview_display must be replaced - (when field_type {:field_type field_type}) ; with new non-nil values - (when (not (nil? preview_display)) {:preview_display preview_display})))) + (check-500 (m/mapply upd Field id (merge {:description description ; you're allowed to unset description and special_type + :special_type special_type} ; but field_type and preview_display must be replaced + (when field_type {:field_type field_type}) ; with new non-nil values + (when-not (nil? preview_display) {:preview_display preview_display})))) (Field id)) (defendpoint GET "/:id/summary" diff --git a/src/metabase/db.clj b/src/metabase/db.clj index 1543ca4a64fd560dc30fb40d578a6747cc1ce451..399d83338af19c1d15d376a98d4aa9378f04abb3 100644 --- a/src/metabase/db.clj +++ b/src/metabase/db.clj @@ -176,28 +176,7 @@ ;; ## SEL -(comment - :id->field `(let [[entity# field#] ~entity] - (->> (sel :many :fields [entity# field# :id] ~@forms) - (map (fn [{id# :id field-val# field#}] - {id# field-val#})) - (into {}))) - :field->id `(let [[entity# field#] ~entity] - (->> (sel :many :fields [entity# field# :id] ~@forms) - (map (fn [{id# :id field-val# field#}] - {field-val# id#})) - (into {}))) - :field->field `(let [[entity# field1# field2#] ~entity] - (->> (sel :many entity# ~@forms) - (map (fn [obj#] - {(field1# obj#) (field2# obj#)})) - (into {}))) - :field->obj `(let [[entity# field#] ~entity] - (->> (sel :many entity# ~@forms) - (map (fn [obj#] - {(field# obj#) obj#})) - (into {}))) - ) +(def ^:dynamic *sel-disable-logging* false) (defmacro sel "Wrapper for korma `select` that calls `post-select` on results and provides a few other conveniences. @@ -207,7 +186,8 @@ (sel :one User :id 1) -> returns the User (or nil) whose id is 1 (sel :many OrgPerm :user_id 1) -> returns sequence of OrgPerms whose user_id is 1 - OPTION, if specified, is one of `:field`, `:fields`, `:id`, `:id->field`, `:field->id`, `:field->obj`, or `:id->fields`. + OPTION, if specified, is one of `:field`, `:fields`, `:id`, `:id->field`, `:field->id`, `:field->obj`, `:id->fields`, + `:field->field`, or `:field->fields`. ;; Only return IDs of objects. (sel :one :id User :email \"cam@metabase.com\") -> 120 @@ -236,6 +216,11 @@ -> {\"venues\" {:id 1, :name \"venues\", ...} \"users\" {:id 2, :name \"users\", ...}} + ;; Return a map of field value -> other fields. + (sel :many :field->fields [Table :name :id :db_id]) + -> {\"venues\" {:id 1, :db_id 1} + \"users\" {:id 2, :db_id 1}} + ;; Return a map of ID -> specified fields (sel :many :id->fields [User :first_name :last_name]) -> {1 {:first_name \"Cam\", :last_name \"Saul\"}, diff --git a/src/metabase/db/internal.clj b/src/metabase/db/internal.clj index 2758e3d906c9847dbe7b436b7b7a5afac68bdd8b..8f8cfe6e1f4331f2d4203561244b83b70f39f05c 100644 --- a/src/metabase/db/internal.clj +++ b/src/metabase/db/internal.clj @@ -82,9 +82,10 @@ ;; Log if applicable (future (when (config/config-bool :mb-db-logging) - (log/debug "DB CALL: " (:name entity) - (or (:fields entity+fields) "*") - (s/replace log-str #"korma.core/" "")))) + (when-not @(resolve 'metabase.db/*sel-disable-logging*) + (log/debug "DB CALL: " (:name entity) + (or (:fields entity+fields) "*") + (s/replace log-str #"korma.core/" ""))))) (->> (k/exec (select-fn entity+fields)) (map (partial models/internal-post-select entity)) @@ -136,6 +137,17 @@ f2# ~f2] (sel:field->field* f1# f2# (sel* [~entity f1# f2#] ~@forms)))) +;;; :field->fields + +(defn sel:field->fields* [key-field other-fields results] + (into {} (for [result results] + {(key-field result) (select-keys result other-fields)}))) + +(defmacro sel:field->fields [[entity key-field & other-fields] & forms] + `(let [key-field# ~key-field + other-fields# ~(vec other-fields)] + (sel:field->fields* key-field# other-fields# (sel* `[~~entity ~key-field# ~@other-fields#] ~@forms)))) + ;;; : id->field (defmacro sel:id->field [[entity field] & forms] diff --git a/src/metabase/driver.clj b/src/metabase/driver.clj index a389355d6c05c813314f8cca98e0e421db549f9c..b00dc02355638ae0d3c06384581e38f3e52ab4bb 100644 --- a/src/metabase/driver.clj +++ b/src/metabase/driver.clj @@ -32,21 +32,26 @@ :name "MongoDB" :example "mongodb://password:username@127.0.0.1:27017/db-name"}}) -(def ^:const class->base-type - "Map of classes returned from DB call to metabase.models.field/base-types" - {java.lang.Boolean :BooleanField - java.lang.Double :FloatField - java.lang.Float :FloatField - java.lang.Integer :IntegerField - java.lang.Long :IntegerField - java.lang.String :TextField - java.math.BigDecimal :DecimalField - java.math.BigInteger :BigIntegerField - java.sql.Date :DateField - java.sql.Timestamp :DateTimeField - java.util.Date :DateField - java.util.UUID :TextField - org.postgresql.util.PGobject :UnknownField}) ; this mapping included here since Native QP uses class->base-type directly. TODO - perhaps make *class-base->type* driver specific? +(defn class->base-type + "Return the `Field.base_type` that corresponds to a given class returned by the DB." + [klass] + (or ({Boolean :BooleanField + Double :FloatField + Float :FloatField + Integer :IntegerField + Long :IntegerField + String :TextField + java.math.BigDecimal :DecimalField + java.math.BigInteger :BigIntegerField + java.sql.Date :DateField + java.sql.Timestamp :DateTimeField + java.util.Date :DateField + java.util.UUID :TextField + org.postgresql.util.PGobject :UnknownField} klass) + (cond + (isa? klass clojure.lang.IPersistentMap) :DictionaryField) + (do (log/warn (format "Don't know how to map class '%s' to a Field base_type, falling back to :UnknownField." klass)) + :UnknownField))) ;; ## Driver Lookup diff --git a/src/metabase/driver/generic_sql.clj b/src/metabase/driver/generic_sql.clj index 505686f572484d5b51f11e4bf8781e72601d07e4..f281f15ed2d77176f21be958cd714be6afb6150c 100644 --- a/src/metabase/driver/generic_sql.clj +++ b/src/metabase/driver/generic_sql.clj @@ -45,6 +45,10 @@ first)))) ;; Query Processing + (wrap-process-query-middleware [_ qp] + (fn [query] + (qp query))) ; Nothing to do here + (process-query [_ query] (qp/process-and-run query)) diff --git a/src/metabase/driver/generic_sql/native.clj b/src/metabase/driver/generic_sql/native.clj index cd36f36f5b5609b8b9d9e17cb73f1a77c898a8d5..146fcb203b1f2852ef0dc49fb0c28495285e94c8 100644 --- a/src/metabase/driver/generic_sql/native.clj +++ b/src/metabase/driver/generic_sql/native.clj @@ -13,11 +13,7 @@ (defn- value->base-type "Attempt to match a value we get back from the DB with the corresponding base-type`." [v] - (if-not v :UnknownField - (or (driver/class->base-type (type v)) - (do (log/warn (format "Missing base type mapping for %s in driver/class->base-type. Please add an entry." - (str (type v)))) - :UnknownField)))) + (driver/class->base-type (type v))) (defn process-and-run "Process and run a native (raw SQL) QUERY." diff --git a/src/metabase/driver/generic_sql/query_processor.clj b/src/metabase/driver/generic_sql/query_processor.clj index 4261ff59f79954a78e0d75492c4331cf612f6a1c..d3e704c4769be4b367f8ee05e0b8f21a7ea2b208 100644 --- a/src/metabase/driver/generic_sql/query_processor.clj +++ b/src/metabase/driver/generic_sql/query_processor.clj @@ -19,19 +19,20 @@ ;; # INTERFACE + +(def ^:dynamic ^:private *query* nil) + (defn- uncastify "Remove CAST statements from a column name if needed. (uncastify \"DATE\") -> \"DATE\" (uncastify \"CAST(DATE AS DATE)\") -> \"DATE\"" - [column-name] + [driver column-name] (let [column-name (name column-name)] (keyword (or (second (re-find #"CAST\([^.\s]+\.([^.\s]+) AS [\w]+\)" column-name)) - (second (re-find (:uncastify-timestamp-regex qp/*driver*) column-name)) + (second (re-find (:uncastify-timestamp-regex driver) column-name)) column-name)))) -(def ^:dynamic ^:private *query* nil) - (defn process-structured "Convert QUERY into a korma `select` form, execute it, and annotate the results." [{{:keys [source-table]} :query, database :database, :as query}] @@ -43,7 +44,7 @@ (filter identity) (mapcat #(if (vector? %) % [%])))) set-timezone-sql (when-let [timezone (:timezone (:details database))] - (when-let [set-timezone-sql (:timezone->set-timezone-sql qp/*driver*)] + (when-let [set-timezone-sql (:timezone->set-timezone-sql (:driver *query*))] `(exec-raw ~(set-timezone-sql timezone)))) korma-form `(let [~entity (korma-entity ~database ~source-table)] ~(if set-timezone-sql `(korma.db/with-db (:db ~entity) @@ -58,7 +59,7 @@ (let [results (eval korma-form)] {:results results - :uncastify-fn uncastify})) + :uncastify-fn (partial uncastify (:driver query))})) (catch java.sql.SQLException e (let [^String message (or (->> (.getMessage e) ; error message comes back like "Error message ... [status-code]" sometimes @@ -95,33 +96,50 @@ (defprotocol IGenericSQLFormattable - (formatted [this])) + (formatted [this] [this include-as?])) (extend-protocol IGenericSQLFormattable Field - (formatted [{:keys [table-name field-name base-type special-type]}] - ;; TODO - add Table names - (cond - (contains? #{:DateField :DateTimeField} base-type) `(raw ~(format "CAST(\"%s\".\"%s\" AS DATE)" table-name field-name)) - (= special-type :timestamp_seconds) `(raw ~((:cast-timestamp-seconds-field-to-date-fn qp/*driver*) table-name field-name)) - (= special-type :timestamp_milliseconds) `(raw ~((:cast-timestamp-milliseconds-field-to-date-fn qp/*driver*) table-name field-name)) - :else (keyword (format "%s.%s" table-name field-name)))) + (formatted + ([this] + (formatted this false)) + ([{:keys [table-name field-name base-type special-type]} include-as?] + ;; TODO - add Table names + (cond + (contains? #{:DateField :DateTimeField} base-type) `(raw ~(str (format "CAST(\"%s\".\"%s\" AS DATE)" table-name field-name) + (when include-as? + (format " AS \"%s\"" field-name)))) + (= special-type :timestamp_seconds) `(raw ~(str ((:cast-timestamp-seconds-field-to-date-fn (:driver *query*)) table-name field-name) + (when include-as? + (format " AS \"%s\"" field-name)))) + (= special-type :timestamp_milliseconds) `(raw ~(str ((:cast-timestamp-milliseconds-field-to-date-fn (:driver *query*)) table-name field-name) + (when include-as? + (format " AS \"%s\"" field-name)))) + :else (keyword (format "%s.%s" table-name field-name))))) + ;; e.g. the ["aggregation" 0] fields we allow in order-by OrderByAggregateField - (formatted [_] - (let [{:keys [aggregation-type]} (:aggregation (:query *query*))] ; determine the name of the aggregation field - `(raw ~(case aggregation-type - :avg "\"avg\"" - :count "\"count\"" - :distinct "\"count\"" - :stddev "\"stddev\"" - :sum "\"sum\"")))) + (formatted + ([this] + (formatted this false)) + ([_ _] + (let [{:keys [aggregation-type]} (:aggregation (:query *query*))] ; determine the name of the aggregation field + `(raw ~(case aggregation-type + :avg "\"avg\"" + :count "\"count\"" + :distinct "\"count\"" + :stddev "\"stddev\"" + :sum "\"sum\""))))) + Value - (formatted [{:keys [value]}] - (if-not (instance? java.util.Date value) value - `(raw ~(format "CAST('%s' AS DATE)" (.toString ^java.util.Date value)))))) + (formatted + ([this] + (formatted this false)) + ([{:keys [value]} _] + (if-not (instance? java.util.Date value) value + `(raw ~(format "CAST('%s' AS DATE)" (.toString ^java.util.Date value))))))) (defmethod apply-form :aggregation [[_ {:keys [aggregation-type field]}]] @@ -147,11 +165,11 @@ ;; Add fields form only for fields that weren't specified in :fields clause -- we don't want to include it twice, or korma will barf (fields ~@(->> fields (filter (partial (complement contains?) (set (:fields (:query *query*))))) - (map formatted)))]) + (map (u/rpartial formatted :include-as))))]) (defmethod apply-form :fields [[_ fields]] - `(fields ~@(map formatted fields))) + `(fields ~@(map (u/rpartial formatted :include-as) fields))) (defn- filter-subclause->predicate diff --git a/src/metabase/driver/interface.clj b/src/metabase/driver/interface.clj index d13bef75b43f6e1dba62410a06086011607dc604..928df060f7b637ffcc659e6eeaa6429e005d2c67 100644 --- a/src/metabase/driver/interface.clj +++ b/src/metabase/driver/interface.clj @@ -5,6 +5,7 @@ (def ^:const driver-optional-features "A set on optional features (as keywords) that may or may not be supported by individual drivers." #{:foreign-keys + :nested-fields ; are nested Fields (i.e., Mongo-style nested keys) supported? :set-timezone :standard-deviation-aggregations :unix-timestamp-special-type-fields}) @@ -52,7 +53,13 @@ (process-query [this query] "Process a native or structured query. (Don't use this directly; instead, use `metabase.driver/process-query`, - which does things like preprocessing before calling the appropriate implementation.)")) + which does things like preprocessing before calling the appropriate implementation.)") + (wrap-process-query-middleware [this qp-fn] + "Custom QP middleware for this driver. + Like `sync-in-context`, but for running queries rather than syncing. This is basically around-advice for the QP pre and post-processing stages. + This should be used to do things like open DB connections that need to remain open for the duration of post-processing. + This middleware is injected into the QP middleware stack immediately after the Query Expander; in other words, it will receive the expanded query. + See the Mongo driver for and example of how this is intended to be used.")) ;; ## ISyncDriverTableFKs Protocol (Optional) @@ -69,12 +76,19 @@ * dest-column-name")) +(defprotocol ISyncDriverFieldNestedFields + "Optional protocol that should provide information about the subfields of a FIELD when applicable. + Drivers that declare support for `:nested-fields` should implement this protocol." + (active-nested-field-name->type [this field] + "Return a map of string names of active child `Fields` of FIELD -> `Field.base_type`.")) + + ;; ## ISyncDriverField Protocols ;; Sync drivers need to implement either ISyncDriverFieldValues or ISyncDriverFieldAvgLength *and* ISyncDriverFieldPercentUrls. ;; ;; ISyncDriverFieldValues is used to provide a generic fallback implementation of the other two that calculate these values by -;; iterating over *every* value of the Field in Clojure-land. Since that's slower, it's preferable to provide implementations +;; iterating over a few thousand values of the Field in Clojure-land. Since that's slower, it's preferable to provide implementations ;; of ISyncDriverFieldAvgLength/ISyncDriverFieldPercentUrls when possible. (You can also implement ISyncDriverFieldValues and ;; *one* of the other two; the optimized implementation will be used for that and the fallback implementation for the other) diff --git a/src/metabase/driver/mongo.clj b/src/metabase/driver/mongo.clj index 37f3b1d04469eb1a58d710a5cea619ad3d4b9a2e..826c97fe99ff016f4c6ca9f25000d027be3980e1 100644 --- a/src/metabase/driver/mongo.clj +++ b/src/metabase/driver/mongo.clj @@ -4,6 +4,7 @@ [clojure.set :as set] [clojure.tools.logging :as log] [colorize.core :as color] + [medley.core :as m] (monger [collection :as mc] [command :as cmd] [conversion :as conv] @@ -13,10 +14,18 @@ [metabase.driver :as driver] [metabase.driver.interface :refer :all] (metabase.driver.mongo [query-processor :as qp] - [util :refer [*mongo-connection* with-mongo-connection values->base-type]]))) + [util :refer [*mongo-connection* with-mongo-connection values->base-type]]) + [metabase.util :as u])) (declare driver) +;; TODO - this isn't necessarily Mongo-specific +(def ^:private ^:const document-scanning-limit + "The maximum number of documents to scan to look for Fields. + We can't feasibly scan every document in a million+ document collection, so scan the first `document-scanning-limit` + documents and hope that the rest follow the same schema." + 10000) + ;;; ### Driver Helper Fns (defn- table->column-names @@ -24,7 +33,7 @@ [table] (with-mongo-connection [^com.mongodb.DBApiLayer conn @(:db table)] (->> (mc/find-maps conn (:name table)) - (take 10000) ; it's probably enough to only consider the first 10,000 docs in the collection instead of iterating over potentially millions of them + (take document-scanning-limit) (map keys) (map set) (reduce set/union)))) @@ -42,7 +51,7 @@ (def ^:const ^:private mongo-driver-features "Optional features supported by the Mongo driver." - #{}) ; nothing yet + #{:nested-fields}) (deftype MongoDriver [] IDriver @@ -62,6 +71,11 @@ (can-connect? this {:details details})) ;;; ### QP + (wrap-process-query-middleware [_ qp] + (fn [query] + (with-mongo-connection [^com.mongodb.DBApiLayer conn (:database query)] + (qp query)))) + (process-query [_ query] (qp/process-and-run query)) @@ -77,25 +91,49 @@ (active-column-names->type [_ table] (with-mongo-connection [_ @(:db table)] - (->> (table->column-names table) - (map (fn [column-name] - {(name column-name) - (field->base-type {:name (name column-name) - :table (delay table)})})) - (into {})))) + (into {} (for [column-name (table->column-names table)] + {(name column-name) + (field->base-type {:name (name column-name) + :table (delay table) + :qualified-name-components (delay [(:name table) (name column-name)])})})))) (table-pks [_ _] #{"_id"}) ISyncDriverFieldValues - (field-values-lazy-seq [_ field] + (field-values-lazy-seq [_ {:keys [qualified-name-components table], :as field}] + (assert (and (map? field) + (delay? qualified-name-components) + (delay? table)) + (format "Field is missing required information:\n%s" (u/pprint-to-str 'red field))) (lazy-seq - (let [table @(:table field)] - (map (keyword (:name field)) - (with-mongo-connection [^com.mongodb.DBApiLayer conn @(:db table)] - (mq/with-collection conn (:name table) - (mq/fields [(:name field)])))))))) - -(def ^:const driver + (assert *mongo-connection* + "You must have an open Mongo connection in order to get lazy results with field-values-lazy-seq.") + (let [table @table + name-components (rest @qualified-name-components)] + (assert (seq name-components)) + (map #(get-in % (map keyword name-components)) + (mq/with-collection *mongo-connection* (:name table) + (mq/fields [(apply str (interpose "." name-components))])))))) + + ISyncDriverFieldNestedFields + (active-nested-field-name->type [this field] + ;; Build a map of nested-field-key -> type -> count + ;; TODO - using an atom isn't the *fastest* thing in the world (but is the easiest); consider alternate implementation + (let [field->type->count (atom {})] + (doseq [val (take document-scanning-limit (field-values-lazy-seq this field))] + (when (map? val) + (doseq [[k v] val] + (swap! field->type->count update-in [k (type v)] #(if % (inc %) 1))))) + ;; (seq types) will give us a seq of pairs like [java.lang.String 500] + (->> @field->type->count + (m/map-vals (fn [type->count] + (->> (seq type->count) ; convert to pairs of [type count] + (sort-by second) ; source by count + last ; take last item (highest count) + first ; keep just the type + driver/class->base-type))))))) ; get corresponding Field base_type + +(def driver "Concrete instance of the MongoDB driver." (MongoDriver.)) diff --git a/src/metabase/driver/mongo/query_processor.clj b/src/metabase/driver/mongo/query_processor.clj index e605a93b6651d8e22c97075be023ce1c32aa2c31..e663381ffc262be87ae4746029644b7b7edc9643 100644 --- a/src/metabase/driver/mongo/query_processor.clj +++ b/src/metabase/driver/mongo/query_processor.clj @@ -1,7 +1,10 @@ (ns metabase.driver.mongo.query-processor (:refer-clojure :exclude [find sort]) (:require [clojure.core.match :refer [match]] + (clojure [set :as set] + [string :as s]) [clojure.tools.logging :as log] + [clojure.walk :as walk] [colorize.core :as color] (monger [collection :as mc] [core :as mg] @@ -12,6 +15,7 @@ [metabase.driver :as driver] (metabase.driver [interface :as i] [query-processor :as qp]) + [metabase.driver.query-processor.expand :as expand] [metabase.driver.mongo.util :refer [with-mongo-connection *mongo-connection* values->base-type]] [metabase.models.field :refer [Field]] [metabase.util :as u]) @@ -32,19 +36,19 @@ (defn process-and-run "Process and run a MongoDB QUERY." - [{query-type :type, database :database, :as query}] + [{query-type :type, :as query}] (binding [*query* query] - (with-mongo-connection [_ database] - (case (keyword query-type) - :query (let [generated-query (process-structured (:query query))] - (when-not qp/*disable-qp-logging* - (log/debug (color/magenta "\n\n******************** Generated Monger Query: ********************\n" - (u/pprint-to-str generated-query) - "\n*****************************************************************\n"))) - {:results (eval generated-query)}) - :native (let [results (eval-raw-command (:query (:native query)))] - {:results (if (sequential? results) results - [results])}))))) + (case (keyword query-type) + :query (let [generated-query (process-structured (:query query))] + (when-not qp/*disable-qp-logging* + (log/debug (u/format-color 'green "\nMONGER FORM:\n%s\n" + (->> generated-query + (walk/postwalk #(if (symbol? %) (symbol (name %)) %)) ; strip namespace qualifiers from Monger form + u/pprint-to-str) "\n"))) ; so it's easier to read + {:results (eval generated-query)}) + :native (let [results (eval-raw-command (:query (:native query)))] + {:results (if (sequential? results) results + [results])})))) ;; # NATIVE QUERY PROCESSOR @@ -82,10 +86,17 @@ [{$match *constraints*}]) ~@(filter identity forms)])) -(defn field->$str +(defn- field->name + "Return qualified string name of FIELD, e.g. `venue` or `venue.address`." + (^String [field separator] + (apply str (interpose separator (rest (expand/qualified-name-components field))))) ; drop the first part, :table-name + (^String [field] + (field->name field "."))) + +(defn- field->$str "Given a FIELD, return a `$`-qualified field name for use in a Mongo aggregate query, e.g. `\"$user_id\"`." [field] - (format "$%s" (name (:field-name field)))) + (format "$%s" (field->name field))) (defn- aggregation:rows [] `(doall (with-collection ^DBApiLayer *mongo-connection* ~*collection-name* @@ -99,7 +110,7 @@ ([field] `[{:count (mc/count ^DBApiLayer *mongo-connection* ~*collection-name* (merge ~*constraints* - {(:field-name field) {$exists true}}))}])) + {~(field->name field) {$exists true}}))}])) (defn- aggregation:avg [field] (aggregate {$group {"_id" nil @@ -170,36 +181,63 @@ :avg ["avg" {$avg (field->$str field)}] :sum ["sum" {$sum (field->$str field)}]))) -(defn do-breakout +;;; BREAKOUT FIELD NAME ESCAPING FOR $GROUP +;; We're not allowed to use field names that contain a period in the Mongo aggregation $group stage. +;; Not OK: +;; {"$group" {"source.username" {"$first" {"$source.username"}, "_id" "$source.username"}}, ...} +;; +;; For *nested* Fields, we'll replace the '.' with '___', and restore the original names afterward. +;; Escaped: +;; {"$group" {"source___username" {"$first" {"$source.username"}, "_id" "$source.username"}}, ...} + +(defn ag-unescape-nested-field-names + "Restore the original, unescaped nested Field names in the keys of RESULTS. + E.g. `:source___service` becomes `:source.service`" + [results] + ;; Build a map of escaped key -> unescaped key by looking at the keys in the first result + ;; e.g. {:source___username :source.username} + (let [replacements (into {} (for [k (keys (first results))] + (let [k-str (name k) + unescaped (s/replace k-str #"___" ".")] + (when-not (= k-str unescaped) + {k (keyword unescaped)}))))] + ;; If the map is non-empty then map set/rename-keys over the results with it + (if-not (seq replacements) + results + (for [row results] + (set/rename-keys row replacements))))) + +(defn- do-breakout "Generate a Monger query from a structured QUERY dictionary that contains a `breakout` clause. Since the Monger query we generate looks very different from ones we generate when no `breakout` clause is present, this is essentialy a separate implementation :/" [{aggregation :aggregation, breakout-fields :breakout, order-by :order-by, limit :limit, :as query}] - (let [[ag-field ag-clause] (breakout-aggregation->field-name+expression aggregation) - fields (map :field-name breakout-fields) + (let [;; Shadow the top-level definition of field->name with one that will use "___" as the separator instead of "." + field->escaped-name (u/rpartial field->name "___") + [ag-field ag-clause] (breakout-aggregation->field-name+expression aggregation) + fields (map field->escaped-name breakout-fields) $fields (map field->$str breakout-fields) fields->$fields (zipmap fields $fields)] - (aggregate {$group (merge {"_id" (if (= (count fields) 1) (first $fields) - fields->$fields)} - (when (and ag-field ag-clause) - {ag-field ag-clause}) - (->> fields->$fields - (map (fn [[field $field]] - (when-not (= field "_id") - {field {$first $field}}))) - (into {})))} - {$sort (->> order-by - (mapcat (fn [{:keys [field direction]}] - [(:field-name field) (case direction - :ascending 1 - :descending -1)])) - (apply sorted-map))} - {$project (merge {"_id" false} - (when ag-field - {ag-field true}) - (zipmap fields (repeat true)))} - (when limit - {$limit limit})))) + `(ag-unescape-nested-field-names + ~(aggregate {$group (merge {"_id" (if (= (count fields) 1) (first $fields) + fields->$fields)} + (when (and ag-field ag-clause) + {ag-field ag-clause}) + (into {} (for [[field $field] fields->$fields] + (when-not (= field "_id") + {field {$first $field}}))))} + {$sort (->> order-by + (mapcat (fn [{:keys [field direction]}] + [(field->escaped-name field) (case direction + :ascending 1 + :descending -1)])) + (apply sorted-map))} + {$project (merge {"_id" false} + (when ag-field + {ag-field true}) + (zipmap fields (repeat true)))} + (when limit + {$limit limit}))))) ;; ## PROCESS-STRUCTURED @@ -241,7 +279,7 @@ ;; ### fields (defclause :fields fields - `[(fields ~(mapv :field-name fields))]) + `[(fields ~(mapv field->name fields))]) ;; ### filter @@ -254,13 +292,13 @@ value)) (defn- parse-filter-subclause [{:keys [filter-type field value] :as filter}] - (let [field (when field (:field-name field)) + (let [field (when field (field->name field)) value (when value (format-value value))] (case filter-type :inside (let [lat (:lat filter) lon (:lon filter)] - {$and [{(:field-name (:field lat)) {$gte (format-value (:min lat)), $lte (format-value (:max lat))}} - {(:field-name (:field lon)) {$gte (format-value (:min lon)), $lte (format-value (:max lon))}}]}) + {$and [{(field->name (:field lat)) {$gte (format-value (:min lat)), $lte (format-value (:max lat))}} + {(field->name (:field lon)) {$gte (format-value (:min lon)), $lte (format-value (:max lon))}}]}) :between {field {$gte (format-value (:min-val filter)) $lte (format-value (:max-val filter))}} :is-null {field {$exists false}} @@ -290,7 +328,7 @@ ;; ### order_by (defclause :order-by subclauses (let [sort-options (mapcat (fn [{:keys [field direction]}] - [(:field-name field) (case direction + [(field->name field) (case direction :ascending 1 :descending -1)]) subclauses)] diff --git a/src/metabase/driver/mongo/util.clj b/src/metabase/driver/mongo/util.clj index d97786f7ac26f6c5b8d6ff421886c21772d14382..595d7ed94bd8db733d2e68bf4641152ae5257eeb 100644 --- a/src/metabase/driver/mongo/util.clj +++ b/src/metabase/driver/mongo/util.clj @@ -34,7 +34,7 @@ "?connectTimeoutMS=" connection-timeout-ms)) -(def ^:dynamic *mongo-connection* +(def ^:dynamic ^com.mongodb.DBApiLayer *mongo-connection* "Connection to a Mongo database. Bound by top-level `with-mongo-connection` so it may be reused within its body." nil) @@ -48,7 +48,7 @@ (:dbname (:details database)) (details-map->connection-string (:details database)) ; new-style -- entire Database obj (:dbname database) (details-map->connection-string database) ; new-style -- connection details map only :else (throw (Exception. (str "with-mongo-connection failed: bad connection details:" (:details database))))) - {conn :conn mongo-connection :db} (mg/connect-via-uri connection-string)] + {conn :conn, mongo-connection :db} (mg/connect-via-uri connection-string)] (log/debug (color/cyan "<< OPENED NEW MONGODB CONNECTION >>")) (try (binding [*mongo-connection* mongo-connection] @@ -76,22 +76,25 @@ (if *mongo-connection* (f# *mongo-connection*) (-with-mongo-connection f# ~database)))) -;; TODO - this is actually more sophisticated than the one used for annotation in the GenericSQL driver, which just takes the -;; types of the values in the first row. -;; We should move this somewhere where it can be shared amongst the drivers and rewrite GenericSQL to use it instead. +;; TODO - this isn't neccesarily Mongo-specific; consider moving (defn values->base-type - "Given a sequence of values, return `Field` `base_type` in the most ghetto way possible. + "Given a sequence of values, return `Field.base_type` in the most ghetto way possible. This just gets counts the types of *every* value and returns the `base_type` for class whose count was highest." [values-seq] {:pre [(sequential? values-seq)]} (or (->> values-seq - (filter identity) ; TODO - why not do a query to return non-nil values of this column instead - (take 1000) ; it's probably fine just to consider the first 1,000 non-nil values when trying to type a column instead of iterating over the whole collection + ;; TODO - why not do a query to return non-nil values of this column instead + (filter identity) + ;; it's probably fine just to consider the first 1,000 *non-nil* values when trying to type a column instead + ;; of iterating over the whole collection. (VALUES-SEQ should be up to 10,000 values, but we don't know how many are + ;; nil) + (take 1000) (group-by type) - (map (fn [[type valus]] - [type (count valus)])) + ;; create tuples like [Integer count]. + (map (fn [[klass valus]] + [klass (count valus)])) (sort-by second) - first - first - driver/class->base-type) + last ; last result will be tuple with highest count + first ; keep just the type + driver/class->base-type) ; convert to Field base_type :UnknownField)) diff --git a/src/metabase/driver/query_processor.clj b/src/metabase/driver/query_processor.clj index 2293f62b8a719ee4a44a04978f5413a6d831043f..25703d2fd7f1ac176e2d44b2ac65da29bf5ddacd 100644 --- a/src/metabase/driver/query_processor.clj +++ b/src/metabase/driver/query_processor.clj @@ -9,7 +9,7 @@ [metabase.db :refer :all] [metabase.driver.interface :as i] [metabase.driver.query-processor.expand :as expand] - (metabase.models [field :refer [Field]] + (metabase.models [field :refer [Field], :as field] [foreign-key :refer [ForeignKey]]) [metabase.util :as u])) @@ -30,10 +30,6 @@ "Should we disable logging for the QP? (e.g., during sync we probably want to turn it off to keep logs less cluttered)." false) -(def ^:dynamic *driver* - "The driver currently being used to process this query." - (atom nil)) - ;; +----------------------------------------------------------------------------------------------------+ ;; | QP INTERNAL IMPLEMENTATION | @@ -50,7 +46,7 @@ (defn- pre-expand [qp] (fn [query] - (qp (expand/expand *driver* query)))) + (qp (expand/expand query)))) (defn- post-add-row-count-and-status @@ -73,8 +69,8 @@ (qp (if (or (not (= ag-type :rows)) breakout fields) query (-> query (assoc-in [:query :fields-is-implicit] true) - (assoc-in [:query :fields] (->> (sel :many [Field :name :base_type :special_type :table_id], :table_id source-table-id, :active true, - :preview_display true, :field_type [not= "sensitive"], (k/order :position :asc), (k/order :id :desc)) + (assoc-in [:query :fields] (->> (sel :many :fields [Field :name :base_type :special_type :table_id], :table_id source-table-id, :active true, + :preview_display true, :field_type [not= "sensitive"], :parent_id nil, (k/order :position :asc), (k/order :id :desc)) (map expand/rename-mb-field-keys) (map expand/map->Field) (map #(expand/resolve-table % {source-table-id source-table}))))))))) @@ -171,8 +167,7 @@ (defn- cumulative-sum [qp] (fn [query] - (let [[cumulative-sum-field query] (pre-cumulative-sum query) - results (qp query)] + (let [[cumulative-sum-field query] (pre-cumulative-sum query)] (cond->> (qp query) cumulative-sum-field (post-cumulative-sum cumulative-sum-field))))) @@ -180,9 +175,10 @@ (defn- limit "Add an implicit `limit` clause to queries with `rows` aggregations, and limit the maximum number of rows that can be returned in post-processing." [qp] - (fn [{{{ag-type :aggregation-type} :aggregation} :query, :as query}] + (fn [{{{ag-type :aggregation-type} :aggregation, limit :limit} :query, :as query}] (let [query (cond-> query - (= ag-type :rows) (assoc :limit max-result-bare-rows)) + (and (not limit) + (= ag-type :rows)) (assoc-in [:query :limit] max-result-bare-rows)) results (qp query)] (update-in results [:rows] (partial take max-result-rows))))) @@ -226,13 +222,20 @@ (defn- order-cols "Construct a sequence of column keywords that should be used for pulling ordered rows from RESULTS. FIELDS should be a sequence of all `Fields` for the `Table` associated with QUERY." - [{{breakout-fields :breakout, fields-fields :fields, fields-is-implicit :fields-is-implicit} :query} results fields] - {:post [(= (set %) - (set (keys (first results))))]} - (let [;; TODO - This function was written before the advent of the expanded query it is designed to work with Field IDs rather than expanded forms - ;; Since this logic is delecate I've side-stepped the issue by converting the expanded Fields back to IDs for the time being. - ;; We should carefully re-work this function to use expanded Fields so we don't need the complicated logic below to fetch their names + [{{breakout-fields :breakout, {ag-type :aggregation-type} :aggregation, fields-fields :fields, fields-is-implicit :fields-is-implicit} :query} results fields] + (let [;; Get all the column name keywords returned by the results + result-kws (set (keys (first results))) + valid-kw? (partial contains? result-kws) + breakout-ids (map :field-id breakout-fields) + + breakout-kws (->> (for [field breakout-fields] + (->> (rest (expand/qualified-name-components field)) ; TODO - this "qualified name for results" should be calculated in the Query expander + (interpose ".") + (apply str) + keyword)) + (filter valid-kw?)) + fields-ids (map :field-id fields-fields) field-id->field (zipmap (map :id fields) fields) @@ -241,23 +244,20 @@ fields-ids (when-not fields-is-implicit fields-ids) all-field-ids (->> fields ; Sort the Fields. (sort-by (fn [{:keys [position special_type name]}] ; For each field generate a vector of - [position ; [position special-type-group name] - (cond ; and Clojure will take care of the rest. + [position ; [position special-type-group name] + (cond ; and Clojure will take care of the rest. (= special_type :id) 0 (= special_type :name) 1 :else 2) name])) (map :id)) ; Return the sorted IDs - ;; Concat the Fields clause IDs + the sequence of all Fields ID for the Table. - ;; Then filter out ones that appear in breakout clause and remove duplicates - ;; which effectively gives us parts #3 and #4 from above. - non-breakout-ids (->> (concat fields-ids all-field-ids) - (filter (complement (partial contains? (set breakout-ids)))) - distinct) - - ;; Get all the column name keywords returned by the results - result-kws (set (keys (first results))) + ;; Get the aggregate column if any + ag-kws (when (and ag-type + (not= ag-type :rows)) + (let [ag (if (= ag-type :distinct) :count + ag-type)] + [ag])) ;; Make a helper function that will take a sequence of Field IDs and convert them to corresponding column name keywords. ;; Don't include names that aren't part of RESULT-KWS: we fetch *all* the Fields for a Table regardless of the Query, so @@ -266,23 +266,38 @@ (some->> (map field-id->field field-ids) (map :name) (map keyword) - (filter (partial contains? result-kws)))) + (filter valid-kw?))) + + ;; Concat the Fields clause IDs + the sequence of all Fields ID for the Table. + ;; Then filter out ones that appear in breakout clause and remove duplicates + ;; which effectively gives us parts #3 and #4 from above. + non-breakout-ids (->> (concat fields-ids all-field-ids) + (filter (complement (partial contains? (set breakout-ids)))) + distinct) + + ;; Use fn above to get the keyword column names of other non-aggregation fields [#3 and #4] + non-breakout-kws (->> (ids->kws non-breakout-ids) + (filter (complement (partial contains? (set ag-kws))))) - ;; Use fn above to get the keyword column names of breakout clause fields [#1] + fields clause fields / other non-aggregation fields [#3 and #4] - breakout-kws (ids->kws breakout-ids) - non-breakout-kws (ids->kws non-breakout-ids) + ;; Collect all other Fields + other-kws (->> result-kws + (filter (complement (partial contains? (set (concat breakout-kws non-breakout-kws ag-kws))))) + sort)] ; sort by name so results are deterministic - ;; Now get all the keyword column names specific to aggregation, such as :sum or :count [#2]. - ;; Just get all the items in RESULT-KWS that *aren't* part of BREAKOUT-KWS or NON-BREAKOUT-KWS - ag-kws (->> result-kws - ;; TODO - Currently, this will never be more than a single Field, since we only - ;; support a single aggregation clause at this point. When we add support for - ;; multiple aggregation clauses, we'll need to add some logic to make sure they're - ;; being ordered correctly, e.g. the first aggregate column before the second, etc. - (filter (complement (partial contains? (set (concat breakout-kws non-breakout-kws))))))] + (when (seq other-kws) + (log/warn (u/format-color 'red "Warning: not 100%% sure how to order these columns: %s" (vec other-kws)))) ;; Now combine the breakout [#1] + aggregate [#2] + "non-breakout" [#3 & #4] column name keywords into a single sequence - (concat breakout-kws ag-kws non-breakout-kws))) + (when-not *disable-qp-logging* + (log/debug (u/format-color 'magenta "Using this ordering: breakout: %s, ag: %s, non-breakout: %s, other: %s" + (vec breakout-kws) (vec ag-kws) (vec non-breakout-kws) (vec other-kws)))) + + (let [ordered-kws (concat breakout-kws ag-kws non-breakout-kws other-kws)] + (assert (and (= (set ordered-kws) result-kws) + (= (count ordered-kws) (count result-kws))) + (format "Order-cols returned invalid results: expected %s, got %s\nbreakout: %s, ag: %s, non-breakout: %s, other: %s" result-kws (vec ordered-kws) + (vec breakout-kws) (vec ag-kws) (vec non-breakout-kws) (vec other-kws))) + ordered-kws))) (defn- add-fields-extra-info "Add `:extra_info` about `ForeignKeys` to `Fields` whose `special_type` is `:fk`." @@ -328,6 +343,20 @@ (and (seq join-table-ids) (sel :one :fields [Field :id :table_id :name :description :base_type :special_type], :name (name col-kw), :table_id [in join-table-ids])) + ;; Otherwise if this is a nested Field recursively find the appropriate info + (let [name-components (s/split (name col-kw) #"\.")] + (when (> (count name-components) 1) + ;; Find the nested Field by recursing through each Field's :children + (loop [field-kw->field field-kw->field, [component & more] (map keyword name-components)] + (when-let [f (field-kw->field component)] + (if-not (seq more) + ;; If the are no more components to recurse through give the resulting Field a qualified name like "source.service" and return it + (assoc f :name (apply str (interpose "." name-components))) + ;; Otherwise recurse with a map of child-name-kw -> child and the rest of the name components + (recur (zipmap (map (comp keyword :name) (:children f)) + (:children f)) + more)))))) + ;; Otherwise it is an aggregation column like :sum, build a map of information to return (merge (assert ag-type) {:name (name col-kw) @@ -341,6 +370,7 @@ ;; count should always be IntegerField/number (= col-kw :count) {:base_type :IntegerField :special_type :number} + ;; Otherwise something went wrong ! :else (do (log/error (u/format-color 'red "Annotation failed: don't know what to do with Field '%s'.\nExpected these Fields:\n%s" col-kw @@ -348,7 +378,10 @@ {:base_type :UnknownField :special_type nil}))))) ;; Add FK info the the resulting Fields - add-fields-extra-info))) + add-fields-extra-info + + ;; Remove extra data from the resulting Fields + (map (u/rpartial dissoc :children :parent_id))))) (defn- post-annotate "Take a sequence of RESULTS of executing QUERY and return the \"annotated\" results we pass to postprocessing -- the map with `:cols`, `:columns`, and `:rows`. @@ -359,13 +392,15 @@ results (if-not uncastify-fn results (for [row results] (m/map-keys uncastify-fn row))) + _ (when-not *disable-qp-logging* + (log/debug (u/format-color 'magenta "Driver QP returned results with keys: %s." (vec (keys (first results)))))) join-table-ids (set (map :table-id join-tables)) - fields (sel :many :fields [Field :id :table_id :name :description :base_type :special_type], - :table_id source-table-id, :active true) + fields (field/unflatten-nested-fields (sel :many :fields [Field :id :table_id :name :description :base_type :special_type :parent_id], :table_id source-table-id, :active true)) ordered-col-kws (order-cols query results fields)] + {:rows (for [row results] - (mapv row ordered-col-kws)) ; might as well return each row and col info as vecs because we're not worried about making - :columns (mapv name ordered-col-kws) ; making them lazy, and results are easier to play with in the REPL / paste into unit tests + (mapv row ordered-col-kws)) ; might as well return each row and col info as vecs because we're not worried about making + :columns (mapv name ordered-col-kws) ; making them lazy, and results are easier to play with in the REPL / paste into unit tests :cols (vec (get-cols-info query fields ordered-col-kws join-table-ids))}))) ; as vecs. Make sure :rows stays lazy! @@ -400,10 +435,21 @@ ;; ;; Pre-processing then happens in order from bottom-to-top; i.e. POST-ANNOTATE gets to modify the results, then LIMIT, then CUMULATIVE-SUM, etc. -(defn- process-structured [driver query] - (let [driver-process-query (partial i/process-query driver)] +(defn- wrap-guard-multiple-calls + "Throw an exception if a QP function accidentally calls (QP QUERY) more than once." + [qp] + (let [called? (atom false)] + (fn [query] + (assert (not @called?) "(QP QUERY) IS BEING CALLED MORE THAN ONCE!") + (reset! called? true) + (qp query)))) + +(defn- process-structured [{:keys [driver], :as query}] + (let [driver-process-query (partial i/process-query driver) + driver-wrap-process-query (partial i/wrap-process-query-middleware driver)] ((<<- wrap-catch-exceptions pre-expand + driver-wrap-process-query post-add-row-count-and-status pre-add-implicit-fields pre-add-implicit-breakout-order-by @@ -412,21 +458,27 @@ limit post-annotate pre-log-query + wrap-guard-multiple-calls driver-process-query) query))) -(defn- process-native [driver query] - (let [driver-process-query (partial i/process-query driver)] +(defn- process-native [{:keys [driver], :as query}] + (let [driver-process-query (partial i/process-query driver) + driver-wrap-process-query (partial i/wrap-process-query-middleware driver)] ((<<- wrap-catch-exceptions + driver-wrap-process-query post-add-row-count-and-status post-convert-unix-timestamps-to-dates limit + wrap-guard-multiple-calls driver-process-query) query))) (defn process "Process a QUERY and return the results." [driver query] - (binding [*driver* driver] - ((case (keyword (:type query)) - :native process-native - :query process-structured) - driver query))) + (when-not *disable-qp-logging* + (log/debug (u/format-color 'blue "\nQUERY:\n%s" (u/pprint-to-str query)))) + ((case (keyword (:type query)) + :native process-native + :query process-structured) + (assoc query + :driver driver))) diff --git a/src/metabase/driver/query_processor/expand.clj b/src/metabase/driver/query_processor/expand.clj index 4d3ec5b8fdc3769d744f84fcd38997b8a179aac3..8e376ec89be051977339b845286aa69c17756262 100644 --- a/src/metabase/driver/query_processor/expand.clj +++ b/src/metabase/driver/query_processor/expand.clj @@ -40,6 +40,7 @@ [string :as s] [walk :as walk]) [medley.core :as m] + [korma.core :as k] [swiss.arrows :refer [-<>]] [metabase.db :refer [sel]] [metabase.driver.interface :as i] @@ -54,7 +55,8 @@ parse-breakout parse-fields parse-filter - parse-order-by) + parse-order-by + ph) ;; ## -------------------- Protocols -------------------- @@ -81,17 +83,31 @@ ;; ## -------------------- Expansion - Impl -------------------- -(def ^:private ^:dynamic *driver* nil) +(def ^:private ^:dynamic *field-ids* + "Bound to an atom containing a set of `Field` IDs referenced in the query being expanded." + nil) + +(def ^:private ^:dynamic *original-query-dict* + "The entire original Query dict being expanded." + nil) + +(def ^:private ^:dynamic *fk-field-ids* + "Bound to an atom containing a set of Foreign Key `Field` IDs (on the `source-table`) that we should use for joining to additional `Tables`." + nil) + +(def ^:private ^:dynamic *table-ids* + "Bound to an atom containing a set of `Table` IDs referenced by `Fields` in the query being expanded." + nil) (defn- assert-driver-supports [^Keyword feature] - {:pre [*driver*]} - (i/assert-driver-supports *driver* feature)) + {:pre [(:driver *original-query-dict*)]} + (i/assert-driver-supports (:driver *original-query-dict*) feature)) (defn- non-empty-clause? [clause] (and clause (or (not (sequential? clause)) (and (seq clause) - (every? identity clause))))) + (not (every? nil? clause)))))) (defn- parse [query-dict] (update-in query-dict [:query] #(-<> (assoc % @@ -104,18 +120,6 @@ :source_table :source-table}) (m/filter-vals non-empty-clause? <>)))) -(def ^:private ^:dynamic *field-ids* - "Bound to an atom containing a set of `Field` IDs referenced in the query being expanded." - nil) - -(def ^:private ^:dynamic *fk-field-ids* - "Bound to an atom containing a set of Foreign Key `Field` IDs (on the `source-table`) that we should use for joining to additional `Tables`." - nil) - -(def ^:private ^:dynamic *table-ids* - "Bound to an atom containing a set of `Table` IDs referenced by `Fields` in the query being expanded." - nil) - (defn rename-mb-field-keys "Rename the keys in a Metabase `Field` to match the format of those in Query Expander `Fields`." [field] @@ -123,18 +127,29 @@ :name :field-name :special_type :special-type :base_type :base-type - :table_id :table-id})) + :table_id :table-id + :parent_id :parent-id})) (defn- resolve-fields "Resolve the `Fields` in an EXPANDED-QUERY-DICT." - [expanded-query-dict field-ids] - (if-not (seq field-ids) expanded-query-dict ; No need to do a DB call or walk expanded-query-dict if we didn't see any Field IDs - (let [fields (->> (sel :many :id->fields [field/Field :name :base_type :special_type :table_id] :id [in field-ids]) - (m/map-vals rename-mb-field-keys))] - (reset! *table-ids* (set (map :table-id (vals fields)))) - ;; This is performed depth-first so we don't end up walking the newly-created Field/Value objects - ;; they may have nil values; this was we don't have to write an implementation of resolve-field for nil - (walk/postwalk #(resolve-field % fields) expanded-query-dict)))) + [expanded-query-dict field-ids & [count]] + (if-not (seq field-ids) + ;; Base case: if there's no field-ids to expand we're done + expanded-query-dict + + ;; Re-bind *field-ids* in case we need to do recursive Field resolution + (binding [*field-ids* (atom #{})] + (let [fields (->> (sel :many :id->fields [field/Field :name :base_type :special_type :table_id :parent_id], :id [in field-ids]) + (m/map-vals rename-mb-field-keys) + (m/map-vals #(assoc % :parent (when (:parent-id %) + (ph (:parent-id %))))))] + (swap! *table-ids* set/union (set (map :table-id (vals fields)))) + + ;; Recurse in case any new [nested] Field placeholders were emitted and we need to do recursive Field resolution + ;; We can't use recur here because binding wraps body in try/catch + (resolve-fields (walk/postwalk #(resolve-field % fields) expanded-query-dict) + @*field-ids* + (inc (or count 0))))))) (defn- resolve-database "Resolve the `Database` in question for an EXPANDED-QUERY-DICT." @@ -192,11 +207,11 @@ (defn expand "Expand a QUERY-DICT." - [driver query-dict] - (binding [*driver* driver - *field-ids* (atom #{}) - *fk-field-ids* (atom #{}) - *table-ids* (atom #{})] + [query-dict] + (binding [*original-query-dict* query-dict + *field-ids* (atom #{}) + *fk-field-ids* (atom #{}) + *table-ids* (atom #{})] (some-> query-dict parse (resolve-fields @*field-ids*) @@ -206,17 +221,40 @@ ;; ## -------------------- Field + Value -------------------- +(defprotocol IField + "Methods specific to the Query Expander `Field` record type." + (qualified-name-components [this] + "Return a vector of name components of the form `[table-name parent-names... field-name]`")) + ;; Field is the expansion of a Field ID in the standard QL (defrecord Field [^Integer field-id ^String field-name ^Keyword base-type ^Keyword special-type ^Integer table-id - ^String table-name] + ^String table-name + ^Integer parent-id + parent] ; Field once its resolved; FieldPlaceholder before that IResolve + (resolve-field [this field-id->fields] + (cond + parent (if (= (type parent) Field) + this + (resolve-field parent field-id->fields)) + parent-id (assoc this :parent (or (field-id->fields parent-id) + (ph parent-id))) + :else this)) + (resolve-table [this table-id->table] (assoc this :table-name (:name (or (table-id->table table-id) - (throw (Exception. (format "Query expansion failed: could not find table %d." table-id)))))))) + (throw (Exception. (format "Query expansion failed: could not find table %d." table-id))))))) + + IField + (qualified-name-components [this] + (conj (if parent + (qualified-name-components parent) + [table-name]) + field-name))) (defn- Field? "Is this a valid value for a `Field` ID in an unexpanded query? (i.e. an integer or `fk->` form)." @@ -243,9 +281,13 @@ (defrecord FieldPlaceholder [^Integer field-id] IResolve (resolve-field [this field-id->fields] - (-> (:field-id this) - field-id->fields - map->Field))) + (or + ;; try to resolve the Field with the ones available in field-id->fields + (some->> (field-id->fields field-id) + (merge this) + map->Field) + ;; If that fails just return ourselves as-is + this))) (defn- parse-value "Convert the `value` of a `Value` to a date or timestamp if needed. @@ -276,14 +318,17 @@ If `*field-ids*` is bound, " ([field-id] (match field-id - (field-id :guard integer?) (do (swap! *field-ids* conj field-id) - (->FieldPlaceholder field-id)) - ["fk->" - (fk-field-id :guard integer?) - (dest-field-id :guard integer?)] (do (assert-driver-supports :foreign-keys) - (swap! *field-ids* conj dest-field-id) - (swap! *fk-field-ids* conj fk-field-id) - (->FieldPlaceholder dest-field-id)))) + (id :guard integer?) + (do (swap! *field-ids* conj id) + (->FieldPlaceholder id)) + + ["fk->" (fk-field-id :guard integer?) (dest-field-id :guard integer?)] + (do (assert-driver-supports :foreign-keys) + (swap! *field-ids* conj dest-field-id) + (swap! *fk-field-ids* conj fk-field-id) + (->FieldPlaceholder dest-field-id)) + + _ (throw (Exception. (str "Invalid field: " field-id))))) ([field-id value] (->ValuePlaceholder (:field-id (ph field-id)) value))) @@ -296,7 +341,8 @@ `(defn ~(vary-meta fn-name assoc :private true) [form#] (when (non-empty-clause? form#) (match form# - ~@match-forms)))) + ~@match-forms + form# (throw (Exception. (format ~(format "%s failed: invalid clause: %%s" fn-name) form#))))))) ;; ## -------------------- Aggregation -------------------- @@ -358,7 +404,7 @@ ;; ### Parsers (defparser parse-filter-subclause - ["INSIDE" lat-field lon-field lat-max lon-min lat-min lon-max] + ["INSIDE" (lat-field :guard Field?) (lon-field :guard Field?) (lat-max :guard number?) (lon-min :guard number?) (lat-min :guard number?) (lon-max :guard number?)] (map->Filter:Inside {:filter-type :inside :lat {:field (ph lat-field) :min (ph lat-field lat-min) @@ -367,18 +413,18 @@ :min (ph lon-field lon-min) :max (ph lon-field lon-max)}}) - ["BETWEEN" field-id min max] + ["BETWEEN" (field-id :guard Field?) (min :guard identity) (max :guard identity)] (map->Filter:Between {:filter-type :between :field (ph field-id) :min-val (ph field-id min) :max-val (ph field-id max)}) - [filter-type field-id val] + [(filter-type :guard (partial contains? #{"=" "!=" "<" ">" "<=" ">="})) (field-id :guard Field?) (val :guard identity)] (map->Filter:Field+Value {:filter-type (keyword filter-type) :field (ph field-id) :value (ph field-id val)}) - [filter-type field-id] + [(filter-type :guard string?) (field-id :guard Field?)] (map->Filter:Field {:filter-type (case filter-type "NOT_NULL" :not-null "IS_NULL" :is-null) @@ -395,8 +441,15 @@ ;; ## -------------------- Order-By -------------------- -(defrecord OrderByAggregateField [^Keyword source ; e.g. :aggregation - ^Integer index]) ; e.g. 0 +(defrecord OrderByAggregateField [^Keyword source ; Name used in original query. Always :aggregation for right now + ^Integer index ; e.g. 0 + ^Aggregation aggregation] ; The aggregation clause being referred to + IField + (qualified-name-components [_] + ;; Return something like [nil "count"] + ;; nil is used where Table name would normally go + [nil (name (:aggregation-type aggregation))])) + (defrecord OrderBySubclause [^Field field ; or aggregate Field? ^Keyword direction]) ; either :ascending or :descending @@ -407,8 +460,10 @@ "descending" :descending)) (defparser parse-order-by-subclause - [["aggregation" index] direction] (->OrderBySubclause (->OrderByAggregateField :aggregation index) - (parse-order-by-direction direction)) + [["aggregation" index] direction] (let [{{:keys [aggregation]} :query} *original-query-dict*] + (assert aggregation "Query does not contain an aggregation clause.") + (->OrderBySubclause (->OrderByAggregateField :aggregation index (parse-aggregation aggregation)) + (parse-order-by-direction direction))) [(field-id :guard Field?) direction] (->OrderBySubclause (ph field-id) (parse-order-by-direction direction))) (defparser parse-order-by diff --git a/src/metabase/driver/sync.clj b/src/metabase/driver/sync.clj index 90fcc3abb97c9fb7f83b5a1edf582ad1d838c75e..311a4f0c4b45efe9996630d795b42a45bc618894 100644 --- a/src/metabase/driver/sync.clj +++ b/src/metabase/driver/sync.clj @@ -3,7 +3,6 @@ (:require [clojure.math.numeric-tower :as math] [clojure.string :as s] [clojure.tools.logging :as log] - [colorize.core :as color] [korma.core :as k] [medley.core :as m] [metabase.db :refer :all] @@ -24,6 +23,7 @@ sync-table-active-fields-and-pks! sync-table-fks! sync-table-fields-metadata! + sync-field-nested-fields! update-table-row-count!) ;; ## sync-database! and sync-table! @@ -31,10 +31,11 @@ (defn sync-database! "Sync DATABASE and all its Tables and Fields." [driver database] - (binding [qp/*disable-qp-logging* true] + (binding [qp/*disable-qp-logging* true + *sel-disable-logging* true] (sync-in-context driver database (fn [] - (log/info (u/format-color 'blue "Syncing %s database %s..." (name (:engine database)) (:name database))) + (log/info (u/format-color 'magenta "Syncing %s database '%s'..." (name (:engine database)) (:name database))) (let [active-table-names (active-table-names driver database) table-name->id (sel :many :field->id [Table :name] :db_id (:id database) :active true)] @@ -47,12 +48,12 @@ (doseq [[table-name table-id] table-name->id] (when-not (contains? active-table-names table-name) (upd Table table-id :active false) - (log/info (format "Marked table %s.%s as inactive." (:name database) table-name)) + (log/info (u/format-color 'cyan "Marked table %s.%s as inactive." (:name database) table-name)) - ;; We need to mark driver Table's Fields as inactive so we don't expose them in UI such as FK selector (etc.) This can happen in the background - (future (k/update Field - (k/where {:table_id table-id}) - (k/set-fields {:active false}))))) + ;; We need to mark driver Table's Fields as inactive so we don't expose them in UI such as FK selector (etc.) + (k/update Field + (k/where {:table_id table-id}) + (k/set-fields {:active false})))) ;; Next, we'll create new Tables (ones that came back in active-table-names but *not* in table-name->id) (log/debug "Creating new tables...") @@ -60,7 +61,7 @@ (doseq [active-table-name active-table-names] (when-not (contains? existing-table-names active-table-name) (ins Table :db_id (:id database), :active true, :name active-table-name) - (log/info (format "Found new table: %s.%s" (:name database) active-table-name)))))) + (log/info (u/format-color 'blue "Found new table: '%s'" active-table-name)))))) ;; Now sync the active tables (log/debug "Syncing active tables...") @@ -68,7 +69,7 @@ (map #(assoc % :db (delay database))) ; replace default delays with ones that reuse database (and don't require a DB call) (sync-database-active-tables! driver)) - (log/info (u/format-color 'blue "Finished syncing %s database %s." (name (:engine database)) (:name database))))))) + (log/info (u/format-color 'magenta "Finished syncing %s database %s." (name (:engine database)) (:name database))))))) (defn sync-table! "Sync a *single* TABLE by running all the sync steps for it. @@ -91,18 +92,18 @@ [driver active-tables] ;; update the row counts for every Table. These *can* happen asynchronously, but since they make a lot of DB calls each so ;; going to block while they run for the time being. (TODO - fix this) - (log/debug (color/green "Updating table row counts...")) + (log/debug "Updating table row counts...") (doseq [table active-tables] (u/try-apply update-table-row-count! table)) ;; Next, create new Fields / mark inactive Fields / mark PKs for each table ;; (TODO - this was originally done in parallel but it was only marginally faster, and harder to debug. Should we switch back at some point?) - (log/debug (color/green "Syncing active Fields + PKs...")) + (log/debug "Syncing active fields + PKs...") (doseq [table active-tables] (u/try-apply sync-table-active-fields-and-pks! driver table)) ;; Once that's finished, we can sync FKs - (log/debug (color/green "Syncing FKs...")) + (log/debug "Syncing FKs...") (doseq [table active-tables] (u/try-apply sync-table-fks! driver table)) @@ -111,10 +112,10 @@ (let [tables-count (count active-tables) finished-tables-count (atom 0)] (doseq [table active-tables] - (log/debug (color/green (format "Syncing metadata for %s.%s..." (:name @(:db table)) (:name table)))) + (log/debug (format "Syncing metadata for table '%s'..." (:name table))) (sync-table-fields-metadata! driver table) (swap! finished-tables-count inc) - (log/info (color/blue (format "Synced %s.%s (%d/%d)" (:name @(:db table)) (:name table) @finished-tables-count tables-count)))))) + (log/info (u/format-color 'magenta "Synced table '%s'. (%d/%d)" (:name table) @finished-tables-count tables-count))))) ;; ## sync-table steps. @@ -130,7 +131,7 @@ (when-not (= (:rows table) table-row-count) (upd Table (:id table) :rows table-row-count))) (catch Throwable e - (log/error (color/red (format "Unable to update row_count for %s: %s" (:name table) (.getMessage e))))))) + (log/error (u/format-color 'red "Unable to update row_count for '%s': %s" (:name table) (.getMessage e)))))) ;; ### 2) sync-table-active-fields-and-pks! @@ -140,8 +141,8 @@ [table pk-fields] {:pre [(set? pk-fields) (every? string? pk-fields)]} - (doseq [{field-name :name field-id :id} (sel :many :fields [Field :name :id] :table_id (:id table) :special_type nil :name [in pk-fields])] - (log/info (format "Field '%s.%s' is a primary key. Marking it as such." (:name table) field-name)) + (doseq [{field-name :name field-id :id} (sel :many :fields [Field :name :id], :table_id (:id table), :special_type nil, :name [in pk-fields], :parent_id nil)] + (log/info (u/format-color 'green "Field '%s.%s' is a primary key. Marking it as such." (:name table) field-name)) (upd Field field-id :special_type :id))) (defn sync-table-active-fields-and-pks! @@ -150,27 +151,36 @@ (let [database @(:db table)] ;; Now do the syncing for Table's Fields (log/debug (format "Determining active Fields for Table '%s'..." (:name table))) - (let [active-column-names->type (active-column-names->type driver table) - field-name->id (sel :many :field->id [Field :name] :table_id (:id table) :active true)] + (let [active-column-names->type (active-column-names->type driver table) + existing-field-name->field (sel :many :field->fields [Field :name :base_type :id], :table_id (:id table), :active true, :parent_id nil)] + (assert (map? active-column-names->type) "active-column-names->type should return a map.") (assert (every? string? (keys active-column-names->type)) "The keys of active-column-names->type should be strings.") (assert (every? (partial contains? field/base-types) (vals active-column-names->type)) "The vals of active-column-names->type should be valid Field base types.") ;; As above, first mark inactive Fields (let [active-column-names (set (keys active-column-names->type))] - (doseq [[field-name field-id] field-name->id] + (doseq [[field-name {field-id :id}] existing-field-name->field] (when-not (contains? active-column-names field-name) (upd Field field-id :active false) - (log/info (format "Marked field %s.%s.%s as inactive." (:name database) (:name table) field-name))))) + (log/info (u/format-color 'cyan "Marked field '%s.%s' as inactive." (:name table) field-name))))) - ;; Next, create new Fields as needed - (let [existing-field-names (set (keys field-name->id))] + ;; Create new Fields, update existing types if needed + (let [existing-field-names (set (keys existing-field-name->field))] (doseq [[active-field-name active-field-type] active-column-names->type] - (when-not (contains? existing-field-names active-field-name) - (ins Field - :table_id (:id table) - :name active-field-name - :base_type active-field-type)))) + ;; If Field doesn't exist create it + (if-not (contains? existing-field-names active-field-name) + (do (log/info (u/format-color 'blue "Found new field: '%s.%s'" (:name table) active-field-name)) + (ins Field + :table_id (:id table) + :name active-field-name + :base_type active-field-type)) + ;; Otherwise update the Field type if needed + (let [{existing-base-type :base_type, existing-field-id :id} (existing-field-name->field active-field-name)] + (when-not (= active-field-type existing-base-type) + (log/info (u/format-color 'blue "Field '%s.%s' has changed from a %s to a %s." (:name table) active-field-name existing-base-type active-field-type)) + (upd Field existing-field-id :base_type active-field-type)))))) + ;; TODO - we need to add functionality to update nested Field base types as well! ;; Now mark PK fields as such if needed (let [pk-fields (table-pks driver table)] @@ -201,17 +211,17 @@ (every? :dest-column-name fks)) "table-fks should return a set of maps with keys :fk-column-name, :dest-table-name, and :dest-column-name.") (when (seq fks) - (let [fk-name->id (sel :many :field->id [Field :name] :table_id (:id table), :special_type nil, :name [in (map :fk-column-name fks)]) - table-name->id (sel :many :field->id [Table :name] :name [in (map :dest-table-name fks)])] + (let [fk-name->id (sel :many :field->id [Field :name], :table_id (:id table), :special_type nil, :name [in (map :fk-column-name fks)], :parent_id nil) + table-name->id (sel :many :field->id [Table :name], :name [in (map :dest-table-name fks)])] (doseq [{:keys [fk-column-name dest-column-name dest-table-name] :as fk} fks] (when-let [fk-column-id (fk-name->id fk-column-name)] (when-let [dest-table-id (table-name->id dest-table-name)] - (when-let [dest-column-id (sel :one :id Field :table_id dest-table-id :name dest-column-name)] - (log/info (format "Marking foreign key '%s.%s' -> '%s.%s'." (:name table) fk-column-name dest-table-name dest-column-name)) + (when-let [dest-column-id (sel :one :id Field, :table_id dest-table-id, :name dest-column-name, :parent_id nil)] + (log/info (u/format-color 'green "Marking foreign key '%s.%s' -> '%s.%s'." (:name table) fk-column-name dest-table-name dest-column-name)) (ins ForeignKey - :origin_id fk-column-id + :origin_id fk-column-id :destination_id dest-column-id - :relationship (determine-fk-type {:id fk-column-id, :table (delay table)})) ; fake a Field instance + :relationship (determine-fk-type {:id fk-column-id, :table (delay table)})) ; fake a Field instance (upd Field fk-column-id :special_type :fk)))))))))) @@ -220,10 +230,11 @@ (defn sync-table-fields-metadata! "Call `sync-field!` for every active Field for TABLE." [driver table] - (let [active-fields (->> (sel :many Field, :table_id (:id table), :active true) - (map #(assoc % :table (delay table))))] ; as above, replace the delay that comes back with one that reuses existing table obj + {:pre [(map? table)]} + (let [active-fields (sel :many Field, :table_id (:id table), :active true, :parent_id nil)] (doseq [field active-fields] - (u/try-apply sync-field! driver field)))) + ;; replace the normal delay for the Field with one that just returns the existing Table so we don't need to re-fetch + (u/try-apply sync-field! driver (assoc field :table (delay table)))))) ;; ## sync-field @@ -244,12 +255,13 @@ [driver field] {:pre [driver field]} - (log/debug (format "Syncing field '%s.%s'..." (:name @(:table field)) (:name field))) + (log/debug (format "Syncing field '%s'..." @(:qualified-name field))) (sync-field->> field (mark-url-field! driver) mark-category-field! (mark-no-preview-display-field! driver) - auto-assign-field-special-type-by-name!)) + auto-assign-field-special-type-by-name! + (sync-field-nested-fields! driver))) ;; Each field-syncing function below should return FIELD with any updates that we made, or nil. @@ -264,8 +276,9 @@ (defn percent-valid-urls "Recursively count the values of non-nil values in VS that are valid URLs, and return it as a percentage." [vs] - (loop [valid-count 0 non-nil-count 0 [v & more :as vs] vs] - (cond (not (seq vs)) (float (/ valid-count non-nil-count)) + (loop [valid-count 0, non-nil-count 0, [v & more :as vs] vs] + (cond (not (seq vs)) (if (zero? non-nil-count) 0.0 + (float (/ valid-count non-nil-count))) (nil? v) (recur valid-count non-nil-count more) :else (let [valid? (and (string? v) (u/is-url? v))] @@ -293,7 +306,7 @@ (assert (>= percent-urls 0.0)) (assert (<= percent-urls 100.0)) (when (> percent-urls percent-valid-url-threshold) - (log/info (format "Field '%s.%s' is %d%% URLs. Marking it as a URL." (:name @(:table field)) (:name field) (int (math/round (* 100 percent-urls))))) + (log/info (u/format-color 'green "Field '%s' is %d%% URLs. Marking it as a URL." @(:qualified-name field) (int (math/round (* 100 percent-urls))))) (upd Field (:id field) :special_type :url) (assoc field :special_type :url))))) @@ -311,7 +324,7 @@ (let [cardinality (queries/field-distinct-count field low-cardinality-threshold)] (when (and (> cardinality 0) (< cardinality low-cardinality-threshold)) - (log/info (format "Field '%s.%s' has %d unique values. Marking it as a category." (:name @(:table field)) (:name field) cardinality)) + (log/info (u/format-color 'green "Field '%s' has %d unique values. Marking it as a category." @(:qualified-name field) cardinality)) (upd Field (:id field) :special_type :category) (assoc field :special_type :category))))) @@ -346,81 +359,109 @@ (let [avg-len (field-avg-length driver field)] (assert (integer? avg-len) "field-avg-length should return an integer.") (when (> avg-len average-length-no-preview-threshold) - (log/info (format "Field '%s.%s' has an average length of %d. Not displaying it in previews." (:name @(:table field)) (:name field) avg-len)) + (log/info (u/format-color 'green "Field '%s' has an average length of %d. Not displaying it in previews." @(:qualified-name field) avg-len)) (upd Field (:id field) :preview_display false) (assoc field :preview_display false))))) ;; ### auto-assign-field-special-type-by-name! -(def ^{:arglists '([field])} +(def ^:private ^{:arglists '([field])} field->name-inferred-special-type "If FIELD has a `name` and `base_type` that matches a known pattern, return the `special_type` we should assign to it." (let [bool-or-int #{:BooleanField :BigIntegerField :IntegerField} float #{:DecimalField :FloatField} int-or-text #{:BigIntegerField :IntegerField :CharField :TextField} text #{:CharField :TextField} - ;; tuples of [pattern set-of-valid-base-types special-type] + ;; tuples of [pattern set-of-valid-base-types special-type [& top-level-only?] ;; * Convert field name to lowercase before matching against a pattern ;; * consider a nil set-of-valid-base-types to mean "match any base type" - pattern+base-types+special-type [[#"^.*_lat$" float :latitude] - [#"^.*_lon$" float :longitude] - [#"^.*_lng$" float :longitude] - [#"^.*_long$" float :longitude] - [#"^.*_longitude$" float :longitude] - [#"^.*_rating$" int-or-text :category] - [#"^.*_type$" int-or-text :category] - [#"^.*_url$" text :url] - [#"^_latitude$" float :latitude] - [#"^active$" bool-or-int :category] - [#"^city$" text :city] - [#"^country$" text :country] - [#"^countrycode$" text :country] - [#"^currency$" int-or-text :category] - [#"^first_name$" text :name] - [#"^full_name$" text :name] - [#"^gender$" int-or-text :category] - [#"^id$" nil :id] - [#"^last_name$" text :name] - [#"^lat$" float :latitude] - [#"^latitude$" float :latitude] - [#"^lon$" float :longitude] - [#"^lng$" float :longitude] - [#"^long$" float :longitude] - [#"^longitude$" float :longitude] - [#"^name$" text :name] - [#"^postalCode$" int-or-text :zip_code] - [#"^postal_code$" int-or-text :zip_code] - [#"^rating$" int-or-text :category] - [#"^role$" int-or-text :category] - [#"^sex$" int-or-text :category] - [#"^state$" text :state] - [#"^status$" int-or-text :category] - [#"^type$" int-or-text :category] - [#"^url$" text :url] - [#"^zip_code$" int-or-text :zip_code] - [#"^zipcode$" int-or-text :zip_code]]] + pattern+base-types+special-type+top-level-only? [[#"^.*_lat$" float :latitude] + [#"^.*_lon$" float :longitude] + [#"^.*_lng$" float :longitude] + [#"^.*_long$" float :longitude] + [#"^.*_longitude$" float :longitude] + [#"^.*_rating$" int-or-text :category] + [#"^.*_type$" int-or-text :category] + [#"^.*_url$" text :url] + [#"^_latitude$" float :latitude] + [#"^active$" bool-or-int :category] + [#"^city$" text :city] + [#"^country$" text :country] + [#"^countrycode$" text :country] + [#"^currency$" int-or-text :category] + [#"^first_name$" text :name] + [#"^full_name$" text :name] + [#"^gender$" int-or-text :category] + [#"^id$" nil :id :top-level-only] + [#"^last_name$" text :name] + [#"^lat$" float :latitude] + [#"^latitude$" float :latitude] + [#"^lon$" float :longitude] + [#"^lng$" float :longitude] + [#"^long$" float :longitude] + [#"^longitude$" float :longitude] + [#"^name$" text :name] + [#"^postalCode$" int-or-text :zip_code] + [#"^postal_code$" int-or-text :zip_code] + [#"^rating$" int-or-text :category] + [#"^role$" int-or-text :category] + [#"^sex$" int-or-text :category] + [#"^state$" text :state] + [#"^status$" int-or-text :category] + [#"^type$" int-or-text :category] + [#"^url$" text :url] + [#"^zip_code$" int-or-text :zip_code] + [#"^zipcode$" int-or-text :zip_code]]] ;; Check that all the pattern tuples are valid - (doseq [[name-pattern base-types special-type] pattern+base-types+special-type] + (doseq [[name-pattern base-types special-type] pattern+base-types+special-type+top-level-only?] (assert (= (type name-pattern) java.util.regex.Pattern)) (assert (every? (partial contains? field/base-types) base-types)) (assert (contains? field/special-types special-type))) - (fn [{base-type :base_type, field-name :name}] + (fn [{base-type :base_type, field-name :name, :as field}] {:pre [(string? field-name) (keyword? base-type)]} - (m/find-first (fn [[name-pattern valid-base-types _]] - (and (or (nil? valid-base-types) - (contains? valid-base-types base-type)) - (re-matches name-pattern (s/lower-case field-name)))) - pattern+base-types+special-type)))) - -(defn auto-assign-field-special-type-by-name! + (or (m/find-first (fn [[name-pattern valid-base-types _ top-level-only?]] + (and (or (nil? valid-base-types) + (contains? valid-base-types base-type)) + (re-matches name-pattern (s/lower-case field-name)) + (or (not top-level-only?) + (nil? (:parent_id field))))) + pattern+base-types+special-type+top-level-only?))))) + +(defn- auto-assign-field-special-type-by-name! "If FIELD doesn't have a special type, but has a name that matches a known pattern like `latitude`, mark it as having the specified special type." [field] (when-not (:special_type field) (when-let [[pattern _ special-type] (field->name-inferred-special-type field)] - (log/info (format "%s '%s.%s' matches '%s'. Setting special_type to '%s'." - (name (:base_type field)) (:name @(:table field)) (:name field) pattern (name special-type))) + (log/info (u/format-color 'green "%s '%s' matches '%s'. Setting special_type to '%s'." + (name (:base_type field)) @(:qualified-name field) pattern (name special-type))) (upd Field (:id field) :special_type special-type) (assoc field :special_type special-type)))) + + +(defn- sync-field-nested-fields! [driver field] + (when (and (= (:base_type field) :DictionaryField) + (supports? driver :nested-fields) ; if one of these is true + (satisfies? ISyncDriverFieldNestedFields driver)) ; the other should be :wink: + (log/debug (format "Syncing nested fields for '%s'..." @(:qualified-name field))) + + (let [nested-field-name->type (active-nested-field-name->type driver field)] + ;; fetch existing nested fields + (let [existing-nested-field-name->id (sel :many :field->id [Field :name], :table_id (:table_id field), :active true, :parent_id (:id field))] + + ;; mark existing nested fields as inactive if they didn't come back from active-nested-field-name->type + (doseq [[nested-field-name nested-field-id] existing-nested-field-name->id] + (when-not (contains? (set (map keyword (keys nested-field-name->type))) (keyword nested-field-name)) + (log/info (u/format-color 'cyan "Marked nested field '%s.%s' as inactive." @(:qualified-name field) nested-field-name)) + (upd Field nested-field-id :active false))) + + ;; OK, now create new Field objects for ones that came back from active-nested-field-name->type but *aren't* in existing-nested-field-name->id + (doseq [[nested-field-name nested-field-type] nested-field-name->type] + (when-not (contains? (set (map keyword (keys existing-nested-field-name->id))) (keyword nested-field-name)) + (log/info (u/format-color 'blue "Found new nested field: '%s.%s'" @(:qualified-name field) (name nested-field-name))) + (let [nested-field (ins Field, :table_id (:table_id field), :parent_id (:id field), :name (name nested-field-name) :base_type (name nested-field-type), :active true)] + ;; Now recursively sync this nested Field + ;; Replace parent so deref doesn't need to do a DB call + (sync-field! driver (assoc nested-field :parent (delay field)))))))))) diff --git a/src/metabase/models/field.clj b/src/metabase/models/field.clj index 71272f40b162adcf6a66a22171eaec7a65f50174..3155fbde6121047c25bf89de8e96c6f1f907d24b 100644 --- a/src/metabase/models/field.clj +++ b/src/metabase/models/field.clj @@ -10,7 +10,8 @@ [interface :refer :all]) [metabase.util :as u])) -(declare field->fk-field) +(declare field->fk-field + qualified-name-components) (def ^:const special-types "Possible values for `Field` `:special_type`." @@ -61,6 +62,7 @@ :DateField :DateTimeField :DecimalField + :DictionaryField :FloatField :IntegerField :TextField @@ -107,16 +109,22 @@ (contains? field :special_type)) (future (create-field-values-if-needed (sel :one [this :id :table_id :base_type :special_type :field_type] :id id))))) - (post-select [_ {:keys [table_id] :as field}] + (post-select [this {:keys [table_id parent_id] :as field}] (map->FieldInstance (u/assoc* field - :table (delay (sel :one 'metabase.models.table/Table :id table_id)) - :db (delay @(:db @(:table <>))) - :target (delay (field->fk-field field)) - :human_readable_name (when (name :field) - (delay (common/name->human-readable-name (:name field))))))) - - (pre-cascade-delete [_ {:keys [id]}] + :table (delay (sel :one 'metabase.models.table/Table :id table_id)) + :db (delay @(:db @(:table <>))) + :target (delay (field->fk-field field)) + :human_readable_name (when (name :field) + (delay (common/name->human-readable-name (:name field)))) + :parent (when parent_id + (delay (this parent_id))) + :children (delay (sel :many this :parent_id (:id field))) + :qualified-name-components (delay (qualified-name-components <>)) + :qualified-name (delay (apply str (interpose "." @(:qualified-name-components <>))))))) + + (pre-cascade-delete [this {:keys [id]}] + (cascade-delete this :parent_id id) (cascade-delete ForeignKey (where (or (= :origin_id id) (= :destination_id id)))) (cascade-delete 'metabase.models.field-values/FieldValues :field_id id))) @@ -132,3 +140,29 @@ (when (= :fk special_type) (let [dest-id (sel :one :field [ForeignKey :destination_id] :origin_id id)] (Field dest-id)))) + +(defn unflatten-nested-fields + "Take a sequence of both top-level and nested FIELDS, and return a sequence of top-level `Fields` + with nested `Fields` moved into sequences keyed by `:children` in their parents. + + (unflatten-nested-fields [{:id 1, :parent_id nil}, {:id 2, :parent_id 1}]) + -> [{:id 1, :parent_id nil, :children [{:id 2, :parent_id 1, :children nil}]}] + + You may optionally specify a different PARENT-ID-KEY; the default is `:parent_id`." + ([fields] + (unflatten-nested-fields fields :parent_id)) + ([fields parent-id-key] + (let [parent-id->fields (group-by parent-id-key fields) + resolve-children (fn resolve-children [field] + (assoc field :children (map resolve-children + (parent-id->fields (:id field)))))] + (map resolve-children (parent-id->fields nil))))) + +(defn- qualified-name-components + "Return the pieces that represent a path to FIELD, of the form `[table-name parent-fields-name* field-name]`." + [{:keys [table parent], :as field}] + {:pre [(delay? table)]} + (conj (if parent + (qualified-name-components @parent) + [(:name @table)]) + (:name field))) diff --git a/src/metabase/models/field_values.clj b/src/metabase/models/field_values.clj index e6c25f51ea83668e05a8ef0e07a98901d7dcdbe2..4792b0151efcf59e38212d10a5d6152a5560922b 100644 --- a/src/metabase/models/field_values.clj +++ b/src/metabase/models/field_values.clj @@ -41,15 +41,14 @@ (defn create-field-values "Create `FieldValues` for a `Field`." - {:arglists '([field] - [field human-readable-values])} - [{field-id :id :as field} & [human-readable-values]] + {:arglists '([field] [field human-readable-values])} + [{field-id :id, field-name :name, :as field} & [human-readable-values]] {:pre [(integer? field-id) - (:table field)]} ; need to pass a full `Field` object with delays beause the `metadata/` functions need those - (log/debug (format "Creating FieldValues for Field %d..." field-id)) + (:table field)]} ; need to pass a full `Field` object with delays beause the `metadata/` functions need those + (log/debug (format "Creating FieldValues for Field %s..." (or field-name field-id))) ; use field name if available (ins FieldValues - :field_id field-id - :values (field-distinct-values field) + :field_id field-id + :values (field-distinct-values field) :human_readable_values human-readable-values)) (defn create-field-values-if-needed diff --git a/src/metabase/models/interface.clj b/src/metabase/models/interface.clj index ac46ee92c838387dc30b58202125acc2ce34db6e..89346f5f1847b11e66c08dff58ca7c75556e0c62 100644 --- a/src/metabase/models/interface.clj +++ b/src/metabase/models/interface.clj @@ -153,8 +153,9 @@ [entity id] (when id (when (metabase.config/config-bool :mb-db-logging) - (clojure.tools.logging/debug - "DB CALL: " (:name entity) id)) + (when-not @(resolve 'metabase.db/*sel-disable-logging*) + (clojure.tools.logging/debug + "DB CALL: " (:name entity) id))) (let [[obj] (k/select (assoc entity :fields (::default-fields entity)) (k/where {:id id}) (k/limit 1))] diff --git a/src/metabase/util.clj b/src/metabase/util.clj index 15210e9d713f55eeb6619dffadaf1760608134fb..4b5d93bb0fa89fedeed3faad54d3a807c0dbb627 100644 --- a/src/metabase/util.clj +++ b/src/metabase/util.clj @@ -268,4 +268,11 @@ ([color-symb x] ((ns-resolve 'colorize.core color-symb) (pprint-to-str x)))) +(defmacro cond-let + "Like `if-let` or `when-let`, but for `cond`." + [binding-form then-form & more] + `(if-let ~binding-form ~then-form + ~(when (seq more) + `(cond-let ~@more)))) + (require-dox-in-this-namespace) diff --git a/test/metabase/api/meta/field_test.clj b/test/metabase/api/meta/field_test.clj index 15b8c860989489b6624851c6b1f309c26bf2b732..003c1fb8f548827e9d7f9401ba32b6a64cb7a33f 100644 --- a/test/metabase/api/meta/field_test.clj +++ b/test/metabase/api/meta/field_test.clj @@ -42,8 +42,10 @@ :field_type "info" :position 0 :preview_display true - :created_at $ - :base_type "TextField"}) + :created_at $ + :base_type "TextField" + :parent_id nil + :parent nil}) ((user->client :rasta) :get 200 (format "meta/field/%d" (id :users :name)))) @@ -62,18 +64,20 @@ (upd Field (id :venues :latitude) :special_type "latitude") ;; match against the modified Field field) - {:description nil - :table_id (id :venues) - :special_type "fk" - :name "LATITUDE" - :updated_at $ - :active true - :id $ - :field_type "info" - :position 0 + {:description nil + :table_id (id :venues) + :special_type "fk" + :name "LATITUDE" + :updated_at $ + :active true + :id $ + :field_type "info" + :position 0 :preview_display true - :created_at $ - :base_type "FloatField"}) + :created_at $ + :base_type "FloatField" + :parent_id nil + :parent nil}) ((user->client :crowberto) :put 200 (format "meta/field/%d" (id :venues :latitude)) {:special_type :fk})) (defn- field->field-values @@ -85,18 +89,18 @@ ;; Should return something useful for a field that has special_type :category (expect-eval-actual-first (match-$ (field->field-values :venues :price) - {:field_id (id :venues :price) + {:field_id (id :venues :price) :human_readable_values {} - :values [1 2 3 4] - :updated_at $ - :created_at $ - :id $}) - (do (upd FieldValues (:id (field->field-values :venues :price)) :human_readable_values nil) ; clear out existing human_readable_values in case they're set + :values [1 2 3 4] + :updated_at $ + :created_at $ + :id $}) + (do (upd FieldValues (:id (field->field-values :venues :price)) :human_readable_values nil) ; clear out existing human_readable_values in case they're set ((user->client :rasta) :get 200 (format "meta/field/%d/values" (id :venues :price))))) ;; Should return nothing for a field whose special_type is *not* :category (expect - {:values {} + {:values {} :human_readable_values {}} ((user->client :rasta) :get 200 (format "meta/field/%d/values" (id :venues :id)))) @@ -107,32 +111,32 @@ (expect-eval-actual-first [{:status "success"} (match-$ (sel :one FieldValues :field_id (id :venues :price)) - {:field_id (id :venues :price) + {:field_id (id :venues :price) :human_readable_values {:1 "$" :2 "$$" :3 "$$$" :4 "$$$$"} - :values [1 2 3 4] - :updated_at $ - :created_at $ - :id $})] + :values [1 2 3 4] + :updated_at $ + :created_at $ + :id $})] [((user->client :crowberto) :post 200 (format "meta/field/%d/value_map_update" (id :venues :price)) {:values_map {:1 "$" - :2 "$$" - :3 "$$$" - :4 "$$$$"}}) + :2 "$$" + :3 "$$$" + :4 "$$$$"}}) ((user->client :rasta) :get 200 (format "meta/field/%d/values" (id :venues :price)))]) ;; Check that we can unset values (expect-eval-actual-first [{:status "success"} (match-$ (sel :one FieldValues :field_id (id :venues :price)) - {:field_id (id :venues :price) + {:field_id (id :venues :price) :human_readable_values {} - :values [1 2 3 4] - :updated_at $ - :created_at $ - :id $})] - [(do (upd FieldValues (:id (field->field-values :venues :price)) :human_readable_values {:1 "$" ; make sure they're set + :values [1 2 3 4] + :updated_at $ + :created_at $ + :id $})] + [(do (upd FieldValues (:id (field->field-values :venues :price)) :human_readable_values {:1 "$" ; make sure they're set :2 "$$" :3 "$$$" :4 "$$$$"}) diff --git a/test/metabase/api/meta/table_test.clj b/test/metabase/api/meta/table_test.clj index f53ad2dba3c7aff2b3f375b3649a3ebb65010b82..2065edbe4c541d5a5863188b8f3477365538f578 100644 --- a/test/metabase/api/meta/table_test.clj +++ b/test/metabase/api/meta/table_test.clj @@ -60,23 +60,23 @@ (match-$ (Table (id :venues)) {:description nil :entity_type nil - :db (match-$ (db) - {:created_at $ - :engine "h2" - :id $ - :updated_at $ - :name "Test Database" - :organization_id nil - :description nil}) - :name "VENUES" - :rows 100 - :updated_at $ + :db (match-$ (db) + {:created_at $ + :engine "h2" + :id $ + :updated_at $ + :name "Test Database" + :organization_id nil + :description nil}) + :name "VENUES" + :rows 100 + :updated_at $ :entity_name nil - :active true - :pk_field (deref $pk_field) - :id (id :venues) - :db_id (db-id) - :created_at $}) + :active true + :pk_field (deref $pk_field) + :id (id :venues) + :db_id (db-id) + :created_at $}) ((user->client :rasta) :get 200 (format "meta/table/%d" (id :venues)))) ;; ## GET /api/meta/table/:id/fields @@ -93,7 +93,9 @@ :position 0 :preview_display true :created_at $ - :base_type "BigIntegerField"}) + :base_type "BigIntegerField" + :parent_id nil + :parent nil}) (match-$ (Field (id :categories :name)) {:description nil :table_id (id :categories) @@ -107,59 +109,65 @@ :position 0 :preview_display true :created_at $ - :base_type "TextField"})] + :base_type "TextField" + :parent_id nil + :parent nil})] ((user->client :rasta) :get 200 (format "meta/table/%d/fields" (id :categories)))) ;; ## GET /api/meta/table/:id/query_metadata (expect (match-$ (Table (id :categories)) - {:description nil - :entity_type nil - :db (match-$ (db) - {:created_at $ - :engine "h2" - :id $ - :updated_at $ - :name "Test Database" - :organization_id nil - :description nil}) - :name "CATEGORIES" - :fields [(match-$ (Field (id :categories :id)) - {:description nil - :table_id (id :categories) - :special_type "id" - :name "ID" - :updated_at $ - :active true - :id $ - :field_type "info" - :position 0 - :target nil - :preview_display true - :created_at $ - :base_type "BigIntegerField"}) - (match-$ (Field (id :categories :name)) - {:description nil - :table_id (id :categories) - :special_type "name" - :name "NAME" - :updated_at $ - :active true - :id $ - :field_type "info" - :position 0 - :target nil - :preview_display true - :created_at $ - :base_type "TextField"})] + {:description nil + :entity_type nil + :db (match-$ (db) + {:created_at $ + :engine "h2" + :id $ + :updated_at $ + :name "Test Database" + :organization_id nil + :description nil}) + :name "CATEGORIES" + :fields [(match-$ (Field (id :categories :id)) + {:description nil + :table_id (id :categories) + :special_type "id" + :name "ID" + :updated_at $ + :active true + :id $ + :field_type "info" + :position 0 + :target nil + :preview_display true + :created_at $ + :base_type "BigIntegerField" + :parent_id nil + :parent nil}) + (match-$ (Field (id :categories :name)) + {:description nil + :table_id (id :categories) + :special_type "name" + :name "NAME" + :updated_at $ + :active true + :id $ + :field_type "info" + :position 0 + :target nil + :preview_display true + :created_at $ + :base_type "TextField" + :parent_id nil + :parent nil})] :field_values {} - :rows 75 - :updated_at $ - :entity_name nil - :active true - :id (id :categories) - :db_id (db-id) - :created_at $}) + :rows 75 + :updated_at $ + :entity_name nil + :active true + :id (id :categories) + :db_id (db-id) + :created_at $}) ((user->client :rasta) :get 200 (format "meta/table/%d/query_metadata" (id :categories)))) @@ -184,80 +192,88 @@ ;;; GET api/meta/table/:id/query_metadata?include_sensitive_fields ;;; Make sure that getting the User table *does* include info about the password field, but not actual values themselves (expect - (match-$ (Table (id :users)) - {:description nil - :entity_type nil - :db (match-$ (db) - {:created_at $ - :engine "h2" - :id $ - :updated_at $ - :name "Test Database" - :organization_id nil - :description nil}) - :name "USERS" - :fields [(match-$ (Field (id :users :id)) - {:description nil - :table_id (id :users) - :special_type "id" - :name "ID" - :updated_at $ - :active true - :id $ - :field_type "info" - :position 0 - :target nil - :preview_display true - :created_at $ - :base_type "BigIntegerField"}) - (match-$ (Field (id :users :last_login)) - {:description nil - :table_id (id :users) - :special_type "category" - :name "LAST_LOGIN" - :updated_at $ - :active true - :id $ - :field_type "info" - :position 0 - :target nil - :preview_display true - :created_at $ - :base_type "DateTimeField"}) - (match-$ (Field (id :users :name)) - {:description nil - :table_id (id :users) - :special_type "category" - :name "NAME" - :updated_at $ - :active true - :id $ - :field_type "info" - :position 0 - :target nil - :preview_display true - :created_at $ - :base_type "TextField"}) - (match-$ (sel :one Field :table_id (id :users) :name "PASSWORD") - {:description nil - :table_id (id :users) - :special_type "category" - :name "PASSWORD" - :updated_at $ - :active true - :id $ - :field_type "sensitive" - :position 0 - :target nil - :preview_display true - :created_at $ - :base_type "TextField"})] - :rows 15 - :updated_at $ - :entity_name nil - :active true - :id (id :users) - :db_id (db-id) + (match-$ (sel :one Table :id (id :users)) + {:description nil + :entity_type nil + :db (match-$ (db) + {:created_at $ + :engine "h2" + :id $ + :updated_at $ + :name "Test Database" + :organization_id nil + :description nil}) + :name "USERS" + :fields [(match-$ (sel :one Field :id (id :users :id)) + {:description nil + :table_id (id :users) + :special_type "id" + :name "ID" + :updated_at $ + :active true + :id $ + :field_type "info" + :position 0 + :target nil + :preview_display true + :created_at $ + :base_type "BigIntegerField" + :parent_id nil + :parent nil}) + (match-$ (sel :one Field :id (id :users :last_login)) + {:description nil + :table_id (id :users) + :special_type "category" + :name "LAST_LOGIN" + :updated_at $ + :active true + :id $ + :field_type "info" + :position 0 + :target nil + :preview_display true + :created_at $ + :base_type "DateTimeField" + :parent_id nil + :parent nil}) + (match-$ (sel :one Field :id (id :users :name)) + {:description nil + :table_id (id :users) + :special_type "category" + :name "NAME" + :updated_at $ + :active true + :id $ + :field_type "info" + :position 0 + :target nil + :preview_display true + :created_at $ + :base_type "TextField" + :parent_id nil + :parent nil}) + (match-$ (sel :one Field :table_id (id :users) :name "PASSWORD") + {:description nil + :table_id (id :users) + :special_type "category" + :name "PASSWORD" + :updated_at $ + :active true + :id $ + :field_type "sensitive" + :position 0 + :target nil + :preview_display true + :created_at $ + :base_type "TextField" + :parent_id nil + :parent nil})] + :rows 15 + :updated_at $ + :entity_name nil + :active true + :id (id :users) + :db_id (db-id) :field_values {(keyword (str (id :users :last_login))) user-last-login-date-strs @@ -277,72 +293,78 @@ "Simcha Yan" "Spiros Teofil" "Szymon Theutrich"]} - :created_at $}) + :created_at $}) ((user->client :rasta) :get 200 (format "meta/table/%d/query_metadata?include_sensitive_fields=true" (id :users)))) ;;; GET api/meta/table/:id/query_metadata ;;; Make sure that getting the User table does *not* include password info (expect (match-$ (Table (id :users)) - {:description nil - :entity_type nil - :db (match-$ (db) - {:created_at $ - :engine "h2" - :id $ - :updated_at $ - :name "Test Database" - :organization_id nil - :description nil}) - :name "USERS" - :fields [(match-$ (Field (id :users :id)) - {:description nil - :table_id (id :users) - :special_type "id" - :name "ID" - :updated_at $ - :active true - :id $ - :field_type "info" - :position 0 - :target nil - :preview_display true - :created_at $ - :base_type "BigIntegerField"}) - (match-$ (Field (id :users :last_login)) - {:description nil - :table_id (id :users) - :special_type "category" - :name "LAST_LOGIN" - :updated_at $ - :active true - :id $ - :field_type "info" - :position 0 - :target nil - :preview_display true - :created_at $ - :base_type "DateTimeField"}) - (match-$ (Field (id :users :name)) - {:description nil - :table_id (id :users) - :special_type "category" - :name "NAME" - :updated_at $ - :active true - :id $ - :field_type "info" - :position 0 - :target nil - :preview_display true - :created_at $ - :base_type "TextField"})] - :rows 15 - :updated_at $ - :entity_name nil - :active true - :id (id :users) - :db_id (db-id) + {:description nil + :entity_type nil + :db (match-$ (db) + {:created_at $ + :engine "h2" + :id $ + :updated_at $ + :name "Test Database" + :organization_id nil + :description nil}) + :name "USERS" + :fields [(match-$ (Field (id :users :id)) + {:description nil + :table_id (id :users) + :special_type "id" + :name "ID" + :updated_at $ + :active true + :id $ + :field_type "info" + :position 0 + :target nil + :preview_display true + :created_at $ + :base_type "BigIntegerField" + :parent_id nil + :parent nil}) + (match-$ (Field (id :users :last_login)) + {:description nil + :table_id (id :users) + :special_type "category" + :name "LAST_LOGIN" + :updated_at $ + :active true + :id $ + :field_type "info" + :position 0 + :target nil + :preview_display true + :created_at $ + :base_type "DateTimeField" + :parent_id nil + :parent nil}) + (match-$ (Field (id :users :name)) + {:description nil + :table_id (id :users) + :special_type "category" + :name "NAME" + :updated_at $ + :active true + :id $ + :field_type "info" + :position 0 + :target nil + :preview_display true + :created_at $ + :base_type "TextField" + :parent_id nil + :parent nil})] + :rows 15 + :updated_at $ + :entity_name nil + :active true + :id (id :users) + :db_id (db-id) :field_values {(keyword (str (id :users :last_login))) user-last-login-date-strs @@ -362,7 +384,7 @@ "Simcha Yan" "Spiros Teofil" "Szymon Theutrich"]} - :created_at $}) + :created_at $}) ((user->client :rasta) :get 200 (format "meta/table/%d/query_metadata" (id :users)))) @@ -374,27 +396,27 @@ table) {:description "What a nice table!" :entity_type "person" - :db (match-$ (db) - {:description nil - :organization_id $ - :name "Test Database" - :updated_at $ - :details $ - :id $ - :engine "h2" - :created_at $}) - :name "USERS" - :rows 15 - :updated_at $ + :db (match-$ (db) + {:description nil + :organization_id $ + :name "Test Database" + :updated_at $ + :details $ + :id $ + :engine "h2" + :created_at $}) + :name "USERS" + :rows 15 + :updated_at $ :entity_name "Userz" - :active true - :pk_field (deref $pk_field) - :id $ - :db_id (db-id) - :created_at $}) + :active true + :pk_field (deref $pk_field) + :id $ + :db_id (db-id) + :created_at $}) (do ((user->client :crowberto) :put 200 (format "meta/table/%d" (id :users)) {:entity_name "Userz" - :entity_type "person" - :description "What a nice table!"}) + :entity_type "person" + :description "What a nice table!"}) ((user->client :crowberto) :get 200 (format "meta/table/%d" (id :users))))) @@ -403,78 +425,82 @@ (expect-let [checkins-user-field (sel :one Field :table_id (id :checkins) :name "USER_ID") users-id-field (sel :one Field :table_id (id :users) :name "ID")] [(match-$ (sel :one ForeignKey :destination_id (:id users-id-field)) - {:id $ - :origin_id (:id checkins-user-field) + {:id $ + :origin_id (:id checkins-user-field) :destination_id (:id users-id-field) - :relationship "Mt1" - :created_at $ - :updated_at $ - :origin (match-$ checkins-user-field - {:id $ - :table_id $ - :name "USER_ID" - :description nil - :base_type "IntegerField" - :preview_display $ - :position $ - :field_type "info" - :active true - :special_type "fk" - :created_at $ - :updated_at $ - :table (match-$ (Table (id :checkins)) - {:description nil - :entity_type nil - :name "CHECKINS" - :rows 1000 - :updated_at $ - :entity_name nil - :active true - :id $ - :db_id $ - :created_at $ - :db (match-$ (db) - {:description nil, - :organization_id nil, - :name "Test Database", - :updated_at $, - :id $, - :engine "h2", - :created_at $})})}) - :destination (match-$ users-id-field - {:id $ - :table_id $ - :name "ID" - :description nil - :base_type "BigIntegerField" - :preview_display $ - :position $ - :field_type "info" - :active true - :special_type "id" - :created_at $ - :updated_at $ - :table (match-$ (Table (id :users)) - {:description nil - :entity_type nil - :name "USERS" - :rows 15 - :updated_at $ - :entity_name nil - :active true - :id $ - :db_id $ - :created_at $})})})] + :relationship "Mt1" + :created_at $ + :updated_at $ + :origin (match-$ checkins-user-field + {:id $ + :table_id $ + :parent_id nil + :parent nil + :name "USER_ID" + :description nil + :base_type "IntegerField" + :preview_display $ + :position $ + :field_type "info" + :active true + :special_type "fk" + :created_at $ + :updated_at $ + :table (match-$ (Table (id :checkins)) + {:description nil + :entity_type nil + :name "CHECKINS" + :rows 1000 + :updated_at $ + :entity_name nil + :active true + :id $ + :db_id $ + :created_at $ + :db (match-$ (db) + {:description nil, + :organization_id nil, + :name "Test Database", + :updated_at $, + :id $, + :engine "h2", + :created_at $})})}) + :destination (match-$ users-id-field + {:id $ + :table_id $ + :parent_id nil + :parent nil + :name "ID" + :description nil + :base_type "BigIntegerField" + :preview_display $ + :position $ + :field_type "info" + :active true + :special_type "id" + :created_at $ + :updated_at $ + :table (match-$ (Table (id :users)) + {:description nil + :entity_type nil + :name "USERS" + :rows 15 + :updated_at $ + :entity_name nil + :active true + :id $ + :db_id $ + :created_at $})})})] ((user->client :rasta) :get 200 (format "meta/table/%d/fks" (id :users)))) ;; ## POST /api/meta/table/:id/reorder (expect-eval-actual-first - {:result "success"} - (let [categories-id-field (sel :one Field :table_id (id :categories) :name "ID") + {:result "success"} + (let [categories-id-field (sel :one Field :table_id (id :categories) :name "ID") categories-name-field (sel :one Field :table_id (id :categories) :name "NAME") - api-response ((user->client :crowberto) :post 200 (format "meta/table/%d/reorder" (id :categories)) - {:new_order [(:id categories-name-field) (:id categories-id-field)]})] + api-response ((user->client :crowberto) :post 200 (format "meta/table/%d/reorder" (id :categories)) + {:new_order [(:id categories-name-field) (:id categories-id-field)]})] ;; check the modified values (have to do it here because the api response tells us nothing) (assert (= 0 (:position (sel :one :fields [Field :position] :id (:id categories-name-field))))) (assert (= 1 (:position (sel :one :fields [Field :position] :id (:id categories-id-field))))) diff --git a/test/metabase/driver/mongo_test.clj b/test/metabase/driver/mongo_test.clj index f97fe8275c35ad114dca50ae5578f7966b93eab2..7f460990eea55f8f5922fc8cc993bb38f1ff9a59 100644 --- a/test/metabase/driver/mongo_test.clj +++ b/test/metabase/driver/mongo_test.clj @@ -59,8 +59,10 @@ [table-name field-name] {:pre [(keyword? table-name) (keyword? field-name)]} - {:name (name field-name) - :table (delay (table-name->fake-table table-name))}) + (let [table-delay (delay (table-name->fake-table table-name))] + {:name (name field-name) + :table table-delay + :qualified-name-components (delay [(name (:name @table-delay)) (name field-name)])})) ;; ## Tests for connection functions diff --git a/test/metabase/driver/query_processor_test.clj b/test/metabase/driver/query_processor_test.clj index 32c9b35a94430454521d257e818b651970bc56a6..aee567b2021325fa2e862014bf4014e01d0f8637 100644 --- a/test/metabase/driver/query_processor_test.clj +++ b/test/metabase/driver/query_processor_test.clj @@ -876,20 +876,18 @@ ;; +------------------------------------------------------------------------------------------------------------------------+ ;; The top 10 cities by number of Tupac sightings -;; Test that we can breakout on an FK field -;; (Note how the FK Field is returned in the results) -;; TODO - this is broken for Postgres! Returns columns in the wrong order -(datasets/expect-with-dataset :h2 - [[16 "Arlington"] - [15 "Albany"] - [14 "Portland"] - [13 "Louisville"] - [13 "Philadelphia"] - [12 "Anchorage"] - [12 "Lincoln"] - [11 "Houston"] - [11 "Irvine"] - [11 "Lakeland"]] +;; Test that we can breakout on an FK field (Note how the FK Field is returned in the results) +(datasets/expect-with-datasets #{:h2 :postgres} + [["Arlington" 16] + ["Albany" 15] + ["Portland" 14] + ["Louisville" 13] + ["Philadelphia" 13] + ["Anchorage" 12] + ["Lincoln" 12] + ["Houston" 11] + ["Irvine" 11] + ["Lakeland" 11]] (Q run with db tupac-sightings return :data :rows tbl sightings @@ -904,33 +902,32 @@ ;; Test that we can filter on an FK field (datasets/expect-with-datasets #{:h2 :postgres} 60 - (-> (query-with-temp-db defs/tupac-sightings - :source_table &sightings:id - :aggregation ["count"] - :filter ["=" ["fk->" &sightings.category_id:id &categories.id:id] 8]) - :data :rows first first)) + (Q run against tupac-sightings + return :data :rows first first + aggregate count of sightings + filter = category_id->categories.id 8)) ;; THE 10 MOST RECENT TUPAC SIGHTINGS (!) ;; (What he was doing when we saw him, sighting ID) ;; Check that we can include an FK field in the :fields clause (datasets/expect-with-datasets #{:h2 :postgres} - [["In the Park" 772] - ["Working at a Pet Store" 894] - ["At the Airport" 684] - ["At a Restaurant" 199] - ["Working as a Limo Driver" 33] - ["At Starbucks" 902] - ["On TV" 927] - ["At a Restaurant" 996] - ["Wearing a Biggie Shirt" 897] - ["In the Expa Office" 499]] - (->> (query-with-temp-db defs/tupac-sightings - :source_table &sightings:id - :fields [&sightings.id:id ["fk->" &sightings.category_id:id &categories.name:id]] - :order_by [[&sightings.timestamp:id "descending"]] - :limit 10) - :data :rows)) + [[772 "In the Park"] + [894 "Working at a Pet Store"] + [684 "At the Airport"] + [199 "At a Restaurant"] + [33 "Working as a Limo Driver"] + [902 "At Starbucks"] + [927 "On TV"] + [996 "At a Restaurant"] + [897 "Wearing a Biggie Shirt"] + [499 "In the Expa Office"]] + (Q run against tupac-sightings + return :data :rows + of sightings + fields id category_id->categories.name + order timestamp- + lim 10)) ;; 1. Check that we can order by Foreign Keys @@ -950,13 +947,11 @@ [2 11 524] [2 13 77] [2 13 202]] - (->> (query-with-temp-db defs/tupac-sightings - :source_table &sightings:id - :order_by [[["fk->" &sightings.city_id:id &cities.name:id] "ascending"] - [["fk->" &sightings.category_id:id &categories.name:id] "descending"] - [&sightings.id:id "ascending"]] - :limit 10) - :data :rows (map butlast) (map reverse))) ; drop timestamps. reverse ordering to make the results columns order match order_by + (Q run against tupac-sightings + return :data :rows (map butlast) (map reverse) ; drop timestamps. reverse ordering to make the results columns order match order_by + of sightings + order city_id->cities.name+ category_id->categories.name- id+ + lim 10)) ;; Check that trying to use a Foreign Key fails for Mongo @@ -969,3 +964,131 @@ [["fk->" &sightings.category_id:id &categories.name:id] "descending"] [&sightings.id:id "ascending"]] :limit 10)) + + +;; +------------------------------------------------------------------------------------------------------------------------+ +;; | MONGO NESTED-FIELD ACCESS | +;; +------------------------------------------------------------------------------------------------------------------------+ + +;;; Nested Field in FILTER +;; Get the first 10 tips where tip.venue.name == "Kyle's Low-Carb Grill" +(expect + [[8 "Kyle's Low-Carb Grill"] + [67 "Kyle's Low-Carb Grill"] + [80 "Kyle's Low-Carb Grill"] + [83 "Kyle's Low-Carb Grill"] + [295 "Kyle's Low-Carb Grill"] + [342 "Kyle's Low-Carb Grill"] + [417 "Kyle's Low-Carb Grill"] + [426 "Kyle's Low-Carb Grill"] + [470 "Kyle's Low-Carb Grill"]] + (Q run against geographical-tips using mongo + return :data :rows (map (fn [[id _ _ {venue-name :name}]] [id venue-name])) + aggregate rows of tips + filter = venue...name "Kyle's Low-Carb Grill" + order id + lim 10)) + +;;; Nested Field in ORDER +;; Let's get all the tips Kyle posted on Twitter sorted by tip.venue.name +(expect + [[446 + {:mentions ["@cams_mexican_gastro_pub"], :tags ["#mexican" "#gastro" "#pub"], :service "twitter", :username "kyle"} + {:large "http://cloudfront.net/6e3a5256-275f-4056-b61a-25990b4bb484/large.jpg", + :medium "http://cloudfront.net/6e3a5256-275f-4056-b61a-25990b4bb484/med.jpg", + :small "http://cloudfront.net/6e3a5256-275f-4056-b61a-25990b4bb484/small.jpg"} + {:phone "415-320-9123", :name "Cam's Mexican Gastro Pub", :categories ["Mexican" "Gastro Pub"], :id "bb958ac5-758e-4f42-b984-6b0e13f25194"}] + [230 + {:mentions ["@haight_european_grill"], :tags ["#european" "#grill"], :service "twitter", :username "kyle"} + {:large "http://cloudfront.net/1dcef7de-a1c4-405b-a9e1-69c92d686ef1/large.jpg", + :medium "http://cloudfront.net/1dcef7de-a1c4-405b-a9e1-69c92d686ef1/med.jpg", + :small "http://cloudfront.net/1dcef7de-a1c4-405b-a9e1-69c92d686ef1/small.jpg"} + {:phone "415-191-2778", :name "Haight European Grill", :categories ["European" "Grill"], :id "7e6281f7-5b17-4056-ada0-85453247bc8f"}] + [319 + {:mentions ["@haight_soul_food_pop_up_food_stand"], :tags ["#soul" "#food" "#pop-up" "#food" "#stand"], :service "twitter", :username "kyle"} + {:large "http://cloudfront.net/8f613909-550f-4d79-96f6-dc498ff65d1b/large.jpg", + :medium "http://cloudfront.net/8f613909-550f-4d79-96f6-dc498ff65d1b/med.jpg", + :small "http://cloudfront.net/8f613909-550f-4d79-96f6-dc498ff65d1b/small.jpg"} + {:phone "415-741-8726", :name "Haight Soul Food Pop-Up Food Stand", :categories ["Soul Food" "Pop-Up Food Stand"], :id "9735184b-1299-410f-a98e-10d9c548af42"}] + [224 + {:mentions ["@pacific_heights_free_range_eatery"], :tags ["#free-range" "#eatery"], :service "twitter", :username "kyle"} + {:large "http://cloudfront.net/cedd4221-dbdb-46c3-95a9-935cce6b3fe5/large.jpg", + :medium "http://cloudfront.net/cedd4221-dbdb-46c3-95a9-935cce6b3fe5/med.jpg", + :small "http://cloudfront.net/cedd4221-dbdb-46c3-95a9-935cce6b3fe5/small.jpg"} + {:phone "415-901-6541", :name "Pacific Heights Free-Range Eatery", :categories ["Free-Range" "Eatery"], :id "88b361c8-ce69-4b2e-b0f2-9deedd574af6"}]] + (Q run against geographical-tips using mongo + return :data :rows + aggregate rows of tips + filter and = source...service "twitter" + = source...username "kyle" + order venue...name)) + +;; Nested Field in AGGREGATION +;; Let's see how many *distinct* venue names are mentioned +(expect 99 + (Q run against geographical-tips using mongo + return :data :rows first first + aggregate distinct venue...name of tips)) + +;; Now let's just get the regular count +(expect 500 + (Q run against geographical-tips using mongo + return :data :rows first first + aggregate count venue...name of tips)) + +;;; Nested Field in BREAKOUT +;; Let's see how many tips we have by source.service +(expect + {:rows [["facebook" 107] + ["flare" 105] + ["foursquare" 100] + ["twitter" 98] + ["yelp" 90]] + :columns ["source.service" "count"]} + (Q run against geographical-tips using mongo + return :data (#(dissoc % :cols)) + aggregate count of tips + breakout source...service)) + +;;; Nested Field in FIELDS +;; Return the first 10 tips with just tip.venue.name +(expect + [[1 {:name "Lucky's Gluten-Free Café"}] + [2 {:name "Joe's Homestyle Eatery"}] + [3 {:name "Lower Pac Heights Cage-Free Coffee House"}] + [4 {:name "Oakland European Liquor Store"}] + [5 {:name "Tenderloin Gormet Restaurant"}] + [6 {:name "Marina Modern Sushi"}] + [7 {:name "Sunset Homestyle Grill"}] + [8 {:name "Kyle's Low-Carb Grill"}] + [9 {:name "Mission Homestyle Churros"}] + [10 {:name "Sameer's Pizza Liquor Store"}]] + (Q run against geographical-tips using mongo + return :data :rows + aggregate rows of tips + order id + fields venue...name + lim 10)) + + +;;; Nested Field w/ ordering by aggregation +(expect + [["jane" 4] + ["kyle" 5] + ["tupac" 5] + ["jessica" 6] + ["bob" 7] + ["lucky_pigeon" 7] + ["joe" 8] + ["mandy" 8] + ["amy" 9] + ["biggie" 9] + ["sameer" 9] + ["cam_saul" 10] + ["rasta_toucan" 13] + [nil 400]] + (Q run against geographical-tips using mongo + return :data :rows + aggregate count of tips + breakout source...mayor + order ag.0)) diff --git a/test/metabase/test/data.clj b/test/metabase/test/data.clj index 3bd4a711a248ce1f5ab639b701934b39e2874791..94bf8c3b1a86a1a315cf05e5b2fcb7f8f826abd0 100644 --- a/test/metabase/test/data.clj +++ b/test/metabase/test/data.clj @@ -13,7 +13,8 @@ (metabase.test.data [data :as data] [datasets :as datasets :refer [*dataset*]] [h2 :as h2] - [interface :refer :all])) + [interface :refer :all]) + [metabase.util :as u]) (:import clojure.lang.Keyword (metabase.test.data.interface DatabaseDefinition FieldDefinition @@ -87,15 +88,15 @@ (or (metabase-instance database-definition engine) (do ;; Create the database - (log/info (color/blue (format "Creating %s database %s..." (name engine) database-name))) + (log/info (u/format-color 'blue "Creating %s database %s..." (name engine) database-name)) (create-physical-db! dataset-loader database-definition) ;; Load data - (log/info (color/blue "Loading data...")) + (log/debug (color/blue "Loading data...")) (doseq [^TableDefinition table-definition (:table-definitions database-definition)] - (log/info (color/blue (format "Loading data for table '%s'..." (:table-name table-definition)))) + (log/info (u/format-color 'blue "Loading data for table '%s'..." (:table-name table-definition))) (load-table-data! dataset-loader database-definition table-definition) - (log/info (color/blue (format "Inserted %d rows." (count (:rows table-definition)))))) + (log/info (u/format-color 'blue "Inserted %d rows." (count (:rows table-definition))))) ;; Add DB object to Metabase DB (log/info (color/blue "Adding DB to Metabase...")) @@ -151,17 +152,21 @@ (defn- table-id->field-name->field "Return a map of lowercased `Field` names -> fields for `Table` with TABLE-ID." [table-id] - (->> (sel :many :field->obj [Field :name] :table_id table-id) - (m/map-keys s/lower-case))) + {:pre [(integer? table-id)]} + (->> (binding [*sel-disable-logging* true] + (sel :many :field->obj [Field :name], :table_id table-id, :parent_id nil)) + (m/map-keys s/lower-case) + (m/map-keys (u/rpartial s/replace #"^_id$" "id")))) ; rename Mongo _id fields to ID so we can use the same name for any driver (defn- db-id->table-name->table "Return a map of lowercased `Table` names -> Tables for `Database` with DATABASE-ID. Add a delay `:field-name->field` to each Table that calls `table-id->field-name->field` for that Table." [database-id] - (->> (sel :many :field->obj [Table :name] :db_id database-id) + {:pre [(integer? database-id)]} + (->> (binding [*sel-disable-logging* true] + (sel :many :field->obj [Table :name] :db_id database-id)) (m/map-keys s/lower-case) - (m/map-vals (fn [table] - (assoc table :field-name->field (delay (table-id->field-name->field (:id table)))))))) + (m/map-vals #(assoc % :field-name->field (delay (table-id->field-name->field (:id %))))))) (defn -temp-db-add-getter-delay "Add a delay `:table-name->table` to DB that calls `db-id->table-name->table`." @@ -174,11 +179,27 @@ With three args, fetch `Field` with FIELD-NAME by recursively fetching `Table` and using its `:field-name->field` delay." ([temp-db table-name] {:pre [(map? temp-db) - (string? table-name)]} + (string? table-name)] + :post [(or (map? %) (assert nil (format "Couldn't find table '%s'.\nValid choices are: %s" table-name + (vec (keys @(:table-name->table temp-db))))))]} (@(:table-name->table temp-db) table-name)) + ([temp-db table-name field-name] - {:pre [(string? field-name)]} - (@(:field-name->field (-temp-get temp-db table-name)) field-name))) + {:pre [(string? field-name)] + :post [(or (map? %) (assert nil (format "Couldn't find field '%s.%s'.\nValid choices are: %s" table-name field-name + (vec (keys @(:field-name->field (-temp-get temp-db table-name)))))))]} + (@(:field-name->field (-temp-get temp-db table-name)) field-name)) + + ([temp-db table-name parent-field-name & nested-field-names] + {:pre [(every? string? nested-field-names)] + :post [(or (map? %) (assert nil (format "Couldn't find nested field '%s.%s.%s'.\nValid choices are: %s" table-name parent-field-name + (apply str (interpose "." nested-field-names)) + (vec (map :name @(:children (apply -temp-get temp-db table-name parent-field-name (butlast nested-field-names))))))))]} + (binding [*sel-disable-logging* true] + (let [parent (apply -temp-get temp-db table-name parent-field-name (butlast nested-field-names)) + children @(:children parent) + child-name->child (zipmap (map :name children) children)] + (child-name->child (last nested-field-names)))))) (defn- walk-expand-& "Walk BODY looking for symbols like `&table` or `&table.field` and expand them to appropriate `-temp-get` forms. @@ -198,17 +219,20 @@ form)) body)) -(defn with-temp-db* [loader ^DatabaseDefinition dbdef f] +(defn -with-temp-db [loader ^DatabaseDefinition dbdef f] (let [dbdef (map->DatabaseDefinition (assoc dbdef :short-lived? true))] (try - (remove-database! loader dbdef) - (let [db (-> (get-or-create-database! loader dbdef) - -temp-db-add-getter-delay)] - (assert db) - (assert (exists? Database :id (:id db))) - (f db)) + (binding [*sel-disable-logging* true] + (remove-database! loader dbdef) + (let [db (-> (get-or-create-database! loader dbdef) + -temp-db-add-getter-delay)] + (assert db) + (assert (exists? Database :id (:id db))) + (binding [*sel-disable-logging* false] + (f db)))) (finally - (remove-database! loader dbdef))))) + (binding [*sel-disable-logging* true] + (remove-database! loader dbdef)))))) (defmacro with-temp-db "Load and sync DATABASE-DEFINITION with DATASET-LOADER and execute BODY with @@ -229,6 +253,6 @@ :aggregation [\"count\"] :filter [\"<\" (:id &events.timestamp) \"1765-01-01\"]}}))" [[db-binding dataset-loader ^DatabaseDefinition database-definition] & body] - `(with-temp-db* ~dataset-loader ~database-definition + `(-with-temp-db ~dataset-loader ~database-definition (fn [~db-binding] ~@(walk-expand-& db-binding body)))) diff --git a/test/metabase/test/data/dataset_definitions.clj b/test/metabase/test/data/dataset_definitions.clj index 5d98f2f7361cce7d64ea19781af6ba2d597e83cd..93868974f7536bbcff58f81b61a2fe81b4dfc218 100644 --- a/test/metabase/test/data/dataset_definitions.clj +++ b/test/metabase/test/data/dataset_definitions.clj @@ -37,3 +37,5 @@ ;; Places, times, and circumstances where Tupac was sighted (def-database-definition-edn tupac-sightings) + +(def-database-definition-edn geographical-tips) diff --git a/test/metabase/test/data/dataset_definitions/geographical-tips.edn b/test/metabase/test/data/dataset_definitions/geographical-tips.edn new file mode 100644 index 0000000000000000000000000000000000000000..271eafce334efe38559688479a75d214b8dd4eff --- /dev/null +++ b/test/metabase/test/data/dataset_definitions/geographical-tips.edn @@ -0,0 +1,3008 @@ +[["tips" [{:field-name "text" + :base-type :CharField} + {:field-name "url" + :base-type :UnknownField} + {:field-name "venue" + :base-type :UnknownField} + {:field-name "source" + :base-type :UnknownField}] + [["Lucky's Gluten-Free Café is a atmospheric and delicious place to have a drink during winter." + {:small "http://cloudfront.net/50576ac9-2211-4198-8915-265d32a6ba82/small.jpg", + :medium "http://cloudfront.net/50576ac9-2211-4198-8915-265d32a6ba82/med.jpg", + :large "http://cloudfront.net/50576ac9-2211-4198-8915-265d32a6ba82/large.jpg"} + {:name "Lucky's Gluten-Free Café", :categories ["Gluten-Free" "Café"], :phone "415-740-2328", :id "379af987-ad40-4a93-88a6-0233e1c14649"} + {:service "facebook", :facebook-photo-id "e46749c3-c532-4dc7-bdc3-da274de3c3ab", :url "http://facebook.com/photos/e46749c3-c532-4dc7-bdc3-da274de3c3ab"}] + ["Joe's Homestyle Eatery is a exclusive and historical place to have breakfast on public holidays." + {:small "http://cloudfront.net/b90e3288-c02c-4744-823a-d4110ca2d71b/small.jpg", + :medium "http://cloudfront.net/b90e3288-c02c-4744-823a-d4110ca2d71b/med.jpg", + :large "http://cloudfront.net/b90e3288-c02c-4744-823a-d4110ca2d71b/large.jpg"} + {:name "Joe's Homestyle Eatery", :categories ["Homestyle" "Eatery"], :phone "415-950-1337", :id "5cc18489-dfaf-417b-900f-5d1d61b961e8"} + {:service "flare", :username "mandy"}] + ["Lower Pac Heights Cage-Free Coffee House is a underground and fantastic place to catch a bite to eat on Saturday night." + {:small "http://cloudfront.net/2122308c-e15a-460e-98a0-a3b604db3cd1/small.jpg", + :medium "http://cloudfront.net/2122308c-e15a-460e-98a0-a3b604db3cd1/med.jpg", + :large "http://cloudfront.net/2122308c-e15a-460e-98a0-a3b604db3cd1/large.jpg"} + {:name "Lower Pac Heights Cage-Free Coffee House", :categories ["Cage-Free" "Coffee House"], :phone "415-697-9309", :id "02b1f618-41a0-406b-96dd-1a017f630b81"} + {:service "flare", :username "mandy"}] + ["Oakland European Liquor Store is a swell and wonderful place to take a date in July." + {:small "http://cloudfront.net/2fb9d1ae-a3d1-4b27-966e-260983f01813/small.jpg", + :medium "http://cloudfront.net/2fb9d1ae-a3d1-4b27-966e-260983f01813/med.jpg", + :large "http://cloudfront.net/2fb9d1ae-a3d1-4b27-966e-260983f01813/large.jpg"} + {:name "Oakland European Liquor Store", :categories ["European" "Liquor Store"], :phone "415-559-1516", :id "e342e7b7-e82d-475d-a822-b2df9c84850d"} + {:service "facebook", :facebook-photo-id "a9855e18-8612-4a97-b86a-aafe0d747bb4", :url "http://facebook.com/photos/a9855e18-8612-4a97-b86a-aafe0d747bb4"}] + ["Tenderloin Gormet Restaurant is a world-famous and popular place to have a drink during winter." + {:small "http://cloudfront.net/30ef9979-b1a9-40a9-82a4-32565401bab5/small.jpg", + :medium "http://cloudfront.net/30ef9979-b1a9-40a9-82a4-32565401bab5/med.jpg", + :large "http://cloudfront.net/30ef9979-b1a9-40a9-82a4-32565401bab5/large.jpg"} + {:name "Tenderloin Gormet Restaurant", :categories ["Gormet" "Restaurant"], :phone "415-127-4197", :id "54a9eac8-d80d-4af8-b6d7-34651a60e59c"} + {:service "twitter", :mentions ["@tenderloin_gormet_restaurant"], :tags ["#gormet" "#restaurant"], :username "tupac"}] + ["Marina Modern Sushi is a fantastic and underappreciated place to have a birthday party weekday afternoons." + {:small "http://cloudfront.net/bc7bac7f-5e92-4023-9b35-fd80f106274a/small.jpg", + :medium "http://cloudfront.net/bc7bac7f-5e92-4023-9b35-fd80f106274a/med.jpg", + :large "http://cloudfront.net/bc7bac7f-5e92-4023-9b35-fd80f106274a/large.jpg"} + {:name "Marina Modern Sushi", :categories ["Modern" "Sushi"], :phone "415-393-7672", :id "21807c63-ca4c-4468-9844-d0c2620fbdfc"} + {:service "facebook", :facebook-photo-id "58084e61-6381-4313-83be-6e35c8424600", :url "http://facebook.com/photos/58084e61-6381-4313-83be-6e35c8424600"}] + ["Sunset Homestyle Grill is a world-famous and well-decorated place to conduct a business meeting Friday nights." + {:small "http://cloudfront.net/3fc720ca-07d7-48b7-bcab-550d951f58b9/small.jpg", + :medium "http://cloudfront.net/3fc720ca-07d7-48b7-bcab-550d951f58b9/med.jpg", + :large "http://cloudfront.net/3fc720ca-07d7-48b7-bcab-550d951f58b9/large.jpg"} + {:name "Sunset Homestyle Grill", :categories ["Homestyle" "Grill"], :phone "415-356-7052", :id "c57673cd-f2d0-4bbc-aed0-6c166d7cf2c3"} + {:service "yelp", :yelp-photo-id "976fdffd-70ca-4b57-b214-80f0eb80f619", :categories ["Homestyle" "Grill"]}] + ["Kyle's Low-Carb Grill is a delicious and underappreciated place to have a after-work cocktail after baseball games." + {:small "http://cloudfront.net/49b48260-5c56-4873-956e-c13423f4feed/small.jpg", + :medium "http://cloudfront.net/49b48260-5c56-4873-956e-c13423f4feed/med.jpg", + :large "http://cloudfront.net/49b48260-5c56-4873-956e-c13423f4feed/large.jpg"} + {:name "Kyle's Low-Carb Grill", :categories ["Low-Carb" "Grill"], :phone "415-992-8278", :id "b27f50c6-55eb-48b0-9fee-17a6ef5243bd"} + {:service "yelp", :yelp-photo-id "058b7d4e-b92b-4408-ba67-659f5b640e4b", :categories ["Low-Carb" "Grill"]}] + ["Mission Homestyle Churros is a groovy and wonderful place to catch a bite to eat weekday afternoons." + {:small "http://cloudfront.net/2110742e-7e30-4a0d-8df4-1d20d2503f42/small.jpg", + :medium "http://cloudfront.net/2110742e-7e30-4a0d-8df4-1d20d2503f42/med.jpg", + :large "http://cloudfront.net/2110742e-7e30-4a0d-8df4-1d20d2503f42/large.jpg"} + {:name "Mission Homestyle Churros", :categories ["Homestyle" "Churros"], :phone "415-343-4489", :id "21d903d3-8bdb-4b7d-b288-6063ad48af44"} + {:service "facebook", :facebook-photo-id "b2a4862c-c5b7-4cfb-8ff6-fb564c28f4c8", :url "http://facebook.com/photos/b2a4862c-c5b7-4cfb-8ff6-fb564c28f4c8"}] + ["Sameer's Pizza Liquor Store is a overrated and decent place to sip a glass of expensive wine during summer." + {:small "http://cloudfront.net/df8c679d-93d5-4008-a129-b33711892cba/small.jpg", + :medium "http://cloudfront.net/df8c679d-93d5-4008-a129-b33711892cba/med.jpg", + :large "http://cloudfront.net/df8c679d-93d5-4008-a129-b33711892cba/large.jpg"} + {:name "Sameer's Pizza Liquor Store", :categories ["Pizza" "Liquor Store"], :phone "415-969-7474", :id "7b9c7dc3-d8f1-498d-843a-e62360449892"} + {:service "twitter", :mentions ["@sameers_pizza_liquor_store"], :tags ["#pizza" "#liquor" "#store"], :username "cam_saul"}] + ["Market St. European Ice Cream Truck is a overrated and overrated place to watch the Giants game on Saturday night." + {:small "http://cloudfront.net/25be5a46-88b0-4ac7-965d-196d4dbab4b8/small.jpg", + :medium "http://cloudfront.net/25be5a46-88b0-4ac7-965d-196d4dbab4b8/med.jpg", + :large "http://cloudfront.net/25be5a46-88b0-4ac7-965d-196d4dbab4b8/large.jpg"} + {:name "Market St. European Ice Cream Truck", :categories ["European" "Ice Cream Truck"], :phone "415-555-4197", :id "4ed53fe4-4bd9-4fa3-8f61-374ea75129ca"} + {:service "facebook", :facebook-photo-id "d3717643-c5c3-4e36-8721-99c2fd65972f", :url "http://facebook.com/photos/d3717643-c5c3-4e36-8721-99c2fd65972f"}] + ["Haight Mexican Restaurant is a fantastic and overrated place to sip a glass of expensive wine with your pet toucan." + {:small "http://cloudfront.net/d01af223-d655-4b62-83a9-63a03118b288/small.jpg", + :medium "http://cloudfront.net/d01af223-d655-4b62-83a9-63a03118b288/med.jpg", + :large "http://cloudfront.net/d01af223-d655-4b62-83a9-63a03118b288/large.jpg"} + {:name "Haight Mexican Restaurant", :categories ["Mexican" "Restaurant"], :phone "415-758-8690", :id "a5e3f0ac-f6e8-4e71-a0a2-3f10f48b4ab1"} + {:service "foursquare", :foursquare-photo-id "5eac8186-24ef-4369-9b46-3bbec215a9c3", :mayor "cam_saul"}] + ["Rasta's Mexican Sushi is a overrated and well-decorated place to watch the Warriors game with your pet toucan." + {:small "http://cloudfront.net/c807068e-1def-4902-987a-75e4e06636f9/small.jpg", + :medium "http://cloudfront.net/c807068e-1def-4902-987a-75e4e06636f9/med.jpg", + :large "http://cloudfront.net/c807068e-1def-4902-987a-75e4e06636f9/large.jpg"} + {:name "Rasta's Mexican Sushi", :categories ["Mexican" "Sushi"], :phone "415-387-1284", :id "e4912a22-e6ac-4806-8377-6497bf533a21"} + {:service "flare", :username "amy"}] + ["Market St. European Ice Cream Truck is a amazing and groovy place to have a drink on Saturday night." + {:small "http://cloudfront.net/1b91096b-206b-4823-8ef9-13f0bf9ac76e/small.jpg", + :medium "http://cloudfront.net/1b91096b-206b-4823-8ef9-13f0bf9ac76e/med.jpg", + :large "http://cloudfront.net/1b91096b-206b-4823-8ef9-13f0bf9ac76e/large.jpg"} + {:name "Market St. European Ice Cream Truck", :categories ["European" "Ice Cream Truck"], :phone "415-555-4197", :id "4ed53fe4-4bd9-4fa3-8f61-374ea75129ca"} + {:service "facebook", :facebook-photo-id "d7ab7046-43cd-472e-a48d-1b11cd3f7b55", :url "http://facebook.com/photos/d7ab7046-43cd-472e-a48d-1b11cd3f7b55"}] + ["Tenderloin Paleo Hotel & Restaurant is a underappreciated and fantastic place to take visiting friends and relatives after baseball games." + {:small "http://cloudfront.net/5ad20c96-0a12-43ff-97e9-8b8b7e373b6c/small.jpg", + :medium "http://cloudfront.net/5ad20c96-0a12-43ff-97e9-8b8b7e373b6c/med.jpg", + :large "http://cloudfront.net/5ad20c96-0a12-43ff-97e9-8b8b7e373b6c/large.jpg"} + {:name "Tenderloin Paleo Hotel & Restaurant", :categories ["Paleo" "Hotel & Restaurant"], :phone "415-402-1652", :id "4dea27b4-6d89-4b86-80a8-5631e171da8d"} + {:service "flare", :username "lucky_pigeon"}] + ["Lower Pac Heights Cage-Free Coffee House is a well-decorated and amazing place to watch the Warriors game when hungover." + {:small "http://cloudfront.net/5809d5db-cfc0-4f54-8760-228bdb3e1697/small.jpg", + :medium "http://cloudfront.net/5809d5db-cfc0-4f54-8760-228bdb3e1697/med.jpg", + :large "http://cloudfront.net/5809d5db-cfc0-4f54-8760-228bdb3e1697/large.jpg"} + {:name "Lower Pac Heights Cage-Free Coffee House", :categories ["Cage-Free" "Coffee House"], :phone "415-697-9309", :id "02b1f618-41a0-406b-96dd-1a017f630b81"} + {:service "foursquare", :foursquare-photo-id "b8ef27c8-8dc4-45ff-9485-c130889eaea1", :mayor "joe"}] + ["Polk St. Mexican Coffee House is a world-famous and modern place to pitch an investor in the spring." + {:small "http://cloudfront.net/a351a826-49e8-43d5-b983-7b4f013e872e/small.jpg", + :medium "http://cloudfront.net/a351a826-49e8-43d5-b983-7b4f013e872e/med.jpg", + :large "http://cloudfront.net/a351a826-49e8-43d5-b983-7b4f013e872e/large.jpg"} + {:name "Polk St. Mexican Coffee House", :categories ["Mexican" "Coffee House"], :phone "415-144-7901", :id "396d36d7-13ad-41fd-86b5-8b70b6ecdabf"} + {:service "facebook", :facebook-photo-id "3c80ed1f-409b-4cfa-bf78-9bbc8cd8f9f5", :url "http://facebook.com/photos/3c80ed1f-409b-4cfa-bf78-9bbc8cd8f9f5"}] + ["Sunset Homestyle Grill is a amazing and world-famous place to drink a craft beer when hungover." + {:small "http://cloudfront.net/d6ff64d9-1779-4cbf-bbbf-6f666a67691e/small.jpg", + :medium "http://cloudfront.net/d6ff64d9-1779-4cbf-bbbf-6f666a67691e/med.jpg", + :large "http://cloudfront.net/d6ff64d9-1779-4cbf-bbbf-6f666a67691e/large.jpg"} + {:name "Sunset Homestyle Grill", :categories ["Homestyle" "Grill"], :phone "415-356-7052", :id "c57673cd-f2d0-4bbc-aed0-6c166d7cf2c3"} + {:service "yelp", :yelp-photo-id "4ab42195-c3df-49e2-b8e9-20e4f5fae63b", :categories ["Homestyle" "Grill"]}] + ["Marina Modern Bar & Grill is a atmospheric and great place to have breakfast during winter." + {:small "http://cloudfront.net/95842ec7-663b-48db-b667-d6b70d0c194d/small.jpg", + :medium "http://cloudfront.net/95842ec7-663b-48db-b667-d6b70d0c194d/med.jpg", + :large "http://cloudfront.net/95842ec7-663b-48db-b667-d6b70d0c194d/large.jpg"} + {:name "Marina Modern Bar & Grill", :categories ["Modern" "Bar & Grill"], :phone "415-203-8530", :id "806144f1-bb7a-4271-8fcb-fc6550f51676"} + {:service "facebook", :facebook-photo-id "0a3e4b0e-ecd7-4c55-8d5f-2265ad57b02a", :url "http://facebook.com/photos/0a3e4b0e-ecd7-4c55-8d5f-2265ad57b02a"}] + ["Polk St. Mexican Coffee House is a swell and wonderful place to have a after-work cocktail in June." + {:small "http://cloudfront.net/4b773ee9-a6db-4e6a-8314-24530a35cdf5/small.jpg", + :medium "http://cloudfront.net/4b773ee9-a6db-4e6a-8314-24530a35cdf5/med.jpg", + :large "http://cloudfront.net/4b773ee9-a6db-4e6a-8314-24530a35cdf5/large.jpg"} + {:name "Polk St. Mexican Coffee House", :categories ["Mexican" "Coffee House"], :phone "415-144-7901", :id "396d36d7-13ad-41fd-86b5-8b70b6ecdabf"} + {:service "twitter", :mentions ["@polk_st._mexican_coffee_house"], :tags ["#mexican" "#coffee" "#house"], :username "amy"}] + ["Nob Hill Korean Taqueria is a decent and modern place to catch a bite to eat on Taco Tuesday." + {:small "http://cloudfront.net/e87adb96-9303-4728-a2e2-ce4d2c1edb74/small.jpg", + :medium "http://cloudfront.net/e87adb96-9303-4728-a2e2-ce4d2c1edb74/med.jpg", + :large "http://cloudfront.net/e87adb96-9303-4728-a2e2-ce4d2c1edb74/large.jpg"} + {:name "Nob Hill Korean Taqueria", :categories ["Korean" "Taqueria"], :phone "415-107-7332", :id "a43c184c-90f5-488c-bb3b-00ea2666d90e"} + {:service "yelp", :yelp-photo-id "b0b7027e-5884-48fb-93d4-d11f44c564df", :categories ["Korean" "Taqueria"]}] + ["SF Deep-Dish Eatery is a world-famous and historical place to catch a bite to eat on a Tuesday afternoon." + {:small "http://cloudfront.net/bda4e656-d27d-44f2-a0ca-8aa8856a4eb5/small.jpg", + :medium "http://cloudfront.net/bda4e656-d27d-44f2-a0ca-8aa8856a4eb5/med.jpg", + :large "http://cloudfront.net/bda4e656-d27d-44f2-a0ca-8aa8856a4eb5/large.jpg"} + {:name "SF Deep-Dish Eatery", :categories ["Deep-Dish" "Eatery"], :phone "415-476-9257", :id "ad41d3f6-c20c-46a7-9e5d-db602fff7d0d"} + {:service "foursquare", :foursquare-photo-id "217e84df-2032-4e13-b32d-20745fc36be0", :mayor "amy"}] + ["Sunset Deep-Dish Hotel & Restaurant is a popular and groovy place to have a drink weekend mornings." + {:small "http://cloudfront.net/6f15c573-5718-4e17-a64b-5b5ac3a4be00/small.jpg", + :medium "http://cloudfront.net/6f15c573-5718-4e17-a64b-5b5ac3a4be00/med.jpg", + :large "http://cloudfront.net/6f15c573-5718-4e17-a64b-5b5ac3a4be00/large.jpg"} + {:name "Sunset Deep-Dish Hotel & Restaurant", :categories ["Deep-Dish" "Hotel & Restaurant"], :phone "415-332-0978", :id "a80745c7-af74-4579-8932-70dd488269e6"} + {:service "twitter", :mentions ["@sunset_deep_dish_hotel_&_restaurant"], :tags ["#deep-dish" "#hotel" "#&" "#restaurant"], :username "lucky_pigeon"}] + ["Polk St. Japanese Liquor Store is a fantastic and delicious place to catch a bite to eat with your pet dog." + {:small "http://cloudfront.net/5af61fa2-9031-4be5-86c8-210e9612a184/small.jpg", + :medium "http://cloudfront.net/5af61fa2-9031-4be5-86c8-210e9612a184/med.jpg", + :large "http://cloudfront.net/5af61fa2-9031-4be5-86c8-210e9612a184/large.jpg"} + {:name "Polk St. Japanese Liquor Store", :categories ["Japanese" "Liquor Store"], :phone "415-726-7986", :id "b57ceac5-328d-4b65-9909-a1f9abc93015"} + {:service "foursquare", :foursquare-photo-id "0928a8d9-8198-4004-ae00-58025bc98a4b", :mayor "mandy"}] + ["Chinatown Paleo Food Truck is a modern and underappreciated place to nurse a hangover weekend mornings." + {:small "http://cloudfront.net/55ab1c20-acae-491b-afa2-455e35c924bd/small.jpg", + :medium "http://cloudfront.net/55ab1c20-acae-491b-afa2-455e35c924bd/med.jpg", + :large "http://cloudfront.net/55ab1c20-acae-491b-afa2-455e35c924bd/large.jpg"} + {:name "Chinatown Paleo Food Truck", :categories ["Paleo" "Food Truck"], :phone "415-583-4380", :id "aa9b5ce9-db74-470e-8573-f2faca24d546"} + {:service "facebook", :facebook-photo-id "533e16df-df5c-4368-81fe-ca4c53797f4c", :url "http://facebook.com/photos/533e16df-df5c-4368-81fe-ca4c53797f4c"}] + ["Rasta's Paleo Churros is a well-decorated and underground place to sip a glass of expensive wine in the fall." + {:small "http://cloudfront.net/45c98735-bbc7-4d81-bac6-d45527446f71/small.jpg", + :medium "http://cloudfront.net/45c98735-bbc7-4d81-bac6-d45527446f71/med.jpg", + :large "http://cloudfront.net/45c98735-bbc7-4d81-bac6-d45527446f71/large.jpg"} + {:name "Rasta's Paleo Churros", :categories ["Paleo" "Churros"], :phone "415-915-0309", :id "3bf48ec6-434b-43b1-be28-7644975ecaf9"} + {:service "facebook", :facebook-photo-id "71b1c7b4-cf56-4cb1-985a-d97edb8bda7c", :url "http://facebook.com/photos/71b1c7b4-cf56-4cb1-985a-d97edb8bda7c"}] + ["Market St. Gluten-Free Café is a classic and exclusive place to drink a craft beer with friends." + {:small "http://cloudfront.net/569435ff-07e5-4369-bf69-075ee03b3f74/small.jpg", + :medium "http://cloudfront.net/569435ff-07e5-4369-bf69-075ee03b3f74/med.jpg", + :large "http://cloudfront.net/569435ff-07e5-4369-bf69-075ee03b3f74/large.jpg"} + {:name "Market St. Gluten-Free Café", :categories ["Gluten-Free" "Café"], :phone "415-697-9776", :id "ce4947d0-071a-4491-b5ac-f8b0241bd54c"} + {:service "facebook", :facebook-photo-id "250305eb-9827-43b8-a190-401a7170eb1e", :url "http://facebook.com/photos/250305eb-9827-43b8-a190-401a7170eb1e"}] + ["Lucky's Old-Fashioned Eatery is a underground and delicious place to watch the Warriors game during summer." + {:small "http://cloudfront.net/188511ac-9217-42ca-8d1e-973072669935/small.jpg", + :medium "http://cloudfront.net/188511ac-9217-42ca-8d1e-973072669935/med.jpg", + :large "http://cloudfront.net/188511ac-9217-42ca-8d1e-973072669935/large.jpg"} + {:name "Lucky's Old-Fashioned Eatery", :categories ["Old-Fashioned" "Eatery"], :phone "415-362-2338", :id "71dc221c-6e82-4d06-8709-93293121b1da"} + {:service "foursquare", :foursquare-photo-id "c1759648-8365-48ca-8068-f3ba5fbb32a4", :mayor "mandy"}] + ["Polk St. Japanese Liquor Store is a underappreciated and modern place to catch a bite to eat weekend mornings." + {:small "http://cloudfront.net/f143dd2d-b46d-4444-ac48-f5253fa0fdae/small.jpg", + :medium "http://cloudfront.net/f143dd2d-b46d-4444-ac48-f5253fa0fdae/med.jpg", + :large "http://cloudfront.net/f143dd2d-b46d-4444-ac48-f5253fa0fdae/large.jpg"} + {:name "Polk St. Japanese Liquor Store", :categories ["Japanese" "Liquor Store"], :phone "415-726-7986", :id "b57ceac5-328d-4b65-9909-a1f9abc93015"} + {:service "facebook", :facebook-photo-id "3f7b182d-22b8-401c-939d-bd08cde5a9ac", :url "http://facebook.com/photos/3f7b182d-22b8-401c-939d-bd08cde5a9ac"}] + ["Cam's Mexican Gastro Pub is a great and world-famous place to catch a bite to eat when hungover." + {:small "http://cloudfront.net/dc69ae89-81f7-49d3-aa4d-ce48621f7497/small.jpg", + :medium "http://cloudfront.net/dc69ae89-81f7-49d3-aa4d-ce48621f7497/med.jpg", + :large "http://cloudfront.net/dc69ae89-81f7-49d3-aa4d-ce48621f7497/large.jpg"} + {:name "Cam's Mexican Gastro Pub", :categories ["Mexican" "Gastro Pub"], :phone "415-320-9123", :id "bb958ac5-758e-4f42-b984-6b0e13f25194"} + {:service "foursquare", :foursquare-photo-id "6a0d547d-2053-4c14-b35f-f658cfc21f84", :mayor "sameer"}] + ["Lucky's Gluten-Free Gastro Pub is a wonderful and horrible place to have a birthday party with friends." + {:small "http://cloudfront.net/5c9aedc4-916f-4552-b520-084495bf5cce/small.jpg", + :medium "http://cloudfront.net/5c9aedc4-916f-4552-b520-084495bf5cce/med.jpg", + :large "http://cloudfront.net/5c9aedc4-916f-4552-b520-084495bf5cce/large.jpg"} + {:name "Lucky's Gluten-Free Gastro Pub", :categories ["Gluten-Free" "Gastro Pub"], :phone "415-391-6443", :id "7ccf8bbb-74b4-48f7-aa7c-43872f63cb1b"} + {:service "facebook", :facebook-photo-id "c5fff3c3-8ea4-42d2-b3dd-29d83c6a9ed2", :url "http://facebook.com/photos/c5fff3c3-8ea4-42d2-b3dd-29d83c6a9ed2"}] + ["Kyle's Chinese Restaurant is a classic and decent place to catch a bite to eat the first Sunday of the month." + {:small "http://cloudfront.net/460f06c2-3fd1-4356-a2ed-cddfef334a76/small.jpg", + :medium "http://cloudfront.net/460f06c2-3fd1-4356-a2ed-cddfef334a76/med.jpg", + :large "http://cloudfront.net/460f06c2-3fd1-4356-a2ed-cddfef334a76/large.jpg"} + {:name "Kyle's Chinese Restaurant", :categories ["Chinese" "Restaurant"], :phone "415-298-9499", :id "de08b3c7-9929-40d8-8c20-dd9317613c17"} + {:service "foursquare", :foursquare-photo-id "eff5660c-a3bf-42e5-9406-a9181f3abf41", :mayor "lucky_pigeon"}] + ["Nob Hill Gluten-Free Coffee House is a wonderful and overrated place to meet new friends the second Saturday of the month." + {:small "http://cloudfront.net/3e975ead-6942-48d4-98a4-01147baf0e9c/small.jpg", + :medium "http://cloudfront.net/3e975ead-6942-48d4-98a4-01147baf0e9c/med.jpg", + :large "http://cloudfront.net/3e975ead-6942-48d4-98a4-01147baf0e9c/large.jpg"} + {:name "Nob Hill Gluten-Free Coffee House", :categories ["Gluten-Free" "Coffee House"], :phone "415-605-9554", :id "df57ff6d-1b5f-46da-b292-32321c6b1a7e"} + {:service "foursquare", :foursquare-photo-id "901ae47c-e6f2-4511-a20b-b89e2b600239", :mayor "cam_saul"}] + ["Sunset American Churros is a amazing and exclusive place to have a birthday party the second Saturday of the month." + {:small "http://cloudfront.net/7db04c5a-49bd-4606-b3c6-af074075db64/small.jpg", + :medium "http://cloudfront.net/7db04c5a-49bd-4606-b3c6-af074075db64/med.jpg", + :large "http://cloudfront.net/7db04c5a-49bd-4606-b3c6-af074075db64/large.jpg"} + {:name "Sunset American Churros", :categories ["American" "Churros"], :phone "415-191-5018", :id "2e88c921-29fb-489b-a956-d3ba1182da73"} + {:service "facebook", :facebook-photo-id "b4f4f18b-fb73-42a5-9628-607f6526a8ca", :url "http://facebook.com/photos/b4f4f18b-fb73-42a5-9628-607f6526a8ca"}] + ["SoMa British Bakery is a atmospheric and underground place to sip Champagne in July." + {:small "http://cloudfront.net/e3da9d78-f911-451c-a64c-929ecbfb477d/small.jpg", + :medium "http://cloudfront.net/e3da9d78-f911-451c-a64c-929ecbfb477d/med.jpg", + :large "http://cloudfront.net/e3da9d78-f911-451c-a64c-929ecbfb477d/large.jpg"} + {:name "SoMa British Bakery", :categories ["British" "Bakery"], :phone "415-909-5728", :id "662cb0d0-8ee6-4db7-aaf1-89eb2530feda"} + {:service "flare", :username "jessica"}] + ["Tenderloin Cage-Free Sushi is a swell and historical place to people-watch on Thursdays." + {:small "http://cloudfront.net/7ea17999-9e65-414a-a86c-891cb0ee41bc/small.jpg", + :medium "http://cloudfront.net/7ea17999-9e65-414a-a86c-891cb0ee41bc/med.jpg", + :large "http://cloudfront.net/7ea17999-9e65-414a-a86c-891cb0ee41bc/large.jpg"} + {:name "Tenderloin Cage-Free Sushi", :categories ["Cage-Free" "Sushi"], :phone "415-348-0644", :id "0b6c036f-82b0-4008-bdfe-5360dd93fb75"} + {:service "twitter", :mentions ["@tenderloin_cage_free_sushi"], :tags ["#cage-free" "#sushi"], :username "joe"}] + ["Sunset American Churros is a great and atmospheric place to conduct a business meeting with friends." + {:small "http://cloudfront.net/e41c5cf4-8ccd-410e-bbad-feed53827baf/small.jpg", + :medium "http://cloudfront.net/e41c5cf4-8ccd-410e-bbad-feed53827baf/med.jpg", + :large "http://cloudfront.net/e41c5cf4-8ccd-410e-bbad-feed53827baf/large.jpg"} + {:name "Sunset American Churros", :categories ["American" "Churros"], :phone "415-191-5018", :id "2e88c921-29fb-489b-a956-d3ba1182da73"} + {:service "yelp", :yelp-photo-id "6e4d9aff-63d3-4b00-9134-a4a3727b9d8d", :categories ["American" "Churros"]}] + ["Rasta's British Food Truck is a fantastic and underground place to sip a glass of expensive wine in June." + {:small "http://cloudfront.net/6670e042-020e-4919-90da-020cf93fab95/small.jpg", + :medium "http://cloudfront.net/6670e042-020e-4919-90da-020cf93fab95/med.jpg", + :large "http://cloudfront.net/6670e042-020e-4919-90da-020cf93fab95/large.jpg"} + {:name "Rasta's British Food Truck", :categories ["British" "Food Truck"], :phone "415-958-9031", :id "b6616c97-01d0-488f-a855-bcd6efe2b899"} + {:service "facebook", :facebook-photo-id "ff353525-8a3c-4510-85ec-c22d5fe5b831", :url "http://facebook.com/photos/ff353525-8a3c-4510-85ec-c22d5fe5b831"}] + ["Haight Gormet Pizzeria is a swell and modern place to have a after-work cocktail weekday afternoons." + {:small "http://cloudfront.net/0d491f2a-db93-4226-bbee-8e3647a6d3fa/small.jpg", + :medium "http://cloudfront.net/0d491f2a-db93-4226-bbee-8e3647a6d3fa/med.jpg", + :large "http://cloudfront.net/0d491f2a-db93-4226-bbee-8e3647a6d3fa/large.jpg"} + {:name "Haight Gormet Pizzeria", :categories ["Gormet" "Pizzeria"], :phone "415-869-2197", :id "0425bdd0-3f57-4108-80e3-78335327355a"} + {:service "facebook", :facebook-photo-id "6387d17f-d000-488b-b9f4-5f808e517f28", :url "http://facebook.com/photos/6387d17f-d000-488b-b9f4-5f808e517f28"}] + ["Haight Chinese Gastro Pub is a modern and great place to meet new friends the second Saturday of the month." + {:small "http://cloudfront.net/562d5a4b-6d03-43b4-89f6-68270448c8cc/small.jpg", + :medium "http://cloudfront.net/562d5a4b-6d03-43b4-89f6-68270448c8cc/med.jpg", + :large "http://cloudfront.net/562d5a4b-6d03-43b4-89f6-68270448c8cc/large.jpg"} + {:name "Haight Chinese Gastro Pub", :categories ["Chinese" "Gastro Pub"], :phone "415-521-5825", :id "12a8dc6e-1b2c-47e2-9c18-3ae220e4806f"} + {:service "facebook", :facebook-photo-id "8c4a5b4b-f72b-41bc-8242-3d31df7f175a", :url "http://facebook.com/photos/8c4a5b4b-f72b-41bc-8242-3d31df7f175a"}] + ["Cam's Old-Fashioned Coffee House is a fantastic and overrated place to have brunch in the spring." + {:small "http://cloudfront.net/06e5458e-25da-45b6-bb26-7662ac033edc/small.jpg", + :medium "http://cloudfront.net/06e5458e-25da-45b6-bb26-7662ac033edc/med.jpg", + :large "http://cloudfront.net/06e5458e-25da-45b6-bb26-7662ac033edc/large.jpg"} + {:name "Cam's Old-Fashioned Coffee House", :categories ["Old-Fashioned" "Coffee House"], :phone "415-871-9473", :id "5d1f918e-ef00-40de-b6e4-3f9f34ee8cd4"} + {:service "yelp", :yelp-photo-id "0562bf77-bb42-464e-951f-6e18206de08b", :categories ["Old-Fashioned" "Coffee House"]}] + ["SoMa Old-Fashioned Pizzeria is a underappreciated and wonderful place to have a after-work cocktail on a Tuesday afternoon." + {:small "http://cloudfront.net/c727c895-8572-453b-b8bf-921298fb9240/small.jpg", + :medium "http://cloudfront.net/c727c895-8572-453b-b8bf-921298fb9240/med.jpg", + :large "http://cloudfront.net/c727c895-8572-453b-b8bf-921298fb9240/large.jpg"} + {:name "SoMa Old-Fashioned Pizzeria", :categories ["Old-Fashioned" "Pizzeria"], :phone "415-966-8856", :id "deb8997b-734d-402b-a181-bd888214bc86"} + {:service "foursquare", :foursquare-photo-id "506df972-9899-40d8-be2d-052a0d32730b", :mayor "joe"}] + ["Sameer's Pizza Liquor Store is a classic and decent place to have a birthday party with your pet dog." + {:small "http://cloudfront.net/4c023049-681d-4c45-a1ed-b7859bb9b8aa/small.jpg", + :medium "http://cloudfront.net/4c023049-681d-4c45-a1ed-b7859bb9b8aa/med.jpg", + :large "http://cloudfront.net/4c023049-681d-4c45-a1ed-b7859bb9b8aa/large.jpg"} + {:name "Sameer's Pizza Liquor Store", :categories ["Pizza" "Liquor Store"], :phone "415-969-7474", :id "7b9c7dc3-d8f1-498d-843a-e62360449892"} + {:service "twitter", :mentions ["@sameers_pizza_liquor_store"], :tags ["#pizza" "#liquor" "#store"], :username "sameer"}] + ["Oakland Afgan Coffee House is a wonderful and historical place to watch the Warriors game Friday nights." + {:small "http://cloudfront.net/8f18bcd6-66de-414a-a087-5d57f042b26b/small.jpg", + :medium "http://cloudfront.net/8f18bcd6-66de-414a-a087-5d57f042b26b/med.jpg", + :large "http://cloudfront.net/8f18bcd6-66de-414a-a087-5d57f042b26b/large.jpg"} + {:name "Oakland Afgan Coffee House", :categories ["Afgan" "Coffee House"], :phone "415-674-0208", :id "dcc9efd9-f34c-4ca1-9a41-386f1130f411"} + {:service "facebook", :facebook-photo-id "d4b18407-5358-43a0-8bee-c53606ceb4b4", :url "http://facebook.com/photos/d4b18407-5358-43a0-8bee-c53606ceb4b4"}] + ["Mission Homestyle Churros is a swell and well-decorated place to sip Champagne with your pet dog." + {:small "http://cloudfront.net/58de8593-50a8-494f-9aa9-99f64db4339b/small.jpg", + :medium "http://cloudfront.net/58de8593-50a8-494f-9aa9-99f64db4339b/med.jpg", + :large "http://cloudfront.net/58de8593-50a8-494f-9aa9-99f64db4339b/large.jpg"} + {:name "Mission Homestyle Churros", :categories ["Homestyle" "Churros"], :phone "415-343-4489", :id "21d903d3-8bdb-4b7d-b288-6063ad48af44"} + {:service "yelp", :yelp-photo-id "9d710fa3-1505-43a4-8f41-ea8c188ee172", :categories ["Homestyle" "Churros"]}] + ["Haight Soul Food Hotel & Restaurant is a swell and swell place to have a drink in the spring." + {:small "http://cloudfront.net/f188d9d9-211f-4b66-87bd-6271f05fbcc6/small.jpg", + :medium "http://cloudfront.net/f188d9d9-211f-4b66-87bd-6271f05fbcc6/med.jpg", + :large "http://cloudfront.net/f188d9d9-211f-4b66-87bd-6271f05fbcc6/large.jpg"} + {:name "Haight Soul Food Hotel & Restaurant", :categories ["Soul Food" "Hotel & Restaurant"], :phone "415-786-9541", :id "11a72eb3-9e96-4703-9a01-e8c2a9469046"} + {:service "facebook", :facebook-photo-id "81f9c252-5b76-45f1-baf9-a2d919ee7695", :url "http://facebook.com/photos/81f9c252-5b76-45f1-baf9-a2d919ee7695"}] + ["Marina Japanese Liquor Store is a historical and horrible place to drink a craft beer the second Saturday of the month." + {:small "http://cloudfront.net/4a27d895-0832-47c1-8e30-5b41c7f2d8aa/small.jpg", + :medium "http://cloudfront.net/4a27d895-0832-47c1-8e30-5b41c7f2d8aa/med.jpg", + :large "http://cloudfront.net/4a27d895-0832-47c1-8e30-5b41c7f2d8aa/large.jpg"} + {:name "Marina Japanese Liquor Store", :categories ["Japanese" "Liquor Store"], :phone "415-587-9819", :id "08fd0138-35dd-41b0-836d-1c652e95ffcd"} + {:service "flare", :username "kyle"}] + ["SF British Pop-Up Food Stand is a groovy and popular place to meet new friends weekend evenings." + {:small "http://cloudfront.net/b1397f37-124b-435d-ab72-ac9002e56f7b/small.jpg", + :medium "http://cloudfront.net/b1397f37-124b-435d-ab72-ac9002e56f7b/med.jpg", + :large "http://cloudfront.net/b1397f37-124b-435d-ab72-ac9002e56f7b/large.jpg"} + {:name "SF British Pop-Up Food Stand", :categories ["British" "Pop-Up Food Stand"], :phone "415-441-3725", :id "19eac087-7b1c-4668-a26c-d7c02cbcd3f6"} + {:service "facebook", :facebook-photo-id "adf58b74-9ac3-4c27-a4a7-b951736b79ae", :url "http://facebook.com/photos/adf58b74-9ac3-4c27-a4a7-b951736b79ae"}] + ["Sameer's GMO-Free Restaurant is a delicious and fantastic place to have breakfast in the spring." + {:small "http://cloudfront.net/e4251131-92d3-4097-82f5-782405eb0ae5/small.jpg", + :medium "http://cloudfront.net/e4251131-92d3-4097-82f5-782405eb0ae5/med.jpg", + :large "http://cloudfront.net/e4251131-92d3-4097-82f5-782405eb0ae5/large.jpg"} + {:name "Sameer's GMO-Free Restaurant", :categories ["GMO-Free" "Restaurant"], :phone "415-128-9430", :id "7ac8a7dd-c07f-45a6-92ba-bdb1b1280af2"} + {:service "twitter", :mentions ["@sameers_gmo_free_restaurant"], :tags ["#gmo-free" "#restaurant"], :username "cam_saul"}] + ["Rasta's Paleo Churros is a underappreciated and atmospheric place to catch a bite to eat when hungover." + {:small "http://cloudfront.net/1729a6bc-19f5-4084-b4fe-e91eafbe07cb/small.jpg", + :medium "http://cloudfront.net/1729a6bc-19f5-4084-b4fe-e91eafbe07cb/med.jpg", + :large "http://cloudfront.net/1729a6bc-19f5-4084-b4fe-e91eafbe07cb/large.jpg"} + {:name "Rasta's Paleo Churros", :categories ["Paleo" "Churros"], :phone "415-915-0309", :id "3bf48ec6-434b-43b1-be28-7644975ecaf9"} + {:service "flare", :username "tupac"}] + ["SoMa Old-Fashioned Pizzeria is a underground and fantastic place to drink a craft beer when hungover." + {:small "http://cloudfront.net/7905ee6d-f63b-4f36-bc51-c8006b4c39f4/small.jpg", + :medium "http://cloudfront.net/7905ee6d-f63b-4f36-bc51-c8006b4c39f4/med.jpg", + :large "http://cloudfront.net/7905ee6d-f63b-4f36-bc51-c8006b4c39f4/large.jpg"} + {:name "SoMa Old-Fashioned Pizzeria", :categories ["Old-Fashioned" "Pizzeria"], :phone "415-966-8856", :id "deb8997b-734d-402b-a181-bd888214bc86"} + {:service "facebook", :facebook-photo-id "98c776ab-0c90-4fa4-a4c6-6f0e19ad6b9e", :url "http://facebook.com/photos/98c776ab-0c90-4fa4-a4c6-6f0e19ad6b9e"}] + ["SoMa Old-Fashioned Pizzeria is a exclusive and underappreciated place to pitch an investor the first Sunday of the month." + {:small "http://cloudfront.net/f46c53c3-8987-4dee-88ba-6693f884f53a/small.jpg", + :medium "http://cloudfront.net/f46c53c3-8987-4dee-88ba-6693f884f53a/med.jpg", + :large "http://cloudfront.net/f46c53c3-8987-4dee-88ba-6693f884f53a/large.jpg"} + {:name "SoMa Old-Fashioned Pizzeria", :categories ["Old-Fashioned" "Pizzeria"], :phone "415-966-8856", :id "deb8997b-734d-402b-a181-bd888214bc86"} + {:service "twitter", :mentions ["@soma_old_fashioned_pizzeria"], :tags ["#old-fashioned" "#pizzeria"], :username "bob"}] + ["Oakland American Grill is a groovy and modern place to sip a glass of expensive wine weekend mornings." + {:small "http://cloudfront.net/700e2004-8a15-4cd6-9341-413c09249098/small.jpg", + :medium "http://cloudfront.net/700e2004-8a15-4cd6-9341-413c09249098/med.jpg", + :large "http://cloudfront.net/700e2004-8a15-4cd6-9341-413c09249098/large.jpg"} + {:name "Oakland American Grill", :categories ["American" "Grill"], :phone "415-660-0889", :id "856f907d-b669-4b9c-8337-bf9c88883746"} + {:service "twitter", :mentions ["@oakland_american_grill"], :tags ["#american" "#grill"], :username "cam_saul"}] + ["Polk St. Deep-Dish Hotel & Restaurant is a atmospheric and horrible place to sip a glass of expensive wine weekday afternoons." + {:small "http://cloudfront.net/d60aea9e-0e15-4bf5-90a8-68fcbb7f1f06/small.jpg", + :medium "http://cloudfront.net/d60aea9e-0e15-4bf5-90a8-68fcbb7f1f06/med.jpg", + :large "http://cloudfront.net/d60aea9e-0e15-4bf5-90a8-68fcbb7f1f06/large.jpg"} + {:name "Polk St. Deep-Dish Hotel & Restaurant", :categories ["Deep-Dish" "Hotel & Restaurant"], :phone "415-666-8681", :id "47f1698e-ae11-46f5-818b-85a59d0affba"} + {:service "twitter", :mentions ["@polk_st._deep_dish_hotel_&_restaurant"], :tags ["#deep-dish" "#hotel" "#&" "#restaurant"], :username "mandy"}] + ["Haight Chinese Gastro Pub is a world-famous and amazing place to sip Champagne when hungover." + {:small "http://cloudfront.net/cae5889b-70e1-4f7e-b097-709ae2db369e/small.jpg", + :medium "http://cloudfront.net/cae5889b-70e1-4f7e-b097-709ae2db369e/med.jpg", + :large "http://cloudfront.net/cae5889b-70e1-4f7e-b097-709ae2db369e/large.jpg"} + {:name "Haight Chinese Gastro Pub", :categories ["Chinese" "Gastro Pub"], :phone "415-521-5825", :id "12a8dc6e-1b2c-47e2-9c18-3ae220e4806f"} + {:service "twitter", :mentions ["@haight_chinese_gastro_pub"], :tags ["#chinese" "#gastro" "#pub"], :username "cam_saul"}] + ["Lucky's Gluten-Free Gastro Pub is a popular and overrated place to watch the Giants game the second Saturday of the month." + {:small "http://cloudfront.net/1f98a61a-b644-4adf-bb2a-24207ed563f8/small.jpg", + :medium "http://cloudfront.net/1f98a61a-b644-4adf-bb2a-24207ed563f8/med.jpg", + :large "http://cloudfront.net/1f98a61a-b644-4adf-bb2a-24207ed563f8/large.jpg"} + {:name "Lucky's Gluten-Free Gastro Pub", :categories ["Gluten-Free" "Gastro Pub"], :phone "415-391-6443", :id "7ccf8bbb-74b4-48f7-aa7c-43872f63cb1b"} + {:service "foursquare", :foursquare-photo-id "9543ebb3-8d33-468f-8ba1-980fe43aa09b", :mayor "amy"}] + ["Lucky's Deep-Dish Gastro Pub is a great and family-friendly place to meet new friends on Saturday night." + {:small "http://cloudfront.net/58db9f8b-70b5-4f9e-9b3e-9eab40907b1e/small.jpg", + :medium "http://cloudfront.net/58db9f8b-70b5-4f9e-9b3e-9eab40907b1e/med.jpg", + :large "http://cloudfront.net/58db9f8b-70b5-4f9e-9b3e-9eab40907b1e/large.jpg"} + {:name "Lucky's Deep-Dish Gastro Pub", :categories ["Deep-Dish" "Gastro Pub"], :phone "415-487-4085", :id "0136c454-0968-41cd-a237-ceec5724cab8"} + {:service "twitter", :mentions ["@luckys_deep_dish_gastro_pub"], :tags ["#deep-dish" "#gastro" "#pub"], :username "biggie"}] + ["Alcatraz Pizza Churros is a delicious and classic place to have breakfast on Taco Tuesday." + {:small "http://cloudfront.net/d3e4995f-57ad-4b71-8321-6d71082cb24c/small.jpg", + :medium "http://cloudfront.net/d3e4995f-57ad-4b71-8321-6d71082cb24c/med.jpg", + :large "http://cloudfront.net/d3e4995f-57ad-4b71-8321-6d71082cb24c/large.jpg"} + {:name "Alcatraz Pizza Churros", :categories ["Pizza" "Churros"], :phone "415-754-7867", :id "df95e4f1-8719-42af-a15d-3ee00de6e04f"} + {:service "flare", :username "mandy"}] + ["SoMa Old-Fashioned Pizzeria is a popular and amazing place to take visiting friends and relatives during summer." + {:small "http://cloudfront.net/d516bb79-8683-4e5a-a2e0-937144346293/small.jpg", + :medium "http://cloudfront.net/d516bb79-8683-4e5a-a2e0-937144346293/med.jpg", + :large "http://cloudfront.net/d516bb79-8683-4e5a-a2e0-937144346293/large.jpg"} + {:name "SoMa Old-Fashioned Pizzeria", :categories ["Old-Fashioned" "Pizzeria"], :phone "415-966-8856", :id "deb8997b-734d-402b-a181-bd888214bc86"} + {:service "flare", :username "jane"}] + ["SoMa Old-Fashioned Pizzeria is a underappreciated and underappreciated place to people-watch weekend evenings." + {:small "http://cloudfront.net/0c331686-89ff-451e-8283-d08742746c3b/small.jpg", + :medium "http://cloudfront.net/0c331686-89ff-451e-8283-d08742746c3b/med.jpg", + :large "http://cloudfront.net/0c331686-89ff-451e-8283-d08742746c3b/large.jpg"} + {:name "SoMa Old-Fashioned Pizzeria", :categories ["Old-Fashioned" "Pizzeria"], :phone "415-966-8856", :id "deb8997b-734d-402b-a181-bd888214bc86"} + {:service "yelp", :yelp-photo-id "cf77c28d-03a5-44d4-9882-4fe4bef60a02", :categories ["Old-Fashioned" "Pizzeria"]}] + ["Tenderloin Cage-Free Sushi is a overrated and historical place to catch a bite to eat the first Sunday of the month." + {:small "http://cloudfront.net/1b419482-e80c-4783-8b7f-d21b04e35a4b/small.jpg", + :medium "http://cloudfront.net/1b419482-e80c-4783-8b7f-d21b04e35a4b/med.jpg", + :large "http://cloudfront.net/1b419482-e80c-4783-8b7f-d21b04e35a4b/large.jpg"} + {:name "Tenderloin Cage-Free Sushi", :categories ["Cage-Free" "Sushi"], :phone "415-348-0644", :id "0b6c036f-82b0-4008-bdfe-5360dd93fb75"} + {:service "twitter", :mentions ["@tenderloin_cage_free_sushi"], :tags ["#cage-free" "#sushi"], :username "mandy"}] + ["Polk St. Red White & Blue Café is a historical and swell place to have a birthday party in the fall." + {:small "http://cloudfront.net/8b0d7fc8-2b5c-4bfb-a947-3fd4753b118c/small.jpg", + :medium "http://cloudfront.net/8b0d7fc8-2b5c-4bfb-a947-3fd4753b118c/med.jpg", + :large "http://cloudfront.net/8b0d7fc8-2b5c-4bfb-a947-3fd4753b118c/large.jpg"} + {:name "Polk St. Red White & Blue Café", :categories ["Red White & Blue" "Café"], :phone "415-986-0661", :id "6eb9d2db-5015-49f0-bd18-f4c4938b7e5a"} + {:service "yelp", :yelp-photo-id "d84760ec-d55c-486d-9b53-e43a20e0395c", :categories ["Red White & Blue" "Café"]}] + ["Market St. Homestyle Pop-Up Food Stand is a classic and underground place to take a date during winter." + {:small "http://cloudfront.net/96e29fb5-834c-437c-9747-b2f33a3f096c/small.jpg", + :medium "http://cloudfront.net/96e29fb5-834c-437c-9747-b2f33a3f096c/med.jpg", + :large "http://cloudfront.net/96e29fb5-834c-437c-9747-b2f33a3f096c/large.jpg"} + {:name "Market St. Homestyle Pop-Up Food Stand", :categories ["Homestyle" "Pop-Up Food Stand"], :phone "415-213-3030", :id "2d873280-e43d-449e-9940-af96ae7df718"} + {:service "foursquare", :foursquare-photo-id "64feb4b2-a403-4be7-aa96-5c5e5595eba5", :mayor "joe"}] + ["Marina Low-Carb Food Truck is a classic and groovy place to nurse a hangover weekend evenings." + {:small "http://cloudfront.net/08213fe2-157e-46f7-a71d-82588347d023/small.jpg", + :medium "http://cloudfront.net/08213fe2-157e-46f7-a71d-82588347d023/med.jpg", + :large "http://cloudfront.net/08213fe2-157e-46f7-a71d-82588347d023/large.jpg"} + {:name "Marina Low-Carb Food Truck", :categories ["Low-Carb" "Food Truck"], :phone "415-748-3513", :id "a13a5beb-19de-40ca-a334-02df3bdf5285"} + {:service "twitter", :mentions ["@marina_low_carb_food_truck"], :tags ["#low-carb" "#food" "#truck"], :username "cam_saul"}] + ["Marina Modern Bar & Grill is a modern and well-decorated place to watch the Giants game on public holidays." + {:small "http://cloudfront.net/00be0fe9-6765-4c76-98ee-f7e2e7e0b7b9/small.jpg", + :medium "http://cloudfront.net/00be0fe9-6765-4c76-98ee-f7e2e7e0b7b9/med.jpg", + :large "http://cloudfront.net/00be0fe9-6765-4c76-98ee-f7e2e7e0b7b9/large.jpg"} + {:name "Marina Modern Bar & Grill", :categories ["Modern" "Bar & Grill"], :phone "415-203-8530", :id "806144f1-bb7a-4271-8fcb-fc6550f51676"} + {:service "foursquare", :foursquare-photo-id "b53c2c6c-b767-4816-b881-52613ecb438d", :mayor "rasta_toucan"}] + ["Alcatraz Cage-Free Restaurant is a swell and underappreciated place to have a after-work cocktail in the fall." + {:small "http://cloudfront.net/3e6adc7d-41cc-426d-8221-e1d75e60c6c9/small.jpg", + :medium "http://cloudfront.net/3e6adc7d-41cc-426d-8221-e1d75e60c6c9/med.jpg", + :large "http://cloudfront.net/3e6adc7d-41cc-426d-8221-e1d75e60c6c9/large.jpg"} + {:name "Alcatraz Cage-Free Restaurant", :categories ["Cage-Free" "Restaurant"], :phone "415-568-0312", :id "fe0c7f8e-4937-4a76-bda4-44ad89c5231c"} + {:service "foursquare", :foursquare-photo-id "a40cb559-779a-45cb-82d4-82361f7ac9c1", :mayor "jane"}] + ["Kyle's Low-Carb Grill is a exclusive and fantastic place to have a birthday party on Saturday night." + {:small "http://cloudfront.net/ec5fd5d5-4e24-4a8e-99a8-1c06c9b6ad83/small.jpg", + :medium "http://cloudfront.net/ec5fd5d5-4e24-4a8e-99a8-1c06c9b6ad83/med.jpg", + :large "http://cloudfront.net/ec5fd5d5-4e24-4a8e-99a8-1c06c9b6ad83/large.jpg"} + {:name "Kyle's Low-Carb Grill", :categories ["Low-Carb" "Grill"], :phone "415-992-8278", :id "b27f50c6-55eb-48b0-9fee-17a6ef5243bd"} + {:service "flare", :username "amy"}] + ["Marina Modern Sushi is a exclusive and fantastic place to conduct a business meeting in the fall." + {:small "http://cloudfront.net/a44ff874-27fd-480a-b723-79516d9a0f5a/small.jpg", + :medium "http://cloudfront.net/a44ff874-27fd-480a-b723-79516d9a0f5a/med.jpg", + :large "http://cloudfront.net/a44ff874-27fd-480a-b723-79516d9a0f5a/large.jpg"} + {:name "Marina Modern Sushi", :categories ["Modern" "Sushi"], :phone "415-393-7672", :id "21807c63-ca4c-4468-9844-d0c2620fbdfc"} + {:service "foursquare", :foursquare-photo-id "4f9300e3-8787-4429-9837-52a8949a920e", :mayor "bob"}] + ["Haight Soul Food Hotel & Restaurant is a world-famous and popular place to nurse a hangover Friday nights." + {:small "http://cloudfront.net/c9d748f5-245c-4a0f-9308-76e56ae5666c/small.jpg", + :medium "http://cloudfront.net/c9d748f5-245c-4a0f-9308-76e56ae5666c/med.jpg", + :large "http://cloudfront.net/c9d748f5-245c-4a0f-9308-76e56ae5666c/large.jpg"} + {:name "Haight Soul Food Hotel & Restaurant", :categories ["Soul Food" "Hotel & Restaurant"], :phone "415-786-9541", :id "11a72eb3-9e96-4703-9a01-e8c2a9469046"} + {:service "flare", :username "kyle"}] + ["Sunset American Churros is a decent and fantastic place to have a drink weekend mornings." + {:small "http://cloudfront.net/7a5085d9-5fe6-49c1-9556-9f196f0938ae/small.jpg", + :medium "http://cloudfront.net/7a5085d9-5fe6-49c1-9556-9f196f0938ae/med.jpg", + :large "http://cloudfront.net/7a5085d9-5fe6-49c1-9556-9f196f0938ae/large.jpg"} + {:name "Sunset American Churros", :categories ["American" "Churros"], :phone "415-191-5018", :id "2e88c921-29fb-489b-a956-d3ba1182da73"} + {:service "facebook", :facebook-photo-id "0bbffb73-91f1-4a27-97df-5e0dbae6532b", :url "http://facebook.com/photos/0bbffb73-91f1-4a27-97df-5e0dbae6532b"}] + ["Cam's Mexican Gastro Pub is a family-friendly and acceptable place to people-watch weekend evenings." + {:small "http://cloudfront.net/a6bddb27-880a-46da-b824-4449b2389a73/small.jpg", + :medium "http://cloudfront.net/a6bddb27-880a-46da-b824-4449b2389a73/med.jpg", + :large "http://cloudfront.net/a6bddb27-880a-46da-b824-4449b2389a73/large.jpg"} + {:name "Cam's Mexican Gastro Pub", :categories ["Mexican" "Gastro Pub"], :phone "415-320-9123", :id "bb958ac5-758e-4f42-b984-6b0e13f25194"} + {:service "yelp", :yelp-photo-id "fd35ac16-71ef-48f8-8512-2078bda1db30", :categories ["Mexican" "Gastro Pub"]}] + ["Nob Hill Gluten-Free Coffee House is a fantastic and historical place to have a after-work cocktail with your pet toucan." + {:small "http://cloudfront.net/c3c254d4-4a06-472a-8373-c96d1cf13ca1/small.jpg", + :medium "http://cloudfront.net/c3c254d4-4a06-472a-8373-c96d1cf13ca1/med.jpg", + :large "http://cloudfront.net/c3c254d4-4a06-472a-8373-c96d1cf13ca1/large.jpg"} + {:name "Nob Hill Gluten-Free Coffee House", :categories ["Gluten-Free" "Coffee House"], :phone "415-605-9554", :id "df57ff6d-1b5f-46da-b292-32321c6b1a7e"} + {:service "facebook", :facebook-photo-id "f9afa193-7158-49d9-9f8c-42542c1e47dd", :url "http://facebook.com/photos/f9afa193-7158-49d9-9f8c-42542c1e47dd"}] + ["Rasta's British Food Truck is a fantastic and underappreciated place to sip a glass of expensive wine when hungover." + {:small "http://cloudfront.net/1f6bfab8-196f-41cb-9abb-55fcf5ce501b/small.jpg", + :medium "http://cloudfront.net/1f6bfab8-196f-41cb-9abb-55fcf5ce501b/med.jpg", + :large "http://cloudfront.net/1f6bfab8-196f-41cb-9abb-55fcf5ce501b/large.jpg"} + {:name "Rasta's British Food Truck", :categories ["British" "Food Truck"], :phone "415-958-9031", :id "b6616c97-01d0-488f-a855-bcd6efe2b899"} + {:service "yelp", :yelp-photo-id "4f557d5b-b69d-48d1-814a-d4cac246e72c", :categories ["British" "Food Truck"]}] + ["Pacific Heights Pizza Bakery is a delicious and amazing place to watch the Giants game weekday afternoons." + {:small "http://cloudfront.net/44645780-38d0-4de9-9d8e-468b9f237a5f/small.jpg", + :medium "http://cloudfront.net/44645780-38d0-4de9-9d8e-468b9f237a5f/med.jpg", + :large "http://cloudfront.net/44645780-38d0-4de9-9d8e-468b9f237a5f/large.jpg"} + {:name "Pacific Heights Pizza Bakery", :categories ["Pizza" "Bakery"], :phone "415-006-0149", :id "7fda37a5-810f-4902-b571-54afe583f0dd"} + {:service "facebook", :facebook-photo-id "0927d96d-e419-49a9-bd61-cb6bb7f46a33", :url "http://facebook.com/photos/0927d96d-e419-49a9-bd61-cb6bb7f46a33"}] + ["Marina No-MSG Sushi is a atmospheric and historical place to sip Champagne on public holidays." + {:small "http://cloudfront.net/679db911-40ba-4250-9b11-46cc3d1a135f/small.jpg", + :medium "http://cloudfront.net/679db911-40ba-4250-9b11-46cc3d1a135f/med.jpg", + :large "http://cloudfront.net/679db911-40ba-4250-9b11-46cc3d1a135f/large.jpg"} + {:name "Marina No-MSG Sushi", :categories ["No-MSG" "Sushi"], :phone "415-856-5937", :id "d51013a3-8547-4705-a5f0-cb11d8206481"} + {:service "facebook", :facebook-photo-id "61efe486-455b-4327-995f-235de7d75f5a", :url "http://facebook.com/photos/61efe486-455b-4327-995f-235de7d75f5a"}] + ["Marina Cage-Free Liquor Store is a well-decorated and delicious place to nurse a hangover on a Tuesday afternoon." + {:small "http://cloudfront.net/bbbe5406-2b10-4af1-a63d-6c98fe101d90/small.jpg", + :medium "http://cloudfront.net/bbbe5406-2b10-4af1-a63d-6c98fe101d90/med.jpg", + :large "http://cloudfront.net/bbbe5406-2b10-4af1-a63d-6c98fe101d90/large.jpg"} + {:name "Marina Cage-Free Liquor Store", :categories ["Cage-Free" "Liquor Store"], :phone "415-571-0783", :id "ad68f549-3000-407e-ab98-7f314cfa4653"} + {:service "facebook", :facebook-photo-id "f13a16b2-3177-4827-bc81-3abccba5506b", :url "http://facebook.com/photos/f13a16b2-3177-4827-bc81-3abccba5506b"}] + ["Lower Pac Heights Deep-Dish Liquor Store is a horrible and exclusive place to have brunch weekend mornings." + {:small "http://cloudfront.net/30f58fc0-8e79-4c25-bffa-5cf022b984a9/small.jpg", + :medium "http://cloudfront.net/30f58fc0-8e79-4c25-bffa-5cf022b984a9/med.jpg", + :large "http://cloudfront.net/30f58fc0-8e79-4c25-bffa-5cf022b984a9/large.jpg"} + {:name "Lower Pac Heights Deep-Dish Liquor Store", :categories ["Deep-Dish" "Liquor Store"], :phone "415-497-3039", :id "4d4eabfc-ff1f-4bc6-88b0-2f55489ff666"} + {:service "facebook", :facebook-photo-id "2b761a56-e66d-4cad-9117-ef6b0b633720", :url "http://facebook.com/photos/2b761a56-e66d-4cad-9117-ef6b0b633720"}] + ["Mission British Café is a world-famous and groovy place to pitch an investor on public holidays." + {:small "http://cloudfront.net/a75b8799-e41c-4a50-b19c-b3cb21ef8ae6/small.jpg", + :medium "http://cloudfront.net/a75b8799-e41c-4a50-b19c-b3cb21ef8ae6/med.jpg", + :large "http://cloudfront.net/a75b8799-e41c-4a50-b19c-b3cb21ef8ae6/large.jpg"} + {:name "Mission British Café", :categories ["British" "Café"], :phone "415-715-7004", :id "c99899e3-439c-4444-9dc4-5598632aec8d"} + {:service "facebook", :facebook-photo-id "9b584eeb-9715-4874-8e56-8a69838e6ddf", :url "http://facebook.com/photos/9b584eeb-9715-4874-8e56-8a69838e6ddf"}] + ["Oakland American Grill is a fantastic and well-decorated place to have a birthday party weekday afternoons." + {:small "http://cloudfront.net/e255e8ea-3015-46bd-94db-198385ae5f7c/small.jpg", + :medium "http://cloudfront.net/e255e8ea-3015-46bd-94db-198385ae5f7c/med.jpg", + :large "http://cloudfront.net/e255e8ea-3015-46bd-94db-198385ae5f7c/large.jpg"} + {:name "Oakland American Grill", :categories ["American" "Grill"], :phone "415-660-0889", :id "856f907d-b669-4b9c-8337-bf9c88883746"} + {:service "flare", :username "sameer"}] + ["Kyle's Low-Carb Grill is a overrated and fantastic place to have breakfast with your pet dog." + {:small "http://cloudfront.net/629404a1-ed36-4288-95e0-e57f6142990f/small.jpg", + :medium "http://cloudfront.net/629404a1-ed36-4288-95e0-e57f6142990f/med.jpg", + :large "http://cloudfront.net/629404a1-ed36-4288-95e0-e57f6142990f/large.jpg"} + {:name "Kyle's Low-Carb Grill", :categories ["Low-Carb" "Grill"], :phone "415-992-8278", :id "b27f50c6-55eb-48b0-9fee-17a6ef5243bd"} + {:service "foursquare", :foursquare-photo-id "35681dab-4f25-420d-9a65-915d233817f0", :mayor "sameer"}] + ["Nob Hill Gluten-Free Coffee House is a underappreciated and family-friendly place to take a date after baseball games." + {:small "http://cloudfront.net/5d8da40a-6415-4dcc-8f51-f8e3ef99f332/small.jpg", + :medium "http://cloudfront.net/5d8da40a-6415-4dcc-8f51-f8e3ef99f332/med.jpg", + :large "http://cloudfront.net/5d8da40a-6415-4dcc-8f51-f8e3ef99f332/large.jpg"} + {:name "Nob Hill Gluten-Free Coffee House", :categories ["Gluten-Free" "Coffee House"], :phone "415-605-9554", :id "df57ff6d-1b5f-46da-b292-32321c6b1a7e"} + {:service "foursquare", :foursquare-photo-id "1714302b-8629-4b18-ab25-2368ad1e4568", :mayor "biggie"}] + ["Alcatraz Cage-Free Restaurant is a underappreciated and groovy place to have a drink when hungover." + {:small "http://cloudfront.net/92e9ca72-bdf1-4e8a-8f21-709644a8a848/small.jpg", + :medium "http://cloudfront.net/92e9ca72-bdf1-4e8a-8f21-709644a8a848/med.jpg", + :large "http://cloudfront.net/92e9ca72-bdf1-4e8a-8f21-709644a8a848/large.jpg"} + {:name "Alcatraz Cage-Free Restaurant", :categories ["Cage-Free" "Restaurant"], :phone "415-568-0312", :id "fe0c7f8e-4937-4a76-bda4-44ad89c5231c"} + {:service "foursquare", :foursquare-photo-id "c1e232ec-e10f-4ad6-b932-84cd854ee3c2", :mayor "amy"}] + ["Kyle's Low-Carb Grill is a decent and well-decorated place to sip a glass of expensive wine in July." + {:small "http://cloudfront.net/2e81de9d-a366-4756-ab0f-c88b1eeb15cc/small.jpg", + :medium "http://cloudfront.net/2e81de9d-a366-4756-ab0f-c88b1eeb15cc/med.jpg", + :large "http://cloudfront.net/2e81de9d-a366-4756-ab0f-c88b1eeb15cc/large.jpg"} + {:name "Kyle's Low-Carb Grill", :categories ["Low-Carb" "Grill"], :phone "415-992-8278", :id "b27f50c6-55eb-48b0-9fee-17a6ef5243bd"} + {:service "yelp", :yelp-photo-id "23e11b87-eb0d-48a8-9eb4-0e7b0453f4d7", :categories ["Low-Carb" "Grill"]}] + ["Lower Pac Heights Deep-Dish Liquor Store is a modern and well-decorated place to pitch an investor after baseball games." + {:small "http://cloudfront.net/28f821ba-fc0c-43d5-90aa-014997910ff4/small.jpg", + :medium "http://cloudfront.net/28f821ba-fc0c-43d5-90aa-014997910ff4/med.jpg", + :large "http://cloudfront.net/28f821ba-fc0c-43d5-90aa-014997910ff4/large.jpg"} + {:name "Lower Pac Heights Deep-Dish Liquor Store", :categories ["Deep-Dish" "Liquor Store"], :phone "415-497-3039", :id "4d4eabfc-ff1f-4bc6-88b0-2f55489ff666"} + {:service "twitter", :mentions ["@lower_pac_heights_deep_dish_liquor_store"], :tags ["#deep-dish" "#liquor" "#store"], :username "sameer"}] + ["Kyle's Chinese Restaurant is a decent and world-famous place to drink a craft beer in the fall." + {:small "http://cloudfront.net/0a8ca876-81d1-43de-928c-6dfaaa99a4d9/small.jpg", + :medium "http://cloudfront.net/0a8ca876-81d1-43de-928c-6dfaaa99a4d9/med.jpg", + :large "http://cloudfront.net/0a8ca876-81d1-43de-928c-6dfaaa99a4d9/large.jpg"} + {:name "Kyle's Chinese Restaurant", :categories ["Chinese" "Restaurant"], :phone "415-298-9499", :id "de08b3c7-9929-40d8-8c20-dd9317613c17"} + {:service "twitter", :mentions ["@kyles_chinese_restaurant"], :tags ["#chinese" "#restaurant"], :username "tupac"}] + ["Lucky's Japanese Bar & Grill is a atmospheric and decent place to watch the Warriors game in the spring." + {:small "http://cloudfront.net/81d4c210-2aad-44be-a5a5-554d0c68a1b3/small.jpg", + :medium "http://cloudfront.net/81d4c210-2aad-44be-a5a5-554d0c68a1b3/med.jpg", + :large "http://cloudfront.net/81d4c210-2aad-44be-a5a5-554d0c68a1b3/large.jpg"} + {:name "Lucky's Japanese Bar & Grill", :categories ["Japanese" "Bar & Grill"], :phone "415-816-1300", :id "602d574a-6fd3-44df-9bac-e71ce1ab5eb4"} + {:service "facebook", :facebook-photo-id "f132a200-55f9-4ae8-b61e-4e932287c502", :url "http://facebook.com/photos/f132a200-55f9-4ae8-b61e-4e932287c502"}] + ["Marina Japanese Liquor Store is a fantastic and modern place to have a birthday party on a Tuesday afternoon." + {:small "http://cloudfront.net/e7db8648-76ac-4f59-aa43-f8e980e929fe/small.jpg", + :medium "http://cloudfront.net/e7db8648-76ac-4f59-aa43-f8e980e929fe/med.jpg", + :large "http://cloudfront.net/e7db8648-76ac-4f59-aa43-f8e980e929fe/large.jpg"} + {:name "Marina Japanese Liquor Store", :categories ["Japanese" "Liquor Store"], :phone "415-587-9819", :id "08fd0138-35dd-41b0-836d-1c652e95ffcd"} + {:service "facebook", :facebook-photo-id "3f37d940-c75b-41f2-b13f-e5b0828843af", :url "http://facebook.com/photos/3f37d940-c75b-41f2-b13f-e5b0828843af"}] + ["Kyle's Chinese Restaurant is a great and delicious place to conduct a business meeting on public holidays." + {:small "http://cloudfront.net/1620fe7b-8e84-46d4-8807-fa47f52be5bb/small.jpg", + :medium "http://cloudfront.net/1620fe7b-8e84-46d4-8807-fa47f52be5bb/med.jpg", + :large "http://cloudfront.net/1620fe7b-8e84-46d4-8807-fa47f52be5bb/large.jpg"} + {:name "Kyle's Chinese Restaurant", :categories ["Chinese" "Restaurant"], :phone "415-298-9499", :id "de08b3c7-9929-40d8-8c20-dd9317613c17"} + {:service "facebook", :facebook-photo-id "27189121-e22e-4dd6-85da-8880be9c513c", :url "http://facebook.com/photos/27189121-e22e-4dd6-85da-8880be9c513c"}] + ["Haight Soul Food Hotel & Restaurant is a world-famous and atmospheric place to sip a glass of expensive wine Friday nights." + {:small "http://cloudfront.net/8b7f40e0-1ab0-4f11-8313-04fb6dc3c1a9/small.jpg", + :medium "http://cloudfront.net/8b7f40e0-1ab0-4f11-8313-04fb6dc3c1a9/med.jpg", + :large "http://cloudfront.net/8b7f40e0-1ab0-4f11-8313-04fb6dc3c1a9/large.jpg"} + {:name "Haight Soul Food Hotel & Restaurant", :categories ["Soul Food" "Hotel & Restaurant"], :phone "415-786-9541", :id "11a72eb3-9e96-4703-9a01-e8c2a9469046"} + {:service "twitter", :mentions ["@haight_soul_food_hotel_&_restaurant"], :tags ["#soul" "#food" "#hotel" "#&" "#restaurant"], :username "cam_saul"}] + ["Haight Soul Food Café is a wonderful and underground place to watch the Warriors game on a Tuesday afternoon." + {:small "http://cloudfront.net/68e3313a-d8cf-4de6-9d85-393b5e881259/small.jpg", + :medium "http://cloudfront.net/68e3313a-d8cf-4de6-9d85-393b5e881259/med.jpg", + :large "http://cloudfront.net/68e3313a-d8cf-4de6-9d85-393b5e881259/large.jpg"} + {:name "Haight Soul Food Café", :categories ["Soul Food" "Café"], :phone "415-257-1769", :id "a1796c4b-da2b-474f-9fd6-4fa96c1eac70"} + {:service "foursquare", :foursquare-photo-id "3bbd4357-aba9-4a8e-837c-c57e42dee4e3", :mayor "biggie"}] + ["Sunset Homestyle Grill is a atmospheric and great place to have brunch weekend mornings." + {:small "http://cloudfront.net/14743cfe-f2e7-4532-bd72-3ef14a277fa6/small.jpg", + :medium "http://cloudfront.net/14743cfe-f2e7-4532-bd72-3ef14a277fa6/med.jpg", + :large "http://cloudfront.net/14743cfe-f2e7-4532-bd72-3ef14a277fa6/large.jpg"} + {:name "Sunset Homestyle Grill", :categories ["Homestyle" "Grill"], :phone "415-356-7052", :id "c57673cd-f2d0-4bbc-aed0-6c166d7cf2c3"} + {:service "facebook", :facebook-photo-id "39e3b7a2-6fcf-4e90-b038-fafcc1e528f6", :url "http://facebook.com/photos/39e3b7a2-6fcf-4e90-b038-fafcc1e528f6"}] + ["Sameer's GMO-Free Pop-Up Food Stand is a wonderful and fantastic place to have breakfast the first Sunday of the month." + {:small "http://cloudfront.net/a9e803dd-7cdf-47ac-8314-3f12eee7fdfc/small.jpg", + :medium "http://cloudfront.net/a9e803dd-7cdf-47ac-8314-3f12eee7fdfc/med.jpg", + :large "http://cloudfront.net/a9e803dd-7cdf-47ac-8314-3f12eee7fdfc/large.jpg"} + {:name "Sameer's GMO-Free Pop-Up Food Stand", :categories ["GMO-Free" "Pop-Up Food Stand"], :phone "415-217-7891", :id "a829efc7-7e03-4e73-b072-83d10d1e3953"} + {:service "yelp", :yelp-photo-id "5bfe141b-4680-45de-be48-4eb8cdb8b791", :categories ["GMO-Free" "Pop-Up Food Stand"]}] + ["Cam's Old-Fashioned Coffee House is a modern and family-friendly place to take visiting friends and relatives Friday nights." + {:small "http://cloudfront.net/490f4943-6aaa-422c-8a7a-5996d806b67f/small.jpg", + :medium "http://cloudfront.net/490f4943-6aaa-422c-8a7a-5996d806b67f/med.jpg", + :large "http://cloudfront.net/490f4943-6aaa-422c-8a7a-5996d806b67f/large.jpg"} + {:name "Cam's Old-Fashioned Coffee House", :categories ["Old-Fashioned" "Coffee House"], :phone "415-871-9473", :id "5d1f918e-ef00-40de-b6e4-3f9f34ee8cd4"} + {:service "flare", :username "tupac"}] + ["Tenderloin Cage-Free Sushi is a overrated and overrated place to watch the Warriors game weekend mornings." + {:small "http://cloudfront.net/6cf1e5bd-3fd0-4ee4-a35a-fbc60cc2d8be/small.jpg", + :medium "http://cloudfront.net/6cf1e5bd-3fd0-4ee4-a35a-fbc60cc2d8be/med.jpg", + :large "http://cloudfront.net/6cf1e5bd-3fd0-4ee4-a35a-fbc60cc2d8be/large.jpg"} + {:name "Tenderloin Cage-Free Sushi", :categories ["Cage-Free" "Sushi"], :phone "415-348-0644", :id "0b6c036f-82b0-4008-bdfe-5360dd93fb75"} + {:service "flare", :username "mandy"}] + ["Tenderloin Red White & Blue Pizzeria is a atmospheric and wonderful place to conduct a business meeting Friday nights." + {:small "http://cloudfront.net/2b9bbbcb-b715-4429-b139-2855600d721e/small.jpg", + :medium "http://cloudfront.net/2b9bbbcb-b715-4429-b139-2855600d721e/med.jpg", + :large "http://cloudfront.net/2b9bbbcb-b715-4429-b139-2855600d721e/large.jpg"} + {:name "Tenderloin Red White & Blue Pizzeria", :categories ["Red White & Blue" "Pizzeria"], :phone "415-719-8143", :id "eba3dbcd-100a-4f38-a701-e0dec157f437"} + {:service "facebook", :facebook-photo-id "331f4c85-0b7b-4106-94be-df6628e6fb09", :url "http://facebook.com/photos/331f4c85-0b7b-4106-94be-df6628e6fb09"}] + ["Haight Soul Food Pop-Up Food Stand is a overrated and amazing place to catch a bite to eat weekday afternoons." + {:small "http://cloudfront.net/da6fff65-7f9d-403a-b905-d21cd0306f69/small.jpg", + :medium "http://cloudfront.net/da6fff65-7f9d-403a-b905-d21cd0306f69/med.jpg", + :large "http://cloudfront.net/da6fff65-7f9d-403a-b905-d21cd0306f69/large.jpg"} + {:name "Haight Soul Food Pop-Up Food Stand", :categories ["Soul Food" "Pop-Up Food Stand"], :phone "415-741-8726", :id "9735184b-1299-410f-a98e-10d9c548af42"} + {:service "foursquare", :foursquare-photo-id "3dcc3e91-1555-476a-9b3b-7834c2f8f1ba", :mayor "biggie"}] + ["Lucky's Old-Fashioned Eatery is a delicious and family-friendly place to drink a craft beer the first Sunday of the month." + {:small "http://cloudfront.net/4437ee2a-9520-4357-a9d5-c321055a249f/small.jpg", + :medium "http://cloudfront.net/4437ee2a-9520-4357-a9d5-c321055a249f/med.jpg", + :large "http://cloudfront.net/4437ee2a-9520-4357-a9d5-c321055a249f/large.jpg"} + {:name "Lucky's Old-Fashioned Eatery", :categories ["Old-Fashioned" "Eatery"], :phone "415-362-2338", :id "71dc221c-6e82-4d06-8709-93293121b1da"} + {:service "twitter", :mentions ["@luckys_old_fashioned_eatery"], :tags ["#old-fashioned" "#eatery"], :username "lucky_pigeon"}] + ["Tenderloin Japanese Ice Cream Truck is a well-decorated and decent place to have breakfast with friends." + {:small "http://cloudfront.net/04f74c1f-c835-46f4-8c7f-823042f2a091/small.jpg", + :medium "http://cloudfront.net/04f74c1f-c835-46f4-8c7f-823042f2a091/med.jpg", + :large "http://cloudfront.net/04f74c1f-c835-46f4-8c7f-823042f2a091/large.jpg"} + {:name "Tenderloin Japanese Ice Cream Truck", :categories ["Japanese" "Ice Cream Truck"], :phone "415-856-0371", :id "5ce47baa-bbef-4bc7-adf6-57842913ea8a"} + {:service "twitter", :mentions ["@tenderloin_japanese_ice_cream_truck"], :tags ["#japanese" "#ice" "#cream" "#truck"], :username "amy"}] + ["Oakland American Grill is a wonderful and underappreciated place to watch the Giants game with friends." + {:small "http://cloudfront.net/11b1c0e3-005a-414a-bd5a-e05880d277d5/small.jpg", + :medium "http://cloudfront.net/11b1c0e3-005a-414a-bd5a-e05880d277d5/med.jpg", + :large "http://cloudfront.net/11b1c0e3-005a-414a-bd5a-e05880d277d5/large.jpg"} + {:name "Oakland American Grill", :categories ["American" "Grill"], :phone "415-660-0889", :id "856f907d-b669-4b9c-8337-bf9c88883746"} + {:service "facebook", :facebook-photo-id "b5874d46-0247-4515-bd96-e8cc562c256d", :url "http://facebook.com/photos/b5874d46-0247-4515-bd96-e8cc562c256d"}] + ["Tenderloin Paleo Hotel & Restaurant is a classic and decent place to sip Champagne the first Sunday of the month." + {:small "http://cloudfront.net/ed01632a-a03e-4f9a-8949-0b06c690abd5/small.jpg", + :medium "http://cloudfront.net/ed01632a-a03e-4f9a-8949-0b06c690abd5/med.jpg", + :large "http://cloudfront.net/ed01632a-a03e-4f9a-8949-0b06c690abd5/large.jpg"} + {:name "Tenderloin Paleo Hotel & Restaurant", :categories ["Paleo" "Hotel & Restaurant"], :phone "415-402-1652", :id "4dea27b4-6d89-4b86-80a8-5631e171da8d"} + {:service "facebook", :facebook-photo-id "dafa1931-d938-44cf-bd12-4321d5c22407", :url "http://facebook.com/photos/dafa1931-d938-44cf-bd12-4321d5c22407"}] + ["Rasta's Paleo Café is a well-decorated and exclusive place to sip a glass of expensive wine on a Tuesday afternoon." + {:small "http://cloudfront.net/42b88fc6-e651-4ca3-9d53-e750df273b71/small.jpg", + :medium "http://cloudfront.net/42b88fc6-e651-4ca3-9d53-e750df273b71/med.jpg", + :large "http://cloudfront.net/42b88fc6-e651-4ca3-9d53-e750df273b71/large.jpg"} + {:name "Rasta's Paleo Café", :categories ["Paleo" "Café"], :phone "415-392-6341", :id "4f9e69be-f06c-46a0-bb8b-f3ddd8218ca1"} + {:service "twitter", :mentions ["@rastas_paleo_café"], :tags ["#paleo" "#café"], :username "jessica"}] + ["Lower Pac Heights Deep-Dish Ice Cream Truck is a family-friendly and atmospheric place to nurse a hangover on Taco Tuesday." + {:small "http://cloudfront.net/6ff7a334-5c10-4be7-8cdf-320f10f12f6e/small.jpg", + :medium "http://cloudfront.net/6ff7a334-5c10-4be7-8cdf-320f10f12f6e/med.jpg", + :large "http://cloudfront.net/6ff7a334-5c10-4be7-8cdf-320f10f12f6e/large.jpg"} + {:name "Lower Pac Heights Deep-Dish Ice Cream Truck", :categories ["Deep-Dish" "Ice Cream Truck"], :phone "415-495-1414", :id "d5efa2f2-496d-41b2-8b85-3d002a22a2bc"} + {:service "foursquare", :foursquare-photo-id "1a06dc7a-c6a4-47e6-a93d-131e99c481da", :mayor "lucky_pigeon"}] + ["Joe's Homestyle Eatery is a popular and atmospheric place to conduct a business meeting on a Tuesday afternoon." + {:small "http://cloudfront.net/42fa4469-19ba-41a2-816f-eeedf65664e5/small.jpg", + :medium "http://cloudfront.net/42fa4469-19ba-41a2-816f-eeedf65664e5/med.jpg", + :large "http://cloudfront.net/42fa4469-19ba-41a2-816f-eeedf65664e5/large.jpg"} + {:name "Joe's Homestyle Eatery", :categories ["Homestyle" "Eatery"], :phone "415-950-1337", :id "5cc18489-dfaf-417b-900f-5d1d61b961e8"} + {:service "foursquare", :foursquare-photo-id "416a5da5-6d01-4a16-956d-dcb85ce88bd5", :mayor "joe"}] + ["Lucky's Low-Carb Coffee House is a exclusive and overrated place to pitch an investor on a Tuesday afternoon." + {:small "http://cloudfront.net/30379e31-47c0-4ba6-be2f-9a1cbd4baa63/small.jpg", + :medium "http://cloudfront.net/30379e31-47c0-4ba6-be2f-9a1cbd4baa63/med.jpg", + :large "http://cloudfront.net/30379e31-47c0-4ba6-be2f-9a1cbd4baa63/large.jpg"} + {:name "Lucky's Low-Carb Coffee House", :categories ["Low-Carb" "Coffee House"], :phone "415-145-7107", :id "81b0f944-f0ce-45e5-b84e-a924c441064a"} + {:service "foursquare", :foursquare-photo-id "3b06925f-553a-4117-9864-b3e35d81d9e0", :mayor "tupac"}] + ["Lower Pac Heights Deep-Dish Ice Cream Truck is a delicious and great place to have a drink on Saturday night." + {:small "http://cloudfront.net/9766eef8-6548-4a83-ab8d-ce023b2681c9/small.jpg", + :medium "http://cloudfront.net/9766eef8-6548-4a83-ab8d-ce023b2681c9/med.jpg", + :large "http://cloudfront.net/9766eef8-6548-4a83-ab8d-ce023b2681c9/large.jpg"} + {:name "Lower Pac Heights Deep-Dish Ice Cream Truck", :categories ["Deep-Dish" "Ice Cream Truck"], :phone "415-495-1414", :id "d5efa2f2-496d-41b2-8b85-3d002a22a2bc"} + {:service "foursquare", :foursquare-photo-id "06e30d03-9b56-4820-adb4-c0b7ddde578b", :mayor "jane"}] + ["Lower Pac Heights Cage-Free Coffee House is a horrible and swell place to people-watch the first Sunday of the month." + {:small "http://cloudfront.net/0bf95c7a-44aa-4a9d-8580-c3b8f79147d2/small.jpg", + :medium "http://cloudfront.net/0bf95c7a-44aa-4a9d-8580-c3b8f79147d2/med.jpg", + :large "http://cloudfront.net/0bf95c7a-44aa-4a9d-8580-c3b8f79147d2/large.jpg"} + {:name "Lower Pac Heights Cage-Free Coffee House", :categories ["Cage-Free" "Coffee House"], :phone "415-697-9309", :id "02b1f618-41a0-406b-96dd-1a017f630b81"} + {:service "twitter", :mentions ["@lower_pac_heights_cage_free_coffee_house"], :tags ["#cage-free" "#coffee" "#house"], :username "biggie"}] + ["Marina Cage-Free Liquor Store is a wonderful and acceptable place to watch the Warriors game during winter." + {:small "http://cloudfront.net/cb4a1c99-85d3-401c-b015-48839206ebfe/small.jpg", + :medium "http://cloudfront.net/cb4a1c99-85d3-401c-b015-48839206ebfe/med.jpg", + :large "http://cloudfront.net/cb4a1c99-85d3-401c-b015-48839206ebfe/large.jpg"} + {:name "Marina Cage-Free Liquor Store", :categories ["Cage-Free" "Liquor Store"], :phone "415-571-0783", :id "ad68f549-3000-407e-ab98-7f314cfa4653"} + {:service "facebook", :facebook-photo-id "16a05fd8-120e-4f96-9857-4f340611e5f9", :url "http://facebook.com/photos/16a05fd8-120e-4f96-9857-4f340611e5f9"}] + ["SoMa Japanese Churros is a fantastic and swell place to drink a craft beer in the spring." + {:small "http://cloudfront.net/20a5ff27-ccd3-4c02-8cf5-7bc11e03b47d/small.jpg", + :medium "http://cloudfront.net/20a5ff27-ccd3-4c02-8cf5-7bc11e03b47d/med.jpg", + :large "http://cloudfront.net/20a5ff27-ccd3-4c02-8cf5-7bc11e03b47d/large.jpg"} + {:name "SoMa Japanese Churros", :categories ["Japanese" "Churros"], :phone "415-404-1510", :id "373858b2-e634-45d0-973d-4d0fed8c438b"} + {:service "facebook", :facebook-photo-id "4022dab6-225b-4677-be1d-7c201233bdee", :url "http://facebook.com/photos/4022dab6-225b-4677-be1d-7c201233bdee"}] + ["Nob Hill Free-Range Ice Cream Truck is a groovy and fantastic place to have brunch on a Tuesday afternoon." + {:small "http://cloudfront.net/2e8ef910-73c2-45d1-8b15-fb2f4913d094/small.jpg", + :medium "http://cloudfront.net/2e8ef910-73c2-45d1-8b15-fb2f4913d094/med.jpg", + :large "http://cloudfront.net/2e8ef910-73c2-45d1-8b15-fb2f4913d094/large.jpg"} + {:name "Nob Hill Free-Range Ice Cream Truck", :categories ["Free-Range" "Ice Cream Truck"], :phone "415-787-4049", :id "08d1e93c-105f-4abf-a9ec-b2e3cd30747e"} + {:service "facebook", :facebook-photo-id "7e9a5a67-48ab-4a54-821c-1a6f59a3ea92", :url "http://facebook.com/photos/7e9a5a67-48ab-4a54-821c-1a6f59a3ea92"}] + ["SoMa Old-Fashioned Pizzeria is a horrible and horrible place to conduct a business meeting on Taco Tuesday." + {:small "http://cloudfront.net/8fd7a773-2103-4d2a-8337-f965ad7bb41e/small.jpg", + :medium "http://cloudfront.net/8fd7a773-2103-4d2a-8337-f965ad7bb41e/med.jpg", + :large "http://cloudfront.net/8fd7a773-2103-4d2a-8337-f965ad7bb41e/large.jpg"} + {:name "SoMa Old-Fashioned Pizzeria", :categories ["Old-Fashioned" "Pizzeria"], :phone "415-966-8856", :id "deb8997b-734d-402b-a181-bd888214bc86"} + {:service "facebook", :facebook-photo-id "a868cdab-32c0-4939-a024-463412457bca", :url "http://facebook.com/photos/a868cdab-32c0-4939-a024-463412457bca"}] + ["Haight Soul Food Pop-Up Food Stand is a fantastic and family-friendly place to take a date with your pet dog." + {:small "http://cloudfront.net/252aa589-8ab3-48d8-861a-bfefd422b257/small.jpg", + :medium "http://cloudfront.net/252aa589-8ab3-48d8-861a-bfefd422b257/med.jpg", + :large "http://cloudfront.net/252aa589-8ab3-48d8-861a-bfefd422b257/large.jpg"} + {:name "Haight Soul Food Pop-Up Food Stand", :categories ["Soul Food" "Pop-Up Food Stand"], :phone "415-741-8726", :id "9735184b-1299-410f-a98e-10d9c548af42"} + {:service "twitter", :mentions ["@haight_soul_food_pop_up_food_stand"], :tags ["#soul" "#food" "#pop-up" "#food" "#stand"], :username "amy"}] + ["Pacific Heights Free-Range Eatery is a atmospheric and modern place to nurse a hangover on Saturday night." + {:small "http://cloudfront.net/860991ab-b4ca-4a5b-93fb-7a6cd7a6d208/small.jpg", + :medium "http://cloudfront.net/860991ab-b4ca-4a5b-93fb-7a6cd7a6d208/med.jpg", + :large "http://cloudfront.net/860991ab-b4ca-4a5b-93fb-7a6cd7a6d208/large.jpg"} + {:name "Pacific Heights Free-Range Eatery", :categories ["Free-Range" "Eatery"], :phone "415-901-6541", :id "88b361c8-ce69-4b2e-b0f2-9deedd574af6"} + {:service "foursquare", :foursquare-photo-id "a3ad3c09-99fc-45e3-b786-0b293eaa525d", :mayor "jane"}] + ["Sameer's GMO-Free Restaurant is a underground and swell place to watch the Warriors game on Thursdays." + {:small "http://cloudfront.net/51846ade-98ab-4b6a-b783-2714d9c751d0/small.jpg", + :medium "http://cloudfront.net/51846ade-98ab-4b6a-b783-2714d9c751d0/med.jpg", + :large "http://cloudfront.net/51846ade-98ab-4b6a-b783-2714d9c751d0/large.jpg"} + {:name "Sameer's GMO-Free Restaurant", :categories ["GMO-Free" "Restaurant"], :phone "415-128-9430", :id "7ac8a7dd-c07f-45a6-92ba-bdb1b1280af2"} + {:service "facebook", :facebook-photo-id "f8d9a1ea-707f-4e4d-9a1b-fbc953f50361", :url "http://facebook.com/photos/f8d9a1ea-707f-4e4d-9a1b-fbc953f50361"}] + ["Rasta's European Taqueria is a acceptable and groovy place to have a birthday party Friday nights." + {:small "http://cloudfront.net/ae49f9bb-7498-44ea-bfaf-e9d4c2f7b7f3/small.jpg", + :medium "http://cloudfront.net/ae49f9bb-7498-44ea-bfaf-e9d4c2f7b7f3/med.jpg", + :large "http://cloudfront.net/ae49f9bb-7498-44ea-bfaf-e9d4c2f7b7f3/large.jpg"} + {:name "Rasta's European Taqueria", :categories ["European" "Taqueria"], :phone "415-631-1599", :id "cb472880-ee6e-46e3-bd58-22cf33109aba"} + {:service "facebook", :facebook-photo-id "50e226aa-7b65-450e-9248-040c24bf3577", :url "http://facebook.com/photos/50e226aa-7b65-450e-9248-040c24bf3577"}] + ["Cam's Old-Fashioned Coffee House is a groovy and classic place to take a date weekend evenings." + {:small "http://cloudfront.net/ba2b9659-02d5-4df5-849d-b24d342176e6/small.jpg", + :medium "http://cloudfront.net/ba2b9659-02d5-4df5-849d-b24d342176e6/med.jpg", + :large "http://cloudfront.net/ba2b9659-02d5-4df5-849d-b24d342176e6/large.jpg"} + {:name "Cam's Old-Fashioned Coffee House", :categories ["Old-Fashioned" "Coffee House"], :phone "415-871-9473", :id "5d1f918e-ef00-40de-b6e4-3f9f34ee8cd4"} + {:service "yelp", :yelp-photo-id "e6266710-a32b-4da5-8d9d-6b0440596c10", :categories ["Old-Fashioned" "Coffee House"]}] + ["Polk St. Mexican Coffee House is a exclusive and well-decorated place to people-watch weekend evenings." + {:small "http://cloudfront.net/2cfd3695-50fd-46fe-b141-07491a10ac99/small.jpg", + :medium "http://cloudfront.net/2cfd3695-50fd-46fe-b141-07491a10ac99/med.jpg", + :large "http://cloudfront.net/2cfd3695-50fd-46fe-b141-07491a10ac99/large.jpg"} + {:name "Polk St. Mexican Coffee House", :categories ["Mexican" "Coffee House"], :phone "415-144-7901", :id "396d36d7-13ad-41fd-86b5-8b70b6ecdabf"} + {:service "twitter", :mentions ["@polk_st._mexican_coffee_house"], :tags ["#mexican" "#coffee" "#house"], :username "sameer"}] + ["Tenderloin Gluten-Free Bar & Grill is a swell and exclusive place to have brunch weekend evenings." + {:small "http://cloudfront.net/cfb304d9-bb56-4b72-b9e7-df983f1fb9e1/small.jpg", + :medium "http://cloudfront.net/cfb304d9-bb56-4b72-b9e7-df983f1fb9e1/med.jpg", + :large "http://cloudfront.net/cfb304d9-bb56-4b72-b9e7-df983f1fb9e1/large.jpg"} + {:name "Tenderloin Gluten-Free Bar & Grill", :categories ["Gluten-Free" "Bar & Grill"], :phone "415-904-0956", :id "0d7e235a-eea8-45b3-aaa7-23b4ea2b50f2"} + {:service "foursquare", :foursquare-photo-id "02362879-deac-452e-b656-976edb806e8b", :mayor "rasta_toucan"}] + ["Sunset Homestyle Grill is a world-famous and fantastic place to meet new friends on public holidays." + {:small "http://cloudfront.net/a1ccd1c5-5144-475a-a151-450c3cc66742/small.jpg", + :medium "http://cloudfront.net/a1ccd1c5-5144-475a-a151-450c3cc66742/med.jpg", + :large "http://cloudfront.net/a1ccd1c5-5144-475a-a151-450c3cc66742/large.jpg"} + {:name "Sunset Homestyle Grill", :categories ["Homestyle" "Grill"], :phone "415-356-7052", :id "c57673cd-f2d0-4bbc-aed0-6c166d7cf2c3"} + {:service "facebook", :facebook-photo-id "fc57f41a-9684-4b88-bb8b-9c223eeb47ef", :url "http://facebook.com/photos/fc57f41a-9684-4b88-bb8b-9c223eeb47ef"}] + ["Haight Chinese Gastro Pub is a exclusive and underappreciated place to drink a craft beer the second Saturday of the month." + {:small "http://cloudfront.net/1ac6a807-49a0-4c57-90c7-4ded792903fe/small.jpg", + :medium "http://cloudfront.net/1ac6a807-49a0-4c57-90c7-4ded792903fe/med.jpg", + :large "http://cloudfront.net/1ac6a807-49a0-4c57-90c7-4ded792903fe/large.jpg"} + {:name "Haight Chinese Gastro Pub", :categories ["Chinese" "Gastro Pub"], :phone "415-521-5825", :id "12a8dc6e-1b2c-47e2-9c18-3ae220e4806f"} + {:service "twitter", :mentions ["@haight_chinese_gastro_pub"], :tags ["#chinese" "#gastro" "#pub"], :username "bob"}] + ["Haight Soul Food Café is a great and popular place to pitch an investor during winter." + {:small "http://cloudfront.net/9e69fff5-926e-4fdd-a160-1dc607ab06a0/small.jpg", + :medium "http://cloudfront.net/9e69fff5-926e-4fdd-a160-1dc607ab06a0/med.jpg", + :large "http://cloudfront.net/9e69fff5-926e-4fdd-a160-1dc607ab06a0/large.jpg"} + {:name "Haight Soul Food Café", :categories ["Soul Food" "Café"], :phone "415-257-1769", :id "a1796c4b-da2b-474f-9fd6-4fa96c1eac70"} + {:service "foursquare", :foursquare-photo-id "dc0709af-da58-4c33-9919-0c3e42e4e0d7", :mayor "rasta_toucan"}] + ["SF Deep-Dish Eatery is a horrible and great place to drink a craft beer on Taco Tuesday." + {:small "http://cloudfront.net/b76358fa-f3cc-470a-9f9f-91be479e7c77/small.jpg", + :medium "http://cloudfront.net/b76358fa-f3cc-470a-9f9f-91be479e7c77/med.jpg", + :large "http://cloudfront.net/b76358fa-f3cc-470a-9f9f-91be479e7c77/large.jpg"} + {:name "SF Deep-Dish Eatery", :categories ["Deep-Dish" "Eatery"], :phone "415-476-9257", :id "ad41d3f6-c20c-46a7-9e5d-db602fff7d0d"} + {:service "foursquare", :foursquare-photo-id "e415f4d4-08f0-4ee0-abab-d2df0bf41fa1", :mayor "cam_saul"}] + ["Pacific Heights Free-Range Eatery is a groovy and historical place to have a birthday party in the spring." + {:small "http://cloudfront.net/b3499888-ccc2-456c-875b-c1b16e6a9fab/small.jpg", + :medium "http://cloudfront.net/b3499888-ccc2-456c-875b-c1b16e6a9fab/med.jpg", + :large "http://cloudfront.net/b3499888-ccc2-456c-875b-c1b16e6a9fab/large.jpg"} + {:name "Pacific Heights Free-Range Eatery", :categories ["Free-Range" "Eatery"], :phone "415-901-6541", :id "88b361c8-ce69-4b2e-b0f2-9deedd574af6"} + {:service "flare", :username "jane"}] + ["Haight Chinese Gastro Pub is a world-famous and popular place to take visiting friends and relatives the second Saturday of the month." + {:small "http://cloudfront.net/98170d41-1145-4c3c-8e4e-ecf65ac1ff26/small.jpg", + :medium "http://cloudfront.net/98170d41-1145-4c3c-8e4e-ecf65ac1ff26/med.jpg", + :large "http://cloudfront.net/98170d41-1145-4c3c-8e4e-ecf65ac1ff26/large.jpg"} + {:name "Haight Chinese Gastro Pub", :categories ["Chinese" "Gastro Pub"], :phone "415-521-5825", :id "12a8dc6e-1b2c-47e2-9c18-3ae220e4806f"} + {:service "foursquare", :foursquare-photo-id "a3de9324-b821-47ec-a07d-36c9c0702400", :mayor "lucky_pigeon"}] + ["Haight Soul Food Pop-Up Food Stand is a great and wonderful place to catch a bite to eat weekend evenings." + {:small "http://cloudfront.net/3187fbfa-fa2c-4109-af80-77b4e0afe5bd/small.jpg", + :medium "http://cloudfront.net/3187fbfa-fa2c-4109-af80-77b4e0afe5bd/med.jpg", + :large "http://cloudfront.net/3187fbfa-fa2c-4109-af80-77b4e0afe5bd/large.jpg"} + {:name "Haight Soul Food Pop-Up Food Stand", :categories ["Soul Food" "Pop-Up Food Stand"], :phone "415-741-8726", :id "9735184b-1299-410f-a98e-10d9c548af42"} + {:service "yelp", :yelp-photo-id "6747488f-9287-40f4-b676-0116b0973bec", :categories ["Soul Food" "Pop-Up Food Stand"]}] + ["Pacific Heights Red White & Blue Bar & Grill is a horrible and decent place to watch the Giants game in July." + {:small "http://cloudfront.net/919615dc-461e-4f34-ac5f-97253d515021/small.jpg", + :medium "http://cloudfront.net/919615dc-461e-4f34-ac5f-97253d515021/med.jpg", + :large "http://cloudfront.net/919615dc-461e-4f34-ac5f-97253d515021/large.jpg"} + {:name "Pacific Heights Red White & Blue Bar & Grill", :categories ["Red White & Blue" "Bar & Grill"], :phone "415-208-2550", :id "c7547aa1-94c1-44bd-bf5a-8655e4698ed8"} + {:service "yelp", :yelp-photo-id "2e35c934-2f4c-417f-a6ab-56a8bda58b16", :categories ["Red White & Blue" "Bar & Grill"]}] + ["Rasta's Old-Fashioned Pop-Up Food Stand is a exclusive and family-friendly place to conduct a business meeting on public holidays." + {:small "http://cloudfront.net/0e16f689-a170-4fe0-9e15-8fd838abdf09/small.jpg", + :medium "http://cloudfront.net/0e16f689-a170-4fe0-9e15-8fd838abdf09/med.jpg", + :large "http://cloudfront.net/0e16f689-a170-4fe0-9e15-8fd838abdf09/large.jpg"} + {:name "Rasta's Old-Fashioned Pop-Up Food Stand", :categories ["Old-Fashioned" "Pop-Up Food Stand"], :phone "415-942-1875", :id "9fd8b920-a877-4888-86bf-578b2724ac4e"} + {:service "flare", :username "tupac"}] + ["Mission Free-Range Liquor Store is a groovy and delicious place to conduct a business meeting in June." + {:small "http://cloudfront.net/d72b00cf-fc40-493a-9f27-aab6ab438e38/small.jpg", + :medium "http://cloudfront.net/d72b00cf-fc40-493a-9f27-aab6ab438e38/med.jpg", + :large "http://cloudfront.net/d72b00cf-fc40-493a-9f27-aab6ab438e38/large.jpg"} + {:name "Mission Free-Range Liquor Store", :categories ["Free-Range" "Liquor Store"], :phone "415-041-3816", :id "6e665924-8e2c-42ab-af58-23a27f017e37"} + {:service "foursquare", :foursquare-photo-id "9f9fdd2d-a3d1-4bc1-b706-7e94d8ea2042", :mayor "rasta_toucan"}] + ["Kyle's European Churros is a underappreciated and family-friendly place to watch the Giants game during summer." + {:small "http://cloudfront.net/046e8027-3830-4251-a7a9-b3039b4300f9/small.jpg", + :medium "http://cloudfront.net/046e8027-3830-4251-a7a9-b3039b4300f9/med.jpg", + :large "http://cloudfront.net/046e8027-3830-4251-a7a9-b3039b4300f9/large.jpg"} + {:name "Kyle's European Churros", :categories ["European" "Churros"], :phone "415-233-8392", :id "5270240c-6e6e-4512-9344-3dc497d6ea49"} + {:service "twitter", :mentions ["@kyles_european_churros"], :tags ["#european" "#churros"], :username "jane"}] + ["Haight Soul Food Hotel & Restaurant is a classic and decent place to people-watch on Taco Tuesday." + {:small "http://cloudfront.net/e23afb15-0deb-4060-a5ff-ec8497816adf/small.jpg", + :medium "http://cloudfront.net/e23afb15-0deb-4060-a5ff-ec8497816adf/med.jpg", + :large "http://cloudfront.net/e23afb15-0deb-4060-a5ff-ec8497816adf/large.jpg"} + {:name "Haight Soul Food Hotel & Restaurant", :categories ["Soul Food" "Hotel & Restaurant"], :phone "415-786-9541", :id "11a72eb3-9e96-4703-9a01-e8c2a9469046"} + {:service "flare", :username "rasta_toucan"}] + ["SoMa Old-Fashioned Pizzeria is a world-famous and classic place to catch a bite to eat in June." + {:small "http://cloudfront.net/926c9016-ec2e-4ef3-b40f-fd404e573d94/small.jpg", + :medium "http://cloudfront.net/926c9016-ec2e-4ef3-b40f-fd404e573d94/med.jpg", + :large "http://cloudfront.net/926c9016-ec2e-4ef3-b40f-fd404e573d94/large.jpg"} + {:name "SoMa Old-Fashioned Pizzeria", :categories ["Old-Fashioned" "Pizzeria"], :phone "415-966-8856", :id "deb8997b-734d-402b-a181-bd888214bc86"} + {:service "yelp", :yelp-photo-id "22e2d196-931a-4251-8fce-a5492c304185", :categories ["Old-Fashioned" "Pizzeria"]}] + ["Haight Chinese Gastro Pub is a modern and underground place to watch the Warriors game weekday afternoons." + {:small "http://cloudfront.net/b6c37f33-b6c7-4684-b96e-54b5ee77cac2/small.jpg", + :medium "http://cloudfront.net/b6c37f33-b6c7-4684-b96e-54b5ee77cac2/med.jpg", + :large "http://cloudfront.net/b6c37f33-b6c7-4684-b96e-54b5ee77cac2/large.jpg"} + {:name "Haight Chinese Gastro Pub", :categories ["Chinese" "Gastro Pub"], :phone "415-521-5825", :id "12a8dc6e-1b2c-47e2-9c18-3ae220e4806f"} + {:service "foursquare", :foursquare-photo-id "0211f4dc-c9db-4505-8b9d-fd5abf151ada", :mayor "rasta_toucan"}] + ["Cam's Old-Fashioned Coffee House is a decent and modern place to conduct a business meeting Friday nights." + {:small "http://cloudfront.net/34c40a62-a21f-4110-a3a9-d633e2bad9da/small.jpg", + :medium "http://cloudfront.net/34c40a62-a21f-4110-a3a9-d633e2bad9da/med.jpg", + :large "http://cloudfront.net/34c40a62-a21f-4110-a3a9-d633e2bad9da/large.jpg"} + {:name "Cam's Old-Fashioned Coffee House", :categories ["Old-Fashioned" "Coffee House"], :phone "415-871-9473", :id "5d1f918e-ef00-40de-b6e4-3f9f34ee8cd4"} + {:service "foursquare", :foursquare-photo-id "dc22d4ab-1f2a-4ad2-babf-9b5b96cc65d7", :mayor "joe"}] + ["Market St. Gluten-Free Café is a classic and wonderful place to watch the Warriors game in June." + {:small "http://cloudfront.net/b8408489-88ec-4ff0-9c18-1dc647aa70aa/small.jpg", + :medium "http://cloudfront.net/b8408489-88ec-4ff0-9c18-1dc647aa70aa/med.jpg", + :large "http://cloudfront.net/b8408489-88ec-4ff0-9c18-1dc647aa70aa/large.jpg"} + {:name "Market St. Gluten-Free Café", :categories ["Gluten-Free" "Café"], :phone "415-697-9776", :id "ce4947d0-071a-4491-b5ac-f8b0241bd54c"} + {:service "foursquare", :foursquare-photo-id "a033581b-09b2-487f-b78d-18af543bc0b6", :mayor "rasta_toucan"}] + ["Tenderloin Paleo Hotel & Restaurant is a world-famous and swell place to sip a glass of expensive wine weekend mornings." + {:small "http://cloudfront.net/00a4a70a-3f26-46a7-b952-a08e9ae1281f/small.jpg", + :medium "http://cloudfront.net/00a4a70a-3f26-46a7-b952-a08e9ae1281f/med.jpg", + :large "http://cloudfront.net/00a4a70a-3f26-46a7-b952-a08e9ae1281f/large.jpg"} + {:name "Tenderloin Paleo Hotel & Restaurant", :categories ["Paleo" "Hotel & Restaurant"], :phone "415-402-1652", :id "4dea27b4-6d89-4b86-80a8-5631e171da8d"} + {:service "foursquare", :foursquare-photo-id "44854a6b-c709-4f0f-ab52-f4b5982ec2ee", :mayor "amy"}] + ["Haight Soul Food Sushi is a swell and acceptable place to nurse a hangover on Saturday night." + {:small "http://cloudfront.net/d14c0a83-f0a8-4bee-b760-4c91bbd44c21/small.jpg", + :medium "http://cloudfront.net/d14c0a83-f0a8-4bee-b760-4c91bbd44c21/med.jpg", + :large "http://cloudfront.net/d14c0a83-f0a8-4bee-b760-4c91bbd44c21/large.jpg"} + {:name "Haight Soul Food Sushi", :categories ["Soul Food" "Sushi"], :phone "415-371-8026", :id "b4df5eb7-d8cd-431d-9d43-381984ec81ae"} + {:service "foursquare", :foursquare-photo-id "73b07d4f-8de1-4c30-be6e-65e891d60dcd", :mayor "sameer"}] + ["SoMa Japanese Churros is a world-famous and modern place to drink a craft beer when hungover." + {:small "http://cloudfront.net/d058c540-7cba-4cdd-82a1-1c19bb6e0926/small.jpg", + :medium "http://cloudfront.net/d058c540-7cba-4cdd-82a1-1c19bb6e0926/med.jpg", + :large "http://cloudfront.net/d058c540-7cba-4cdd-82a1-1c19bb6e0926/large.jpg"} + {:name "SoMa Japanese Churros", :categories ["Japanese" "Churros"], :phone "415-404-1510", :id "373858b2-e634-45d0-973d-4d0fed8c438b"} + {:service "flare", :username "bob"}] + ["Marina Low-Carb Food Truck is a fantastic and decent place to watch the Giants game in the spring." + {:small "http://cloudfront.net/7984c64f-8b50-49fe-bb19-d96b88d692eb/small.jpg", + :medium "http://cloudfront.net/7984c64f-8b50-49fe-bb19-d96b88d692eb/med.jpg", + :large "http://cloudfront.net/7984c64f-8b50-49fe-bb19-d96b88d692eb/large.jpg"} + {:name "Marina Low-Carb Food Truck", :categories ["Low-Carb" "Food Truck"], :phone "415-748-3513", :id "a13a5beb-19de-40ca-a334-02df3bdf5285"} + {:service "facebook", :facebook-photo-id "7888806a-6bcb-43e4-89a6-ee06096d8f6f", :url "http://facebook.com/photos/7888806a-6bcb-43e4-89a6-ee06096d8f6f"}] + ["Rasta's Paleo Churros is a historical and acceptable place to catch a bite to eat on public holidays." + {:small "http://cloudfront.net/d04563bd-9802-4fd9-bcb3-132544b06612/small.jpg", + :medium "http://cloudfront.net/d04563bd-9802-4fd9-bcb3-132544b06612/med.jpg", + :large "http://cloudfront.net/d04563bd-9802-4fd9-bcb3-132544b06612/large.jpg"} + {:name "Rasta's Paleo Churros", :categories ["Paleo" "Churros"], :phone "415-915-0309", :id "3bf48ec6-434b-43b1-be28-7644975ecaf9"} + {:service "yelp", :yelp-photo-id "e63bccf1-3d28-46c5-a3ca-7b8b8063a785", :categories ["Paleo" "Churros"]}] + ["Sameer's GMO-Free Pop-Up Food Stand is a wonderful and horrible place to drink a craft beer on Thursdays." + {:small "http://cloudfront.net/4a743cdf-ef0f-4b8c-839f-c7691449fe9e/small.jpg", + :medium "http://cloudfront.net/4a743cdf-ef0f-4b8c-839f-c7691449fe9e/med.jpg", + :large "http://cloudfront.net/4a743cdf-ef0f-4b8c-839f-c7691449fe9e/large.jpg"} + {:name "Sameer's GMO-Free Pop-Up Food Stand", :categories ["GMO-Free" "Pop-Up Food Stand"], :phone "415-217-7891", :id "a829efc7-7e03-4e73-b072-83d10d1e3953"} + {:service "foursquare", :foursquare-photo-id "2ae13e5f-bfef-46cb-8bfb-f568f5fa383d", :mayor "amy"}] + ["Marina Cage-Free Liquor Store is a delicious and acceptable place to pitch an investor on Taco Tuesday." + {:small "http://cloudfront.net/aa31e351-b952-4757-80ce-5563659b677e/small.jpg", + :medium "http://cloudfront.net/aa31e351-b952-4757-80ce-5563659b677e/med.jpg", + :large "http://cloudfront.net/aa31e351-b952-4757-80ce-5563659b677e/large.jpg"} + {:name "Marina Cage-Free Liquor Store", :categories ["Cage-Free" "Liquor Store"], :phone "415-571-0783", :id "ad68f549-3000-407e-ab98-7f314cfa4653"} + {:service "yelp", :yelp-photo-id "03bfbbbc-ca84-459a-a81e-3a2e08986052", :categories ["Cage-Free" "Liquor Store"]}] + ["Mission British Café is a decent and decent place to nurse a hangover after baseball games." + {:small "http://cloudfront.net/dd60f1e4-5954-4bb3-b826-684715614901/small.jpg", + :medium "http://cloudfront.net/dd60f1e4-5954-4bb3-b826-684715614901/med.jpg", + :large "http://cloudfront.net/dd60f1e4-5954-4bb3-b826-684715614901/large.jpg"} + {:name "Mission British Café", :categories ["British" "Café"], :phone "415-715-7004", :id "c99899e3-439c-4444-9dc4-5598632aec8d"} + {:service "yelp", :yelp-photo-id "4b790ee2-1c3f-4ee6-bd5d-9debea0377e1", :categories ["British" "Café"]}] + ["Sameer's Pizza Liquor Store is a great and popular place to nurse a hangover during summer." + {:small "http://cloudfront.net/b579ac79-99e9-4be9-861a-36e540f0d335/small.jpg", + :medium "http://cloudfront.net/b579ac79-99e9-4be9-861a-36e540f0d335/med.jpg", + :large "http://cloudfront.net/b579ac79-99e9-4be9-861a-36e540f0d335/large.jpg"} + {:name "Sameer's Pizza Liquor Store", :categories ["Pizza" "Liquor Store"], :phone "415-969-7474", :id "7b9c7dc3-d8f1-498d-843a-e62360449892"} + {:service "foursquare", :foursquare-photo-id "a8cc052d-7c49-4c06-b81d-81d5208ed90c", :mayor "jessica"}] + ["Pacific Heights Irish Grill is a overrated and underground place to people-watch in June." + {:small "http://cloudfront.net/11e72119-4d0a-4136-b84f-46ce0d184767/small.jpg", + :medium "http://cloudfront.net/11e72119-4d0a-4136-b84f-46ce0d184767/med.jpg", + :large "http://cloudfront.net/11e72119-4d0a-4136-b84f-46ce0d184767/large.jpg"} + {:name "Pacific Heights Irish Grill", :categories ["Irish" "Grill"], :phone "415-491-2202", :id "d6b92dfc-56e9-4f65-b1d0-595f120043d9"} + {:service "foursquare", :foursquare-photo-id "5d6e0806-1f8c-43c7-84c4-2209c132d438", :mayor "mandy"}] + ["SoMa Old-Fashioned Pizzeria is a exclusive and acceptable place to watch the Giants game in June." + {:small "http://cloudfront.net/a17fc0ad-3eb8-4b64-877e-4320b87f1ec5/small.jpg", + :medium "http://cloudfront.net/a17fc0ad-3eb8-4b64-877e-4320b87f1ec5/med.jpg", + :large "http://cloudfront.net/a17fc0ad-3eb8-4b64-877e-4320b87f1ec5/large.jpg"} + {:name "SoMa Old-Fashioned Pizzeria", :categories ["Old-Fashioned" "Pizzeria"], :phone "415-966-8856", :id "deb8997b-734d-402b-a181-bd888214bc86"} + {:service "foursquare", :foursquare-photo-id "5b08d58b-61b1-464a-8380-0bf9d846a9be", :mayor "sameer"}] + ["Mission Chinese Liquor Store is a swell and well-decorated place to catch a bite to eat during summer." + {:small "http://cloudfront.net/1cb214d4-0140-4aa5-aa4c-259f6d980fea/small.jpg", + :medium "http://cloudfront.net/1cb214d4-0140-4aa5-aa4c-259f6d980fea/med.jpg", + :large "http://cloudfront.net/1cb214d4-0140-4aa5-aa4c-259f6d980fea/large.jpg"} + {:name "Mission Chinese Liquor Store", :categories ["Chinese" "Liquor Store"], :phone "415-906-6919", :id "00132b5b-31fc-46f0-a288-f547f23477ee"} + {:service "twitter", :mentions ["@mission_chinese_liquor_store"], :tags ["#chinese" "#liquor" "#store"], :username "lucky_pigeon"}] + ["Haight Soul Food Sushi is a swell and underappreciated place to watch the Giants game in the spring." + {:small "http://cloudfront.net/88e6aee5-89e4-4b52-ac93-d721652a8d36/small.jpg", + :medium "http://cloudfront.net/88e6aee5-89e4-4b52-ac93-d721652a8d36/med.jpg", + :large "http://cloudfront.net/88e6aee5-89e4-4b52-ac93-d721652a8d36/large.jpg"} + {:name "Haight Soul Food Sushi", :categories ["Soul Food" "Sushi"], :phone "415-371-8026", :id "b4df5eb7-d8cd-431d-9d43-381984ec81ae"} + {:service "yelp", :yelp-photo-id "4043626c-1296-45f8-ba01-eca54642defa", :categories ["Soul Food" "Sushi"]}] + ["Market St. Homestyle Pop-Up Food Stand is a amazing and classic place to sip Champagne Friday nights." + {:small "http://cloudfront.net/d4d40457-4efa-414e-8741-a10843fea0fd/small.jpg", + :medium "http://cloudfront.net/d4d40457-4efa-414e-8741-a10843fea0fd/med.jpg", + :large "http://cloudfront.net/d4d40457-4efa-414e-8741-a10843fea0fd/large.jpg"} + {:name "Market St. Homestyle Pop-Up Food Stand", :categories ["Homestyle" "Pop-Up Food Stand"], :phone "415-213-3030", :id "2d873280-e43d-449e-9940-af96ae7df718"} + {:service "yelp", :yelp-photo-id "9b425a8e-9bcb-40ce-98df-52e8c6e47978", :categories ["Homestyle" "Pop-Up Food Stand"]}] + ["Polk St. Deep-Dish Hotel & Restaurant is a well-decorated and fantastic place to take a date in June." + {:small "http://cloudfront.net/0f7bdbd7-40e6-4b97-81c0-daae0875c3b7/small.jpg", + :medium "http://cloudfront.net/0f7bdbd7-40e6-4b97-81c0-daae0875c3b7/med.jpg", + :large "http://cloudfront.net/0f7bdbd7-40e6-4b97-81c0-daae0875c3b7/large.jpg"} + {:name "Polk St. Deep-Dish Hotel & Restaurant", :categories ["Deep-Dish" "Hotel & Restaurant"], :phone "415-666-8681", :id "47f1698e-ae11-46f5-818b-85a59d0affba"} + {:service "facebook", :facebook-photo-id "cd159f6b-6c5d-400e-90e9-c667d40cea43", :url "http://facebook.com/photos/cd159f6b-6c5d-400e-90e9-c667d40cea43"}] + ["Pacific Heights Red White & Blue Bar & Grill is a decent and family-friendly place to have a drink on Saturday night." + {:small "http://cloudfront.net/e7211862-740e-4100-919a-4fd221276ef1/small.jpg", + :medium "http://cloudfront.net/e7211862-740e-4100-919a-4fd221276ef1/med.jpg", + :large "http://cloudfront.net/e7211862-740e-4100-919a-4fd221276ef1/large.jpg"} + {:name "Pacific Heights Red White & Blue Bar & Grill", :categories ["Red White & Blue" "Bar & Grill"], :phone "415-208-2550", :id "c7547aa1-94c1-44bd-bf5a-8655e4698ed8"} + {:service "twitter", :mentions ["@pacific_heights_red_white_&_blue_bar_&_grill"], :tags ["#red" "#white" "#&" "#blue" "#bar" "#&" "#grill"], :username "cam_saul"}] + ["Haight European Grill is a wonderful and horrible place to sip Champagne with your pet dog." + {:small "http://cloudfront.net/f08661e3-3f7d-4e05-b3b7-25643b1478f1/small.jpg", + :medium "http://cloudfront.net/f08661e3-3f7d-4e05-b3b7-25643b1478f1/med.jpg", + :large "http://cloudfront.net/f08661e3-3f7d-4e05-b3b7-25643b1478f1/large.jpg"} + {:name "Haight European Grill", :categories ["European" "Grill"], :phone "415-191-2778", :id "7e6281f7-5b17-4056-ada0-85453247bc8f"} + {:service "foursquare", :foursquare-photo-id "bad6706f-9387-4946-a65f-872b5055638b", :mayor "tupac"}] + ["Cam's Old-Fashioned Coffee House is a underappreciated and family-friendly place to have a drink with friends." + {:small "http://cloudfront.net/83f2915e-edff-49bf-bb42-2236c634f0da/small.jpg", + :medium "http://cloudfront.net/83f2915e-edff-49bf-bb42-2236c634f0da/med.jpg", + :large "http://cloudfront.net/83f2915e-edff-49bf-bb42-2236c634f0da/large.jpg"} + {:name "Cam's Old-Fashioned Coffee House", :categories ["Old-Fashioned" "Coffee House"], :phone "415-871-9473", :id "5d1f918e-ef00-40de-b6e4-3f9f34ee8cd4"} + {:service "twitter", :mentions ["@cams_old_fashioned_coffee_house"], :tags ["#old-fashioned" "#coffee" "#house"], :username "rasta_toucan"}] + ["Cam's Old-Fashioned Coffee House is a exclusive and fantastic place to have a birthday party weekend evenings." + {:small "http://cloudfront.net/2f4200f6-8a0e-4a51-a80d-2783be070731/small.jpg", + :medium "http://cloudfront.net/2f4200f6-8a0e-4a51-a80d-2783be070731/med.jpg", + :large "http://cloudfront.net/2f4200f6-8a0e-4a51-a80d-2783be070731/large.jpg"} + {:name "Cam's Old-Fashioned Coffee House", :categories ["Old-Fashioned" "Coffee House"], :phone "415-871-9473", :id "5d1f918e-ef00-40de-b6e4-3f9f34ee8cd4"} + {:service "yelp", :yelp-photo-id "dcc28cfa-c4aa-4c03-8985-48b72c682e06", :categories ["Old-Fashioned" "Coffee House"]}] + ["Mission Homestyle Churros is a well-decorated and exclusive place to have a after-work cocktail the first Sunday of the month." + {:small "http://cloudfront.net/2c0d54dc-414f-45c0-9844-643b05dda78d/small.jpg", + :medium "http://cloudfront.net/2c0d54dc-414f-45c0-9844-643b05dda78d/med.jpg", + :large "http://cloudfront.net/2c0d54dc-414f-45c0-9844-643b05dda78d/large.jpg"} + {:name "Mission Homestyle Churros", :categories ["Homestyle" "Churros"], :phone "415-343-4489", :id "21d903d3-8bdb-4b7d-b288-6063ad48af44"} + {:service "flare", :username "cam_saul"}] + ["Alcatraz Pizza Churros is a groovy and underappreciated place to take visiting friends and relatives on Taco Tuesday." + {:small "http://cloudfront.net/e566abbe-39c9-47a6-be61-1654ca23d783/small.jpg", + :medium "http://cloudfront.net/e566abbe-39c9-47a6-be61-1654ca23d783/med.jpg", + :large "http://cloudfront.net/e566abbe-39c9-47a6-be61-1654ca23d783/large.jpg"} + {:name "Alcatraz Pizza Churros", :categories ["Pizza" "Churros"], :phone "415-754-7867", :id "df95e4f1-8719-42af-a15d-3ee00de6e04f"} + {:service "yelp", :yelp-photo-id "161da098-e1d5-43d2-9ca9-f10ca0b48008", :categories ["Pizza" "Churros"]}] + ["Polk St. Deep-Dish Hotel & Restaurant is a modern and swell place to have a birthday party on Saturday night." + {:small "http://cloudfront.net/3511bf6b-6066-4139-bde0-ce2a4ffee9bd/small.jpg", + :medium "http://cloudfront.net/3511bf6b-6066-4139-bde0-ce2a4ffee9bd/med.jpg", + :large "http://cloudfront.net/3511bf6b-6066-4139-bde0-ce2a4ffee9bd/large.jpg"} + {:name "Polk St. Deep-Dish Hotel & Restaurant", :categories ["Deep-Dish" "Hotel & Restaurant"], :phone "415-666-8681", :id "47f1698e-ae11-46f5-818b-85a59d0affba"} + {:service "foursquare", :foursquare-photo-id "a8cf3fba-ecc4-46b5-97de-35f953def90e", :mayor "bob"}] + ["Rasta's Mexican Sushi is a swell and horrible place to conduct a business meeting in June." + {:small "http://cloudfront.net/5ec77779-ac5d-4c0e-8d7c-a83cb883db1b/small.jpg", + :medium "http://cloudfront.net/5ec77779-ac5d-4c0e-8d7c-a83cb883db1b/med.jpg", + :large "http://cloudfront.net/5ec77779-ac5d-4c0e-8d7c-a83cb883db1b/large.jpg"} + {:name "Rasta's Mexican Sushi", :categories ["Mexican" "Sushi"], :phone "415-387-1284", :id "e4912a22-e6ac-4806-8377-6497bf533a21"} + {:service "yelp", :yelp-photo-id "7679858d-2f30-4af5-b620-e406eb0b2f73", :categories ["Mexican" "Sushi"]}] + ["Haight Chinese Gastro Pub is a underappreciated and overrated place to take a date weekend evenings." + {:small "http://cloudfront.net/10d9c169-e448-4c5a-bc35-d6c316f0ebf6/small.jpg", + :medium "http://cloudfront.net/10d9c169-e448-4c5a-bc35-d6c316f0ebf6/med.jpg", + :large "http://cloudfront.net/10d9c169-e448-4c5a-bc35-d6c316f0ebf6/large.jpg"} + {:name "Haight Chinese Gastro Pub", :categories ["Chinese" "Gastro Pub"], :phone "415-521-5825", :id "12a8dc6e-1b2c-47e2-9c18-3ae220e4806f"} + {:service "twitter", :mentions ["@haight_chinese_gastro_pub"], :tags ["#chinese" "#gastro" "#pub"], :username "tupac"}] + ["Alcatraz Modern Eatery is a underground and underground place to meet new friends on a Tuesday afternoon." + {:small "http://cloudfront.net/ea1f1ae6-f34f-429d-b3ae-f31d4eec1560/small.jpg", + :medium "http://cloudfront.net/ea1f1ae6-f34f-429d-b3ae-f31d4eec1560/med.jpg", + :large "http://cloudfront.net/ea1f1ae6-f34f-429d-b3ae-f31d4eec1560/large.jpg"} + {:name "Alcatraz Modern Eatery", :categories ["Modern" "Eatery"], :phone "415-899-2965", :id "bbfafaac-e825-4c4f-8655-f5e697148d9c"} + {:service "facebook", :facebook-photo-id "ab85f630-7d6d-445f-9848-48461b588909", :url "http://facebook.com/photos/ab85f630-7d6d-445f-9848-48461b588909"}] + ["Haight Soul Food Café is a amazing and fantastic place to drink a craft beer with your pet dog." + {:small "http://cloudfront.net/0acfc578-9f98-4e18-aa00-b9f4913d5aa8/small.jpg", + :medium "http://cloudfront.net/0acfc578-9f98-4e18-aa00-b9f4913d5aa8/med.jpg", + :large "http://cloudfront.net/0acfc578-9f98-4e18-aa00-b9f4913d5aa8/large.jpg"} + {:name "Haight Soul Food Café", :categories ["Soul Food" "Café"], :phone "415-257-1769", :id "a1796c4b-da2b-474f-9fd6-4fa96c1eac70"} + {:service "facebook", :facebook-photo-id "050b4f87-87f7-45c2-aeb7-d02cae41b076", :url "http://facebook.com/photos/050b4f87-87f7-45c2-aeb7-d02cae41b076"}] + ["Lucky's Gluten-Free Café is a swell and horrible place to watch the Giants game with your pet dog." + {:small "http://cloudfront.net/f2ce5a16-6524-49f1-aa01-c231e73d8e0e/small.jpg", + :medium "http://cloudfront.net/f2ce5a16-6524-49f1-aa01-c231e73d8e0e/med.jpg", + :large "http://cloudfront.net/f2ce5a16-6524-49f1-aa01-c231e73d8e0e/large.jpg"} + {:name "Lucky's Gluten-Free Café", :categories ["Gluten-Free" "Café"], :phone "415-740-2328", :id "379af987-ad40-4a93-88a6-0233e1c14649"} + {:service "foursquare", :foursquare-photo-id "f63d629b-0e13-4c23-8cc8-6b08ca2e8213", :mayor "tupac"}] + ["Joe's Modern Coffee House is a exclusive and exclusive place to sip Champagne with your pet dog." + {:small "http://cloudfront.net/1388c27c-c987-4d8d-8f26-60094f8057e8/small.jpg", + :medium "http://cloudfront.net/1388c27c-c987-4d8d-8f26-60094f8057e8/med.jpg", + :large "http://cloudfront.net/1388c27c-c987-4d8d-8f26-60094f8057e8/large.jpg"} + {:name "Joe's Modern Coffee House", :categories ["Modern" "Coffee House"], :phone "415-331-5269", :id "cd9e3610-9ead-4f0b-ad93-cd53611d49fe"} + {:service "yelp", :yelp-photo-id "ff896124-cf74-44f8-ab68-631810f8bbff", :categories ["Modern" "Coffee House"]}] + ["Oakland Low-Carb Bakery is a classic and groovy place to have breakfast on Taco Tuesday." + {:small "http://cloudfront.net/1fde679f-4f4d-4cd9-b4ba-33417d84f2bb/small.jpg", + :medium "http://cloudfront.net/1fde679f-4f4d-4cd9-b4ba-33417d84f2bb/med.jpg", + :large "http://cloudfront.net/1fde679f-4f4d-4cd9-b4ba-33417d84f2bb/large.jpg"} + {:name "Oakland Low-Carb Bakery", :categories ["Low-Carb" "Bakery"], :phone "415-546-0101", :id "da7dd72d-60fb-495b-a2c0-1e2ae73a1a86"} + {:service "foursquare", :foursquare-photo-id "8608a711-9a31-4f23-a2b5-01de2b554df0", :mayor "lucky_pigeon"}] + ["Mission Homestyle Churros is a exclusive and popular place to have a birthday party Friday nights." + {:small "http://cloudfront.net/d50c5351-db5f-409d-a3db-b17509974e6c/small.jpg", + :medium "http://cloudfront.net/d50c5351-db5f-409d-a3db-b17509974e6c/med.jpg", + :large "http://cloudfront.net/d50c5351-db5f-409d-a3db-b17509974e6c/large.jpg"} + {:name "Mission Homestyle Churros", :categories ["Homestyle" "Churros"], :phone "415-343-4489", :id "21d903d3-8bdb-4b7d-b288-6063ad48af44"} + {:service "foursquare", :foursquare-photo-id "b9f30495-8334-43b1-bcbd-e3b9e8cab52a", :mayor "cam_saul"}] + ["Haight Mexican Restaurant is a swell and exclusive place to meet new friends with your pet dog." + {:small "http://cloudfront.net/86e0d356-dd46-477f-9dae-338c30ea5a2b/small.jpg", + :medium "http://cloudfront.net/86e0d356-dd46-477f-9dae-338c30ea5a2b/med.jpg", + :large "http://cloudfront.net/86e0d356-dd46-477f-9dae-338c30ea5a2b/large.jpg"} + {:name "Haight Mexican Restaurant", :categories ["Mexican" "Restaurant"], :phone "415-758-8690", :id "a5e3f0ac-f6e8-4e71-a0a2-3f10f48b4ab1"} + {:service "twitter", :mentions ["@haight_mexican_restaurant"], :tags ["#mexican" "#restaurant"], :username "rasta_toucan"}] + ["Sameer's GMO-Free Pop-Up Food Stand is a great and decent place to have breakfast in June." + {:small "http://cloudfront.net/5c8a43de-d9a0-49a9-8cc6-eb9dea7800fb/small.jpg", + :medium "http://cloudfront.net/5c8a43de-d9a0-49a9-8cc6-eb9dea7800fb/med.jpg", + :large "http://cloudfront.net/5c8a43de-d9a0-49a9-8cc6-eb9dea7800fb/large.jpg"} + {:name "Sameer's GMO-Free Pop-Up Food Stand", :categories ["GMO-Free" "Pop-Up Food Stand"], :phone "415-217-7891", :id "a829efc7-7e03-4e73-b072-83d10d1e3953"} + {:service "twitter", :mentions ["@sameers_gmo_free_pop_up_food_stand"], :tags ["#gmo-free" "#pop-up" "#food" "#stand"], :username "bob"}] + ["Cam's Old-Fashioned Coffee House is a delicious and modern place to people-watch on Taco Tuesday." + {:small "http://cloudfront.net/9a3e6c72-3ab0-4105-bde4-0c08ee753d40/small.jpg", + :medium "http://cloudfront.net/9a3e6c72-3ab0-4105-bde4-0c08ee753d40/med.jpg", + :large "http://cloudfront.net/9a3e6c72-3ab0-4105-bde4-0c08ee753d40/large.jpg"} + {:name "Cam's Old-Fashioned Coffee House", :categories ["Old-Fashioned" "Coffee House"], :phone "415-868-2973", :id "27592c2b-e682-44bb-be28-8e9a622becca"} + {:service "foursquare", :foursquare-photo-id "4549eb5a-9e17-4b08-bfb3-44acfcc9d494", :mayor "tupac"}] + ["Cam's Mexican Gastro Pub is a great and swell place to drink a craft beer in June." + {:small "http://cloudfront.net/036ac2ce-65b1-4a03-aef3-6919a7789f00/small.jpg", + :medium "http://cloudfront.net/036ac2ce-65b1-4a03-aef3-6919a7789f00/med.jpg", + :large "http://cloudfront.net/036ac2ce-65b1-4a03-aef3-6919a7789f00/large.jpg"} + {:name "Cam's Mexican Gastro Pub", :categories ["Mexican" "Gastro Pub"], :phone "415-320-9123", :id "bb958ac5-758e-4f42-b984-6b0e13f25194"} + {:service "flare", :username "jane"}] + ["Sunset Homestyle Grill is a overrated and underappreciated place to nurse a hangover in the fall." + {:small "http://cloudfront.net/7b59a37b-1f2c-4cf1-a6dd-14bbf2111904/small.jpg", + :medium "http://cloudfront.net/7b59a37b-1f2c-4cf1-a6dd-14bbf2111904/med.jpg", + :large "http://cloudfront.net/7b59a37b-1f2c-4cf1-a6dd-14bbf2111904/large.jpg"} + {:name "Sunset Homestyle Grill", :categories ["Homestyle" "Grill"], :phone "415-356-7052", :id "c57673cd-f2d0-4bbc-aed0-6c166d7cf2c3"} + {:service "twitter", :mentions ["@sunset_homestyle_grill"], :tags ["#homestyle" "#grill"], :username "amy"}] + ["Polk St. Red White & Blue Café is a delicious and swell place to conduct a business meeting on Thursdays." + {:small "http://cloudfront.net/9aefced2-f658-485c-a1c5-ee5cbdbb42d7/small.jpg", + :medium "http://cloudfront.net/9aefced2-f658-485c-a1c5-ee5cbdbb42d7/med.jpg", + :large "http://cloudfront.net/9aefced2-f658-485c-a1c5-ee5cbdbb42d7/large.jpg"} + {:name "Polk St. Red White & Blue Café", :categories ["Red White & Blue" "Café"], :phone "415-986-0661", :id "6eb9d2db-5015-49f0-bd18-f4c4938b7e5a"} + {:service "facebook", :facebook-photo-id "01ba3c6f-4360-406a-b1d3-5e6ca8d28a5a", :url "http://facebook.com/photos/01ba3c6f-4360-406a-b1d3-5e6ca8d28a5a"}] + ["Haight European Grill is a swell and overrated place to watch the Giants game with your pet dog." + {:small "http://cloudfront.net/b0c4fee2-d9cd-4183-8695-8466dd15c08d/small.jpg", + :medium "http://cloudfront.net/b0c4fee2-d9cd-4183-8695-8466dd15c08d/med.jpg", + :large "http://cloudfront.net/b0c4fee2-d9cd-4183-8695-8466dd15c08d/large.jpg"} + {:name "Haight European Grill", :categories ["European" "Grill"], :phone "415-191-2778", :id "7e6281f7-5b17-4056-ada0-85453247bc8f"} + {:service "facebook", :facebook-photo-id "42762b62-7cff-4a54-a0f7-f16c1f42d81c", :url "http://facebook.com/photos/42762b62-7cff-4a54-a0f7-f16c1f42d81c"}] + ["Haight Soul Food Pop-Up Food Stand is a amazing and world-famous place to sip a glass of expensive wine weekend evenings." + {:small "http://cloudfront.net/1285fa6f-a990-46c0-ad26-443d959f183c/small.jpg", + :medium "http://cloudfront.net/1285fa6f-a990-46c0-ad26-443d959f183c/med.jpg", + :large "http://cloudfront.net/1285fa6f-a990-46c0-ad26-443d959f183c/large.jpg"} + {:name "Haight Soul Food Pop-Up Food Stand", :categories ["Soul Food" "Pop-Up Food Stand"], :phone "415-741-8726", :id "9735184b-1299-410f-a98e-10d9c548af42"} + {:service "twitter", :mentions ["@haight_soul_food_pop_up_food_stand"], :tags ["#soul" "#food" "#pop-up" "#food" "#stand"], :username "sameer"}] + ["Tenderloin Red White & Blue Pizzeria is a decent and underappreciated place to have a drink on Taco Tuesday." + {:small "http://cloudfront.net/0229bb84-6bc0-4ed4-b3da-dc743a49c8c8/small.jpg", + :medium "http://cloudfront.net/0229bb84-6bc0-4ed4-b3da-dc743a49c8c8/med.jpg", + :large "http://cloudfront.net/0229bb84-6bc0-4ed4-b3da-dc743a49c8c8/large.jpg"} + {:name "Tenderloin Red White & Blue Pizzeria", :categories ["Red White & Blue" "Pizzeria"], :phone "415-719-8143", :id "eba3dbcd-100a-4f38-a701-e0dec157f437"} + {:service "twitter", :mentions ["@tenderloin_red_white_&_blue_pizzeria"], :tags ["#red" "#white" "#&" "#blue" "#pizzeria"], :username "jessica"}] + ["Sunset Homestyle Grill is a amazing and wonderful place to have brunch on a Tuesday afternoon." + {:small "http://cloudfront.net/4203f53b-6a89-4f14-a597-305c3b2a27a1/small.jpg", + :medium "http://cloudfront.net/4203f53b-6a89-4f14-a597-305c3b2a27a1/med.jpg", + :large "http://cloudfront.net/4203f53b-6a89-4f14-a597-305c3b2a27a1/large.jpg"} + {:name "Sunset Homestyle Grill", :categories ["Homestyle" "Grill"], :phone "415-356-7052", :id "c57673cd-f2d0-4bbc-aed0-6c166d7cf2c3"} + {:service "twitter", :mentions ["@sunset_homestyle_grill"], :tags ["#homestyle" "#grill"], :username "jane"}] + ["Lucky's Old-Fashioned Eatery is a decent and acceptable place to have a birthday party on public holidays." + {:small "http://cloudfront.net/9ea7bca8-891b-4b8e-be13-6374a91604a9/small.jpg", + :medium "http://cloudfront.net/9ea7bca8-891b-4b8e-be13-6374a91604a9/med.jpg", + :large "http://cloudfront.net/9ea7bca8-891b-4b8e-be13-6374a91604a9/large.jpg"} + {:name "Lucky's Old-Fashioned Eatery", :categories ["Old-Fashioned" "Eatery"], :phone "415-362-2338", :id "71dc221c-6e82-4d06-8709-93293121b1da"} + {:service "foursquare", :foursquare-photo-id "b555bae0-96f0-492f-807d-484460d33f62", :mayor "sameer"}] + ["Pacific Heights Free-Range Eatery is a swell and swell place to have a birthday party the first Sunday of the month." + {:small "http://cloudfront.net/52083005-7074-4b91-b2e7-aa3cbd81bb21/small.jpg", + :medium "http://cloudfront.net/52083005-7074-4b91-b2e7-aa3cbd81bb21/med.jpg", + :large "http://cloudfront.net/52083005-7074-4b91-b2e7-aa3cbd81bb21/large.jpg"} + {:name "Pacific Heights Free-Range Eatery", :categories ["Free-Range" "Eatery"], :phone "415-901-6541", :id "88b361c8-ce69-4b2e-b0f2-9deedd574af6"} + {:service "foursquare", :foursquare-photo-id "bb3679fc-46fa-4b28-b0d2-291b301c67c1", :mayor "rasta_toucan"}] + ["Mission Homestyle Churros is a swell and great place to have a birthday party weekend evenings." + {:small "http://cloudfront.net/bd9023b0-0eda-41d6-823c-9cf526e5f5f3/small.jpg", + :medium "http://cloudfront.net/bd9023b0-0eda-41d6-823c-9cf526e5f5f3/med.jpg", + :large "http://cloudfront.net/bd9023b0-0eda-41d6-823c-9cf526e5f5f3/large.jpg"} + {:name "Mission Homestyle Churros", :categories ["Homestyle" "Churros"], :phone "415-343-4489", :id "21d903d3-8bdb-4b7d-b288-6063ad48af44"} + {:service "foursquare", :foursquare-photo-id "76baeb06-f79c-43c4-ae15-108b69983aa3", :mayor "mandy"}] + ["Mission Japanese Coffee House is a classic and wonderful place to watch the Warriors game in the spring." + {:small "http://cloudfront.net/8bff3709-0af5-4e6f-9963-575cb5044b37/small.jpg", + :medium "http://cloudfront.net/8bff3709-0af5-4e6f-9963-575cb5044b37/med.jpg", + :large "http://cloudfront.net/8bff3709-0af5-4e6f-9963-575cb5044b37/large.jpg"} + {:name "Mission Japanese Coffee House", :categories ["Japanese" "Coffee House"], :phone "415-561-0506", :id "60dd274e-0cbf-4521-946d-8a4e0f151150"} + {:service "yelp", :yelp-photo-id "35f2e21d-b05f-4e7c-adfe-e8d70ecd478e", :categories ["Japanese" "Coffee House"]}] + ["Kyle's Free-Range Taqueria is a popular and modern place to have a birthday party in June." + {:small "http://cloudfront.net/00ba782a-b50a-4246-88eb-c965f39a27b2/small.jpg", + :medium "http://cloudfront.net/00ba782a-b50a-4246-88eb-c965f39a27b2/med.jpg", + :large "http://cloudfront.net/00ba782a-b50a-4246-88eb-c965f39a27b2/large.jpg"} + {:name "Kyle's Free-Range Taqueria", :categories ["Free-Range" "Taqueria"], :phone "415-201-7832", :id "7aeb9416-4fe6-45f9-8849-6b8ba6d3f3b9"} + {:service "yelp", :yelp-photo-id "f1bc8db8-9cd5-4664-b6d9-440618790ffc", :categories ["Free-Range" "Taqueria"]}] + ["Haight Soul Food Hotel & Restaurant is a wonderful and amazing place to meet new friends with friends." + {:small "http://cloudfront.net/84c1175b-e3aa-47cd-83b8-abf1122496bf/small.jpg", + :medium "http://cloudfront.net/84c1175b-e3aa-47cd-83b8-abf1122496bf/med.jpg", + :large "http://cloudfront.net/84c1175b-e3aa-47cd-83b8-abf1122496bf/large.jpg"} + {:name "Haight Soul Food Hotel & Restaurant", :categories ["Soul Food" "Hotel & Restaurant"], :phone "415-786-9541", :id "11a72eb3-9e96-4703-9a01-e8c2a9469046"} + {:service "flare", :username "mandy"}] + ["SoMa Japanese Churros is a fantastic and world-famous place to watch the Warriors game when hungover." + {:small "http://cloudfront.net/60f59426-c2db-4bb5-885b-af2641a8f7b5/small.jpg", + :medium "http://cloudfront.net/60f59426-c2db-4bb5-885b-af2641a8f7b5/med.jpg", + :large "http://cloudfront.net/60f59426-c2db-4bb5-885b-af2641a8f7b5/large.jpg"} + {:name "SoMa Japanese Churros", :categories ["Japanese" "Churros"], :phone "415-404-1510", :id "373858b2-e634-45d0-973d-4d0fed8c438b"} + {:service "twitter", :mentions ["@soma_japanese_churros"], :tags ["#japanese" "#churros"], :username "tupac"}] + ["Pacific Heights Pizza Bakery is a overrated and exclusive place to take visiting friends and relatives on public holidays." + {:small "http://cloudfront.net/721c2488-5e3b-4f25-9da4-463c5cbecf21/small.jpg", + :medium "http://cloudfront.net/721c2488-5e3b-4f25-9da4-463c5cbecf21/med.jpg", + :large "http://cloudfront.net/721c2488-5e3b-4f25-9da4-463c5cbecf21/large.jpg"} + {:name "Pacific Heights Pizza Bakery", :categories ["Pizza" "Bakery"], :phone "415-006-0149", :id "7fda37a5-810f-4902-b571-54afe583f0dd"} + {:service "yelp", :yelp-photo-id "da8703eb-dd7c-4559-b39c-e2a6fff91b92", :categories ["Pizza" "Bakery"]}] + ["Mission British Café is a decent and swell place to drink a craft beer in June." + {:small "http://cloudfront.net/75976ced-01ca-4762-93aa-3e9b255a8dc7/small.jpg", + :medium "http://cloudfront.net/75976ced-01ca-4762-93aa-3e9b255a8dc7/med.jpg", + :large "http://cloudfront.net/75976ced-01ca-4762-93aa-3e9b255a8dc7/large.jpg"} + {:name "Mission British Café", :categories ["British" "Café"], :phone "415-715-7004", :id "c99899e3-439c-4444-9dc4-5598632aec8d"} + {:service "twitter", :mentions ["@mission_british_café"], :tags ["#british" "#café"], :username "lucky_pigeon"}] + ["Oakland American Grill is a amazing and underground place to have brunch on Saturday night." + {:small "http://cloudfront.net/a848ecf7-8d6a-4bb4-bd15-674fac1b7f79/small.jpg", + :medium "http://cloudfront.net/a848ecf7-8d6a-4bb4-bd15-674fac1b7f79/med.jpg", + :large "http://cloudfront.net/a848ecf7-8d6a-4bb4-bd15-674fac1b7f79/large.jpg"} + {:name "Oakland American Grill", :categories ["American" "Grill"], :phone "415-660-0889", :id "856f907d-b669-4b9c-8337-bf9c88883746"} + {:service "flare", :username "mandy"}] + ["Pacific Heights Free-Range Eatery is a great and modern place to take visiting friends and relatives on a Tuesday afternoon." + {:small "http://cloudfront.net/b12a9c02-b78a-43de-a4e4-838be542a7b7/small.jpg", + :medium "http://cloudfront.net/b12a9c02-b78a-43de-a4e4-838be542a7b7/med.jpg", + :large "http://cloudfront.net/b12a9c02-b78a-43de-a4e4-838be542a7b7/large.jpg"} + {:name "Pacific Heights Free-Range Eatery", :categories ["Free-Range" "Eatery"], :phone "415-901-6541", :id "88b361c8-ce69-4b2e-b0f2-9deedd574af6"} + {:service "twitter", :mentions ["@pacific_heights_free_range_eatery"], :tags ["#free-range" "#eatery"], :username "biggie"}] + ["SoMa Old-Fashioned Pizzeria is a groovy and delicious place to nurse a hangover when hungover." + {:small "http://cloudfront.net/1182d679-c0cb-4250-b219-d6da0fefaa2e/small.jpg", + :medium "http://cloudfront.net/1182d679-c0cb-4250-b219-d6da0fefaa2e/med.jpg", + :large "http://cloudfront.net/1182d679-c0cb-4250-b219-d6da0fefaa2e/large.jpg"} + {:name "SoMa Old-Fashioned Pizzeria", :categories ["Old-Fashioned" "Pizzeria"], :phone "415-966-8856", :id "deb8997b-734d-402b-a181-bd888214bc86"} + {:service "flare", :username "joe"}] + ["Haight Mexican Restaurant is a historical and horrible place to sip Champagne weekday afternoons." + {:small "http://cloudfront.net/18129bb1-88f0-45c2-ba6b-bb86b8004a18/small.jpg", + :medium "http://cloudfront.net/18129bb1-88f0-45c2-ba6b-bb86b8004a18/med.jpg", + :large "http://cloudfront.net/18129bb1-88f0-45c2-ba6b-bb86b8004a18/large.jpg"} + {:name "Haight Mexican Restaurant", :categories ["Mexican" "Restaurant"], :phone "415-758-8690", :id "a5e3f0ac-f6e8-4e71-a0a2-3f10f48b4ab1"} + {:service "twitter", :mentions ["@haight_mexican_restaurant"], :tags ["#mexican" "#restaurant"], :username "amy"}] + ["Nob Hill Korean Taqueria is a overrated and classic place to pitch an investor weekend mornings." + {:small "http://cloudfront.net/c466065c-7be8-46d0-8920-f0a98d4d94ef/small.jpg", + :medium "http://cloudfront.net/c466065c-7be8-46d0-8920-f0a98d4d94ef/med.jpg", + :large "http://cloudfront.net/c466065c-7be8-46d0-8920-f0a98d4d94ef/large.jpg"} + {:name "Nob Hill Korean Taqueria", :categories ["Korean" "Taqueria"], :phone "415-107-7332", :id "a43c184c-90f5-488c-bb3b-00ea2666d90e"} + {:service "twitter", :mentions ["@nob_hill_korean_taqueria"], :tags ["#korean" "#taqueria"], :username "jessica"}] + ["Joe's Modern Coffee House is a acceptable and fantastic place to watch the Giants game in the spring." + {:small "http://cloudfront.net/ec13291b-2bee-4993-8198-0c1f799f9a3b/small.jpg", + :medium "http://cloudfront.net/ec13291b-2bee-4993-8198-0c1f799f9a3b/med.jpg", + :large "http://cloudfront.net/ec13291b-2bee-4993-8198-0c1f799f9a3b/large.jpg"} + {:name "Joe's Modern Coffee House", :categories ["Modern" "Coffee House"], :phone "415-331-5269", :id "cd9e3610-9ead-4f0b-ad93-cd53611d49fe"} + {:service "foursquare", :foursquare-photo-id "e00287d6-3633-42a1-a096-7756abdc25fa", :mayor "joe"}] + ["Kyle's Chinese Restaurant is a exclusive and great place to have a after-work cocktail on Saturday night." + {:small "http://cloudfront.net/bc41c4ff-50e6-4971-9de8-14cd0ed203ee/small.jpg", + :medium "http://cloudfront.net/bc41c4ff-50e6-4971-9de8-14cd0ed203ee/med.jpg", + :large "http://cloudfront.net/bc41c4ff-50e6-4971-9de8-14cd0ed203ee/large.jpg"} + {:name "Kyle's Chinese Restaurant", :categories ["Chinese" "Restaurant"], :phone "415-298-9499", :id "de08b3c7-9929-40d8-8c20-dd9317613c17"} + {:service "twitter", :mentions ["@kyles_chinese_restaurant"], :tags ["#chinese" "#restaurant"], :username "jessica"}] + ["Pacific Heights Soul Food Coffee House is a fantastic and great place to catch a bite to eat in June." + {:small "http://cloudfront.net/b00518e6-988c-4175-a17a-e49b5924a014/small.jpg", + :medium "http://cloudfront.net/b00518e6-988c-4175-a17a-e49b5924a014/med.jpg", + :large "http://cloudfront.net/b00518e6-988c-4175-a17a-e49b5924a014/large.jpg"} + {:name "Pacific Heights Soul Food Coffee House", :categories ["Soul Food" "Coffee House"], :phone "415-838-3464", :id "6aed1816-4c64-4f76-8e2a-619528e5b48d"} + {:service "facebook", :facebook-photo-id "a909c6c2-faee-4994-9f99-a922f40d64ea", :url "http://facebook.com/photos/a909c6c2-faee-4994-9f99-a922f40d64ea"}] + ["Chinatown Paleo Food Truck is a underground and well-decorated place to drink a craft beer weekend evenings." + {:small "http://cloudfront.net/ffa9dc6d-5d1c-4475-923c-72934f3e8762/small.jpg", + :medium "http://cloudfront.net/ffa9dc6d-5d1c-4475-923c-72934f3e8762/med.jpg", + :large "http://cloudfront.net/ffa9dc6d-5d1c-4475-923c-72934f3e8762/large.jpg"} + {:name "Chinatown Paleo Food Truck", :categories ["Paleo" "Food Truck"], :phone "415-583-4380", :id "aa9b5ce9-db74-470e-8573-f2faca24d546"} + {:service "twitter", :mentions ["@chinatown_paleo_food_truck"], :tags ["#paleo" "#food" "#truck"], :username "joe"}] + ["Lucky's Low-Carb Coffee House is a delicious and atmospheric place to take a date during winter." + {:small "http://cloudfront.net/256793eb-de7f-45c6-9143-9fde81134caf/small.jpg", + :medium "http://cloudfront.net/256793eb-de7f-45c6-9143-9fde81134caf/med.jpg", + :large "http://cloudfront.net/256793eb-de7f-45c6-9143-9fde81134caf/large.jpg"} + {:name "Lucky's Low-Carb Coffee House", :categories ["Low-Carb" "Coffee House"], :phone "415-145-7107", :id "81b0f944-f0ce-45e5-b84e-a924c441064a"} + {:service "flare", :username "tupac"}] + ["Sunset American Churros is a underground and acceptable place to have a drink in the spring." + {:small "http://cloudfront.net/d138162c-e3bc-4d50-b3bd-55bc9b599e9a/small.jpg", + :medium "http://cloudfront.net/d138162c-e3bc-4d50-b3bd-55bc9b599e9a/med.jpg", + :large "http://cloudfront.net/d138162c-e3bc-4d50-b3bd-55bc9b599e9a/large.jpg"} + {:name "Sunset American Churros", :categories ["American" "Churros"], :phone "415-191-5018", :id "2e88c921-29fb-489b-a956-d3ba1182da73"} + {:service "facebook", :facebook-photo-id "33e0c40f-43b2-4f59-8539-9ff952ce06c3", :url "http://facebook.com/photos/33e0c40f-43b2-4f59-8539-9ff952ce06c3"}] + ["Kyle's Chinese Restaurant is a historical and atmospheric place to watch the Warriors game in June." + {:small "http://cloudfront.net/267696df-7262-4dab-9c71-0346861f9a9c/small.jpg", + :medium "http://cloudfront.net/267696df-7262-4dab-9c71-0346861f9a9c/med.jpg", + :large "http://cloudfront.net/267696df-7262-4dab-9c71-0346861f9a9c/large.jpg"} + {:name "Kyle's Chinese Restaurant", :categories ["Chinese" "Restaurant"], :phone "415-298-9499", :id "de08b3c7-9929-40d8-8c20-dd9317613c17"} + {:service "facebook", :facebook-photo-id "36ecd1a2-0282-422c-995a-664668a4cb80", :url "http://facebook.com/photos/36ecd1a2-0282-422c-995a-664668a4cb80"}] + ["Polk St. Korean Taqueria is a overrated and popular place to watch the Warriors game with your pet dog." + {:small "http://cloudfront.net/a4d216fa-08b5-4f73-a236-eabf0b3e38e7/small.jpg", + :medium "http://cloudfront.net/a4d216fa-08b5-4f73-a236-eabf0b3e38e7/med.jpg", + :large "http://cloudfront.net/a4d216fa-08b5-4f73-a236-eabf0b3e38e7/large.jpg"} + {:name "Polk St. Korean Taqueria", :categories ["Korean" "Taqueria"], :phone "415-511-5531", :id "ddb09b32-6f0b-4e54-98c7-14398f16ca4e"} + {:service "flare", :username "amy"}] + ["Marina Japanese Liquor Store is a underappreciated and fantastic place to conduct a business meeting in June." + {:small "http://cloudfront.net/cd9e419f-75ef-404b-8637-7120d469b743/small.jpg", + :medium "http://cloudfront.net/cd9e419f-75ef-404b-8637-7120d469b743/med.jpg", + :large "http://cloudfront.net/cd9e419f-75ef-404b-8637-7120d469b743/large.jpg"} + {:name "Marina Japanese Liquor Store", :categories ["Japanese" "Liquor Store"], :phone "415-587-9819", :id "08fd0138-35dd-41b0-836d-1c652e95ffcd"} + {:service "twitter", :mentions ["@marina_japanese_liquor_store"], :tags ["#japanese" "#liquor" "#store"], :username "jane"}] + ["Polk St. Red White & Blue Café is a underground and horrible place to nurse a hangover on Taco Tuesday." + {:small "http://cloudfront.net/9629743a-e19f-4443-966e-b3cede3cce45/small.jpg", + :medium "http://cloudfront.net/9629743a-e19f-4443-966e-b3cede3cce45/med.jpg", + :large "http://cloudfront.net/9629743a-e19f-4443-966e-b3cede3cce45/large.jpg"} + {:name "Polk St. Red White & Blue Café", :categories ["Red White & Blue" "Café"], :phone "415-986-0661", :id "6eb9d2db-5015-49f0-bd18-f4c4938b7e5a"} + {:service "facebook", :facebook-photo-id "54f31306-b4ce-46f6-b30e-37dc2f10fc18", :url "http://facebook.com/photos/54f31306-b4ce-46f6-b30e-37dc2f10fc18"}] + ["SoMa Old-Fashioned Pizzeria is a groovy and amazing place to have brunch with your pet toucan." + {:small "http://cloudfront.net/a29570eb-e53f-4ddd-ab61-747cf6709515/small.jpg", + :medium "http://cloudfront.net/a29570eb-e53f-4ddd-ab61-747cf6709515/med.jpg", + :large "http://cloudfront.net/a29570eb-e53f-4ddd-ab61-747cf6709515/large.jpg"} + {:name "SoMa Old-Fashioned Pizzeria", :categories ["Old-Fashioned" "Pizzeria"], :phone "415-966-8856", :id "deb8997b-734d-402b-a181-bd888214bc86"} + {:service "yelp", :yelp-photo-id "c8f35c36-0f19-4318-a0e4-88d96b5b72eb", :categories ["Old-Fashioned" "Pizzeria"]}] + ["Haight Soul Food Hotel & Restaurant is a popular and modern place to take visiting friends and relatives weekday afternoons." + {:small "http://cloudfront.net/a0842810-2d69-48c3-ba37-941479473250/small.jpg", + :medium "http://cloudfront.net/a0842810-2d69-48c3-ba37-941479473250/med.jpg", + :large "http://cloudfront.net/a0842810-2d69-48c3-ba37-941479473250/large.jpg"} + {:name "Haight Soul Food Hotel & Restaurant", :categories ["Soul Food" "Hotel & Restaurant"], :phone "415-786-9541", :id "11a72eb3-9e96-4703-9a01-e8c2a9469046"} + {:service "yelp", :yelp-photo-id "d753f0de-f1db-4cf0-9260-ad3eeee4aa9c", :categories ["Soul Food" "Hotel & Restaurant"]}] + ["Marina Low-Carb Food Truck is a underappreciated and modern place to take a date with friends." + {:small "http://cloudfront.net/576cf625-4a9e-443a-b0df-ffb279f418f8/small.jpg", + :medium "http://cloudfront.net/576cf625-4a9e-443a-b0df-ffb279f418f8/med.jpg", + :large "http://cloudfront.net/576cf625-4a9e-443a-b0df-ffb279f418f8/large.jpg"} + {:name "Marina Low-Carb Food Truck", :categories ["Low-Carb" "Food Truck"], :phone "415-748-3513", :id "a13a5beb-19de-40ca-a334-02df3bdf5285"} + {:service "foursquare", :foursquare-photo-id "a5c87b6a-a1e6-4abe-932f-90dde0e8125b", :mayor "tupac"}] + ["Mission Soul Food Pizzeria is a amazing and world-famous place to have a after-work cocktail in the spring." + {:small "http://cloudfront.net/171695dd-4ad1-4c50-9056-27d6d80d524a/small.jpg", + :medium "http://cloudfront.net/171695dd-4ad1-4c50-9056-27d6d80d524a/med.jpg", + :large "http://cloudfront.net/171695dd-4ad1-4c50-9056-27d6d80d524a/large.jpg"} + {:name "Mission Soul Food Pizzeria", :categories ["Soul Food" "Pizzeria"], :phone "415-437-3479", :id "9905fe61-44cb-4626-843b-5d725c7949bb"} + {:service "yelp", :yelp-photo-id "728c253f-8576-4e91-a0f7-8f4409ef3ea3", :categories ["Soul Food" "Pizzeria"]}] + ["Cam's Old-Fashioned Coffee House is a delicious and overrated place to have a birthday party weekend mornings." + {:small "http://cloudfront.net/8270c1f4-e118-4d40-a5f1-f2257c99b3ab/small.jpg", + :medium "http://cloudfront.net/8270c1f4-e118-4d40-a5f1-f2257c99b3ab/med.jpg", + :large "http://cloudfront.net/8270c1f4-e118-4d40-a5f1-f2257c99b3ab/large.jpg"} + {:name "Cam's Old-Fashioned Coffee House", :categories ["Old-Fashioned" "Coffee House"], :phone "415-871-9473", :id "5d1f918e-ef00-40de-b6e4-3f9f34ee8cd4"} + {:service "twitter", :mentions ["@cams_old_fashioned_coffee_house"], :tags ["#old-fashioned" "#coffee" "#house"], :username "tupac"}] + ["Haight European Grill is a family-friendly and wonderful place to sip Champagne on Saturday night." + {:small "http://cloudfront.net/1b7745d8-0082-495f-a767-589bfd31dc04/small.jpg", + :medium "http://cloudfront.net/1b7745d8-0082-495f-a767-589bfd31dc04/med.jpg", + :large "http://cloudfront.net/1b7745d8-0082-495f-a767-589bfd31dc04/large.jpg"} + {:name "Haight European Grill", :categories ["European" "Grill"], :phone "415-191-2778", :id "7e6281f7-5b17-4056-ada0-85453247bc8f"} + {:service "twitter", :mentions ["@haight_european_grill"], :tags ["#european" "#grill"], :username "lucky_pigeon"}] + ["Polk St. Deep-Dish Hotel & Restaurant is a world-famous and popular place to sip Champagne with your pet dog." + {:small "http://cloudfront.net/d81ae9d6-676c-45c5-add1-f0c007db6de5/small.jpg", + :medium "http://cloudfront.net/d81ae9d6-676c-45c5-add1-f0c007db6de5/med.jpg", + :large "http://cloudfront.net/d81ae9d6-676c-45c5-add1-f0c007db6de5/large.jpg"} + {:name "Polk St. Deep-Dish Hotel & Restaurant", :categories ["Deep-Dish" "Hotel & Restaurant"], :phone "415-666-8681", :id "47f1698e-ae11-46f5-818b-85a59d0affba"} + {:service "flare", :username "mandy"}] + ["SF Afgan Restaurant is a horrible and delicious place to sip a glass of expensive wine the first Sunday of the month." + {:small "http://cloudfront.net/77737d07-f201-4898-90b9-72b7d4c73de9/small.jpg", + :medium "http://cloudfront.net/77737d07-f201-4898-90b9-72b7d4c73de9/med.jpg", + :large "http://cloudfront.net/77737d07-f201-4898-90b9-72b7d4c73de9/large.jpg"} + {:name "SF Afgan Restaurant", :categories ["Afgan" "Restaurant"], :phone "415-451-4697", :id "66ccc68a-db9a-470c-a17b-7764d23daced"} + {:service "twitter", :mentions ["@sf_afgan_restaurant"], :tags ["#afgan" "#restaurant"], :username "rasta_toucan"}] + ["Tenderloin Gluten-Free Bar & Grill is a exclusive and wonderful place to meet new friends on Thursdays." + {:small "http://cloudfront.net/48526d8c-934d-4f24-b31e-ce1bc15bc734/small.jpg", + :medium "http://cloudfront.net/48526d8c-934d-4f24-b31e-ce1bc15bc734/med.jpg", + :large "http://cloudfront.net/48526d8c-934d-4f24-b31e-ce1bc15bc734/large.jpg"} + {:name "Tenderloin Gluten-Free Bar & Grill", :categories ["Gluten-Free" "Bar & Grill"], :phone "415-904-0956", :id "0d7e235a-eea8-45b3-aaa7-23b4ea2b50f2"} + {:service "facebook", :facebook-photo-id "f073cb47-002c-4db7-b80a-c5ed327d0ac9", :url "http://facebook.com/photos/f073cb47-002c-4db7-b80a-c5ed327d0ac9"}] + ["Pacific Heights Irish Grill is a overrated and popular place to catch a bite to eat during winter." + {:small "http://cloudfront.net/c01dad01-edba-4413-b723-52d87a587f2d/small.jpg", + :medium "http://cloudfront.net/c01dad01-edba-4413-b723-52d87a587f2d/med.jpg", + :large "http://cloudfront.net/c01dad01-edba-4413-b723-52d87a587f2d/large.jpg"} + {:name "Pacific Heights Irish Grill", :categories ["Irish" "Grill"], :phone "415-491-2202", :id "d6b92dfc-56e9-4f65-b1d0-595f120043d9"} + {:service "flare", :username "lucky_pigeon"}] + ["Haight Soul Food Café is a horrible and underground place to take visiting friends and relatives on Thursdays." + {:small "http://cloudfront.net/f3bd2564-9a67-4712-86e7-5c1abe816bd0/small.jpg", + :medium "http://cloudfront.net/f3bd2564-9a67-4712-86e7-5c1abe816bd0/med.jpg", + :large "http://cloudfront.net/f3bd2564-9a67-4712-86e7-5c1abe816bd0/large.jpg"} + {:name "Haight Soul Food Café", :categories ["Soul Food" "Café"], :phone "415-257-1769", :id "a1796c4b-da2b-474f-9fd6-4fa96c1eac70"} + {:service "flare", :username "jane"}] + ["Marina Cage-Free Liquor Store is a delicious and family-friendly place to watch the Warriors game after baseball games." + {:small "http://cloudfront.net/ea520d10-a490-416c-a603-058a212a3e0c/small.jpg", + :medium "http://cloudfront.net/ea520d10-a490-416c-a603-058a212a3e0c/med.jpg", + :large "http://cloudfront.net/ea520d10-a490-416c-a603-058a212a3e0c/large.jpg"} + {:name "Marina Cage-Free Liquor Store", :categories ["Cage-Free" "Liquor Store"], :phone "415-571-0783", :id "ad68f549-3000-407e-ab98-7f314cfa4653"} + {:service "twitter", :mentions ["@marina_cage_free_liquor_store"], :tags ["#cage-free" "#liquor" "#store"], :username "bob"}] + ["Lower Pac Heights Deep-Dish Ice Cream Truck is a classic and underground place to take a date with friends." + {:small "http://cloudfront.net/6a960d66-43a5-4053-b5d6-b03ab59a263b/small.jpg", + :medium "http://cloudfront.net/6a960d66-43a5-4053-b5d6-b03ab59a263b/med.jpg", + :large "http://cloudfront.net/6a960d66-43a5-4053-b5d6-b03ab59a263b/large.jpg"} + {:name "Lower Pac Heights Deep-Dish Ice Cream Truck", :categories ["Deep-Dish" "Ice Cream Truck"], :phone "415-495-1414", :id "d5efa2f2-496d-41b2-8b85-3d002a22a2bc"} + {:service "flare", :username "rasta_toucan"}] + ["SoMa Old-Fashioned Pizzeria is a swell and acceptable place to sip a glass of expensive wine on a Tuesday afternoon." + {:small "http://cloudfront.net/a04b6844-2caf-4303-9921-e750208f79ee/small.jpg", + :medium "http://cloudfront.net/a04b6844-2caf-4303-9921-e750208f79ee/med.jpg", + :large "http://cloudfront.net/a04b6844-2caf-4303-9921-e750208f79ee/large.jpg"} + {:name "SoMa Old-Fashioned Pizzeria", :categories ["Old-Fashioned" "Pizzeria"], :phone "415-966-8856", :id "deb8997b-734d-402b-a181-bd888214bc86"} + {:service "twitter", :mentions ["@soma_old_fashioned_pizzeria"], :tags ["#old-fashioned" "#pizzeria"], :username "biggie"}] + ["SF British Pop-Up Food Stand is a swell and popular place to people-watch on Thursdays." + {:small "http://cloudfront.net/fc488143-5e29-44be-9dc4-2b7a3653c3b3/small.jpg", + :medium "http://cloudfront.net/fc488143-5e29-44be-9dc4-2b7a3653c3b3/med.jpg", + :large "http://cloudfront.net/fc488143-5e29-44be-9dc4-2b7a3653c3b3/large.jpg"} + {:name "SF British Pop-Up Food Stand", :categories ["British" "Pop-Up Food Stand"], :phone "415-441-3725", :id "19eac087-7b1c-4668-a26c-d7c02cbcd3f6"} + {:service "facebook", :facebook-photo-id "98c2e661-c20e-41df-967a-b65635e34031", :url "http://facebook.com/photos/98c2e661-c20e-41df-967a-b65635e34031"}] + ["Lower Pac Heights Cage-Free Coffee House is a classic and fantastic place to take visiting friends and relatives with your pet dog." + {:small "http://cloudfront.net/9b7e4812-949c-4b68-8ff5-312e6bf143fb/small.jpg", + :medium "http://cloudfront.net/9b7e4812-949c-4b68-8ff5-312e6bf143fb/med.jpg", + :large "http://cloudfront.net/9b7e4812-949c-4b68-8ff5-312e6bf143fb/large.jpg"} + {:name "Lower Pac Heights Cage-Free Coffee House", :categories ["Cage-Free" "Coffee House"], :phone "415-697-9309", :id "02b1f618-41a0-406b-96dd-1a017f630b81"} + {:service "flare", :username "rasta_toucan"}] + ["Haight Mexican Restaurant is a amazing and underappreciated place to nurse a hangover weekday afternoons." + {:small "http://cloudfront.net/df889bbe-152e-483f-9599-a6e12ae821a7/small.jpg", + :medium "http://cloudfront.net/df889bbe-152e-483f-9599-a6e12ae821a7/med.jpg", + :large "http://cloudfront.net/df889bbe-152e-483f-9599-a6e12ae821a7/large.jpg"} + {:name "Haight Mexican Restaurant", :categories ["Mexican" "Restaurant"], :phone "415-758-8690", :id "a5e3f0ac-f6e8-4e71-a0a2-3f10f48b4ab1"} + {:service "twitter", :mentions ["@haight_mexican_restaurant"], :tags ["#mexican" "#restaurant"], :username "rasta_toucan"}] + ["Cam's Old-Fashioned Coffee House is a delicious and great place to pitch an investor when hungover." + {:small "http://cloudfront.net/80c44503-734b-4aac-8659-1d90ddd579ab/small.jpg", + :medium "http://cloudfront.net/80c44503-734b-4aac-8659-1d90ddd579ab/med.jpg", + :large "http://cloudfront.net/80c44503-734b-4aac-8659-1d90ddd579ab/large.jpg"} + {:name "Cam's Old-Fashioned Coffee House", :categories ["Old-Fashioned" "Coffee House"], :phone "415-868-2973", :id "27592c2b-e682-44bb-be28-8e9a622becca"} + {:service "facebook", :facebook-photo-id "81a3711b-d0c6-4100-b3f7-b18f67613a09", :url "http://facebook.com/photos/81a3711b-d0c6-4100-b3f7-b18f67613a09"}] + ["Tenderloin Gormet Restaurant is a modern and decent place to have a after-work cocktail during winter." + {:small "http://cloudfront.net/cf1833eb-476c-45fc-8eb5-68fd009f0871/small.jpg", + :medium "http://cloudfront.net/cf1833eb-476c-45fc-8eb5-68fd009f0871/med.jpg", + :large "http://cloudfront.net/cf1833eb-476c-45fc-8eb5-68fd009f0871/large.jpg"} + {:name "Tenderloin Gormet Restaurant", :categories ["Gormet" "Restaurant"], :phone "415-127-4197", :id "54a9eac8-d80d-4af8-b6d7-34651a60e59c"} + {:service "flare", :username "joe"}] + ["Chinatown Paleo Food Truck is a classic and underground place to watch the Giants game in the fall." + {:small "http://cloudfront.net/9432fe46-24be-40e9-a0b5-8824149712fd/small.jpg", + :medium "http://cloudfront.net/9432fe46-24be-40e9-a0b5-8824149712fd/med.jpg", + :large "http://cloudfront.net/9432fe46-24be-40e9-a0b5-8824149712fd/large.jpg"} + {:name "Chinatown Paleo Food Truck", :categories ["Paleo" "Food Truck"], :phone "415-583-4380", :id "aa9b5ce9-db74-470e-8573-f2faca24d546"} + {:service "twitter", :mentions ["@chinatown_paleo_food_truck"], :tags ["#paleo" "#food" "#truck"], :username "joe"}] + ["Haight Soul Food Hotel & Restaurant is a family-friendly and amazing place to watch the Warriors game in June." + {:small "http://cloudfront.net/2fff12e5-6582-4c35-8fed-4bae3f61acc1/small.jpg", + :medium "http://cloudfront.net/2fff12e5-6582-4c35-8fed-4bae3f61acc1/med.jpg", + :large "http://cloudfront.net/2fff12e5-6582-4c35-8fed-4bae3f61acc1/large.jpg"} + {:name "Haight Soul Food Hotel & Restaurant", :categories ["Soul Food" "Hotel & Restaurant"], :phone "415-786-9541", :id "11a72eb3-9e96-4703-9a01-e8c2a9469046"} + {:service "twitter", :mentions ["@haight_soul_food_hotel_&_restaurant"], :tags ["#soul" "#food" "#hotel" "#&" "#restaurant"], :username "bob"}] + ["Haight Soul Food Pop-Up Food Stand is a decent and underground place to conduct a business meeting in the spring." + {:small "http://cloudfront.net/6509339f-e90f-4961-9041-25984c0068e1/small.jpg", + :medium "http://cloudfront.net/6509339f-e90f-4961-9041-25984c0068e1/med.jpg", + :large "http://cloudfront.net/6509339f-e90f-4961-9041-25984c0068e1/large.jpg"} + {:name "Haight Soul Food Pop-Up Food Stand", :categories ["Soul Food" "Pop-Up Food Stand"], :phone "415-741-8726", :id "9735184b-1299-410f-a98e-10d9c548af42"} + {:service "flare", :username "kyle"}] + ["Joe's Modern Coffee House is a underappreciated and delicious place to take a date in July." + {:small "http://cloudfront.net/1402d2a6-e94f-43dc-b66c-22621bfc0709/small.jpg", + :medium "http://cloudfront.net/1402d2a6-e94f-43dc-b66c-22621bfc0709/med.jpg", + :large "http://cloudfront.net/1402d2a6-e94f-43dc-b66c-22621bfc0709/large.jpg"} + {:name "Joe's Modern Coffee House", :categories ["Modern" "Coffee House"], :phone "415-331-5269", :id "cd9e3610-9ead-4f0b-ad93-cd53611d49fe"} + {:service "foursquare", :foursquare-photo-id "7d0faa6d-0d94-4920-894a-8da7537839a7", :mayor "bob"}] + ["Polk St. Mexican Coffee House is a popular and historical place to catch a bite to eat with your pet toucan." + {:small "http://cloudfront.net/a8c99e30-dc02-456c-b752-91702acb84c5/small.jpg", + :medium "http://cloudfront.net/a8c99e30-dc02-456c-b752-91702acb84c5/med.jpg", + :large "http://cloudfront.net/a8c99e30-dc02-456c-b752-91702acb84c5/large.jpg"} + {:name "Polk St. Mexican Coffee House", :categories ["Mexican" "Coffee House"], :phone "415-144-7901", :id "396d36d7-13ad-41fd-86b5-8b70b6ecdabf"} + {:service "flare", :username "biggie"}] + ["Marina No-MSG Sushi is a overrated and overrated place to pitch an investor Friday nights." + {:small "http://cloudfront.net/c14d1b58-b607-4a3b-86c0-331e6b965534/small.jpg", + :medium "http://cloudfront.net/c14d1b58-b607-4a3b-86c0-331e6b965534/med.jpg", + :large "http://cloudfront.net/c14d1b58-b607-4a3b-86c0-331e6b965534/large.jpg"} + {:name "Marina No-MSG Sushi", :categories ["No-MSG" "Sushi"], :phone "415-856-5937", :id "d51013a3-8547-4705-a5f0-cb11d8206481"} + {:service "twitter", :mentions ["@marina_no_msg_sushi"], :tags ["#no-msg" "#sushi"], :username "rasta_toucan"}] + ["Sunset Deep-Dish Hotel & Restaurant is a horrible and world-famous place to catch a bite to eat Friday nights." + {:small "http://cloudfront.net/038a5a74-18a8-48f2-a771-6a32c9c57d98/small.jpg", + :medium "http://cloudfront.net/038a5a74-18a8-48f2-a771-6a32c9c57d98/med.jpg", + :large "http://cloudfront.net/038a5a74-18a8-48f2-a771-6a32c9c57d98/large.jpg"} + {:name "Sunset Deep-Dish Hotel & Restaurant", :categories ["Deep-Dish" "Hotel & Restaurant"], :phone "415-332-0978", :id "a80745c7-af74-4579-8932-70dd488269e6"} + {:service "twitter", :mentions ["@sunset_deep_dish_hotel_&_restaurant"], :tags ["#deep-dish" "#hotel" "#&" "#restaurant"], :username "rasta_toucan"}] + ["Pacific Heights Free-Range Eatery is a wonderful and modern place to take visiting friends and relatives Friday nights." + {:small "http://cloudfront.net/cedd4221-dbdb-46c3-95a9-935cce6b3fe5/small.jpg", + :medium "http://cloudfront.net/cedd4221-dbdb-46c3-95a9-935cce6b3fe5/med.jpg", + :large "http://cloudfront.net/cedd4221-dbdb-46c3-95a9-935cce6b3fe5/large.jpg"} + {:name "Pacific Heights Free-Range Eatery", :categories ["Free-Range" "Eatery"], :phone "415-901-6541", :id "88b361c8-ce69-4b2e-b0f2-9deedd574af6"} + {:service "twitter", :mentions ["@pacific_heights_free_range_eatery"], :tags ["#free-range" "#eatery"], :username "kyle"}] + ["Lucky's Deep-Dish Gastro Pub is a decent and delicious place to watch the Giants game on Taco Tuesday." + {:small "http://cloudfront.net/a9b4f7f8-637b-4b83-9881-78d3b7f417dc/small.jpg", + :medium "http://cloudfront.net/a9b4f7f8-637b-4b83-9881-78d3b7f417dc/med.jpg", + :large "http://cloudfront.net/a9b4f7f8-637b-4b83-9881-78d3b7f417dc/large.jpg"} + {:name "Lucky's Deep-Dish Gastro Pub", :categories ["Deep-Dish" "Gastro Pub"], :phone "415-487-4085", :id "0136c454-0968-41cd-a237-ceec5724cab8"} + {:service "facebook", :facebook-photo-id "dfad685c-f2a7-4ab4-ba45-a9d94919d8f6", :url "http://facebook.com/photos/dfad685c-f2a7-4ab4-ba45-a9d94919d8f6"}] + ["Polk St. Mexican Coffee House is a world-famous and horrible place to pitch an investor the second Saturday of the month." + {:small "http://cloudfront.net/cf7ce0ef-0ce6-4138-9f33-a13de2a6c752/small.jpg", + :medium "http://cloudfront.net/cf7ce0ef-0ce6-4138-9f33-a13de2a6c752/med.jpg", + :large "http://cloudfront.net/cf7ce0ef-0ce6-4138-9f33-a13de2a6c752/large.jpg"} + {:name "Polk St. Mexican Coffee House", :categories ["Mexican" "Coffee House"], :phone "415-144-7901", :id "396d36d7-13ad-41fd-86b5-8b70b6ecdabf"} + {:service "twitter", :mentions ["@polk_st._mexican_coffee_house"], :tags ["#mexican" "#coffee" "#house"], :username "bob"}] + ["Pacific Heights Pizza Bakery is a acceptable and fantastic place to have a after-work cocktail after baseball games." + {:small "http://cloudfront.net/71ec461a-21c1-4c41-9fe5-31e69dfd95f4/small.jpg", + :medium "http://cloudfront.net/71ec461a-21c1-4c41-9fe5-31e69dfd95f4/med.jpg", + :large "http://cloudfront.net/71ec461a-21c1-4c41-9fe5-31e69dfd95f4/large.jpg"} + {:name "Pacific Heights Pizza Bakery", :categories ["Pizza" "Bakery"], :phone "415-006-0149", :id "7fda37a5-810f-4902-b571-54afe583f0dd"} + {:service "yelp", :yelp-photo-id "a7da25d3-cf05-444f-9a1c-11541fdbdb78", :categories ["Pizza" "Bakery"]}] + ["SoMa Old-Fashioned Pizzeria is a underappreciated and groovy place to take a date with your pet dog." + {:small "http://cloudfront.net/03e3ec21-9842-4b57-a311-3c1ecf5716c3/small.jpg", + :medium "http://cloudfront.net/03e3ec21-9842-4b57-a311-3c1ecf5716c3/med.jpg", + :large "http://cloudfront.net/03e3ec21-9842-4b57-a311-3c1ecf5716c3/large.jpg"} + {:name "SoMa Old-Fashioned Pizzeria", :categories ["Old-Fashioned" "Pizzeria"], :phone "415-966-8856", :id "deb8997b-734d-402b-a181-bd888214bc86"} + {:service "flare", :username "lucky_pigeon"}] + ["Marina Homestyle Pop-Up Food Stand is a amazing and acceptable place to take visiting friends and relatives during winter." + {:small "http://cloudfront.net/2da70d7a-afe8-4708-84a4-8023b813194d/small.jpg", + :medium "http://cloudfront.net/2da70d7a-afe8-4708-84a4-8023b813194d/med.jpg", + :large "http://cloudfront.net/2da70d7a-afe8-4708-84a4-8023b813194d/large.jpg"} + {:name "Marina Homestyle Pop-Up Food Stand", :categories ["Homestyle" "Pop-Up Food Stand"], :phone "415-094-4567", :id "88a7ae3c-8b36-4901-a0c5-b82342cba6cd"} + {:service "yelp", :yelp-photo-id "a031364b-8035-45ba-8948-3e3d42cd0bb1", :categories ["Homestyle" "Pop-Up Food Stand"]}] + ["Haight European Grill is a horrible and amazing place to have a birthday party during winter." + {:small "http://cloudfront.net/1dcef7de-a1c4-405b-a9e1-69c92d686ef1/small.jpg", + :medium "http://cloudfront.net/1dcef7de-a1c4-405b-a9e1-69c92d686ef1/med.jpg", + :large "http://cloudfront.net/1dcef7de-a1c4-405b-a9e1-69c92d686ef1/large.jpg"} + {:name "Haight European Grill", :categories ["European" "Grill"], :phone "415-191-2778", :id "7e6281f7-5b17-4056-ada0-85453247bc8f"} + {:service "twitter", :mentions ["@haight_european_grill"], :tags ["#european" "#grill"], :username "kyle"}] + ["SoMa Japanese Churros is a horrible and overrated place to people-watch during winter." + {:small "http://cloudfront.net/f49ffbdc-9f8b-4191-ad5c-a6d32a709fec/small.jpg", + :medium "http://cloudfront.net/f49ffbdc-9f8b-4191-ad5c-a6d32a709fec/med.jpg", + :large "http://cloudfront.net/f49ffbdc-9f8b-4191-ad5c-a6d32a709fec/large.jpg"} + {:name "SoMa Japanese Churros", :categories ["Japanese" "Churros"], :phone "415-404-1510", :id "373858b2-e634-45d0-973d-4d0fed8c438b"} + {:service "facebook", :facebook-photo-id "d9c67f4f-f651-4e29-91bf-fe8a13c51a42", :url "http://facebook.com/photos/d9c67f4f-f651-4e29-91bf-fe8a13c51a42"}] + ["Marina Japanese Liquor Store is a wonderful and historical place to people-watch in July." + {:small "http://cloudfront.net/b6595a58-aa5a-4653-a582-321a0499af2c/small.jpg", + :medium "http://cloudfront.net/b6595a58-aa5a-4653-a582-321a0499af2c/med.jpg", + :large "http://cloudfront.net/b6595a58-aa5a-4653-a582-321a0499af2c/large.jpg"} + {:name "Marina Japanese Liquor Store", :categories ["Japanese" "Liquor Store"], :phone "415-587-9819", :id "08fd0138-35dd-41b0-836d-1c652e95ffcd"} + {:service "yelp", :yelp-photo-id "a09b2b6a-2cc4-4c75-ad94-bc4b255f2609", :categories ["Japanese" "Liquor Store"]}] + ["Nob Hill Gluten-Free Coffee House is a popular and historical place to take visiting friends and relatives weekend mornings." + {:small "http://cloudfront.net/f42279fb-7c4d-4dc6-8788-04be94c68b67/small.jpg", + :medium "http://cloudfront.net/f42279fb-7c4d-4dc6-8788-04be94c68b67/med.jpg", + :large "http://cloudfront.net/f42279fb-7c4d-4dc6-8788-04be94c68b67/large.jpg"} + {:name "Nob Hill Gluten-Free Coffee House", :categories ["Gluten-Free" "Coffee House"], :phone "415-605-9554", :id "df57ff6d-1b5f-46da-b292-32321c6b1a7e"} + {:service "foursquare", :foursquare-photo-id "e11de3d3-8166-424c-91a4-857d3abb8487", :mayor "kyle"}] + ["Polk St. Deep-Dish Hotel & Restaurant is a family-friendly and amazing place to take visiting friends and relatives in the spring." + {:small "http://cloudfront.net/a938a596-27ad-4d83-bc32-d5113d44bc56/small.jpg", + :medium "http://cloudfront.net/a938a596-27ad-4d83-bc32-d5113d44bc56/med.jpg", + :large "http://cloudfront.net/a938a596-27ad-4d83-bc32-d5113d44bc56/large.jpg"} + {:name "Polk St. Deep-Dish Hotel & Restaurant", :categories ["Deep-Dish" "Hotel & Restaurant"], :phone "415-666-8681", :id "47f1698e-ae11-46f5-818b-85a59d0affba"} + {:service "yelp", :yelp-photo-id "101ea39b-5bc5-4bb7-905e-017f1a8271c8", :categories ["Deep-Dish" "Hotel & Restaurant"]}] + ["Joe's No-MSG Sushi is a underappreciated and family-friendly place to people-watch weekend evenings." + {:small "http://cloudfront.net/5469697b-2b07-46ab-b9ef-3bfa449e7db5/small.jpg", + :medium "http://cloudfront.net/5469697b-2b07-46ab-b9ef-3bfa449e7db5/med.jpg", + :large "http://cloudfront.net/5469697b-2b07-46ab-b9ef-3bfa449e7db5/large.jpg"} + {:name "Joe's No-MSG Sushi", :categories ["No-MSG" "Sushi"], :phone "415-739-8157", :id "9ff21570-cd5b-415e-933a-52144f551b86"} + {:service "flare", :username "sameer"}] + ["Mission Soul Food Pizzeria is a acceptable and historical place to pitch an investor during winter." + {:small "http://cloudfront.net/38325411-8a54-4a10-930f-0c1a439d0f78/small.jpg", + :medium "http://cloudfront.net/38325411-8a54-4a10-930f-0c1a439d0f78/med.jpg", + :large "http://cloudfront.net/38325411-8a54-4a10-930f-0c1a439d0f78/large.jpg"} + {:name "Mission Soul Food Pizzeria", :categories ["Soul Food" "Pizzeria"], :phone "415-437-3479", :id "9905fe61-44cb-4626-843b-5d725c7949bb"} + {:service "yelp", :yelp-photo-id "22c576f1-d64a-44c1-a510-85ed03cd8a9c", :categories ["Soul Food" "Pizzeria"]}] + ["Joe's Homestyle Eatery is a popular and underappreciated place to drink a craft beer with your pet toucan." + {:small "http://cloudfront.net/9d0c77a6-ac3a-4221-8125-c670c48a963a/small.jpg", + :medium "http://cloudfront.net/9d0c77a6-ac3a-4221-8125-c670c48a963a/med.jpg", + :large "http://cloudfront.net/9d0c77a6-ac3a-4221-8125-c670c48a963a/large.jpg"} + {:name "Joe's Homestyle Eatery", :categories ["Homestyle" "Eatery"], :phone "415-950-1337", :id "5cc18489-dfaf-417b-900f-5d1d61b961e8"} + {:service "facebook", :facebook-photo-id "3e5b3749-a758-4acf-b87e-aadca225127c", :url "http://facebook.com/photos/3e5b3749-a758-4acf-b87e-aadca225127c"}] + ["Market St. Gluten-Free Café is a family-friendly and family-friendly place to have a after-work cocktail when hungover." + {:small "http://cloudfront.net/f1891933-8d88-4e80-92c3-76b0a10c6b45/small.jpg", + :medium "http://cloudfront.net/f1891933-8d88-4e80-92c3-76b0a10c6b45/med.jpg", + :large "http://cloudfront.net/f1891933-8d88-4e80-92c3-76b0a10c6b45/large.jpg"} + {:name "Market St. Gluten-Free Café", :categories ["Gluten-Free" "Café"], :phone "415-697-9776", :id "ce4947d0-071a-4491-b5ac-f8b0241bd54c"} + {:service "flare", :username "amy"}] + ["Tenderloin Cage-Free Sushi is a swell and classic place to nurse a hangover with your pet toucan." + {:small "http://cloudfront.net/325091a5-4a50-45bd-9566-654940a8932c/small.jpg", + :medium "http://cloudfront.net/325091a5-4a50-45bd-9566-654940a8932c/med.jpg", + :large "http://cloudfront.net/325091a5-4a50-45bd-9566-654940a8932c/large.jpg"} + {:name "Tenderloin Cage-Free Sushi", :categories ["Cage-Free" "Sushi"], :phone "415-348-0644", :id "0b6c036f-82b0-4008-bdfe-5360dd93fb75"} + {:service "twitter", :mentions ["@tenderloin_cage_free_sushi"], :tags ["#cage-free" "#sushi"], :username "joe"}] + ["Mission Chinese Liquor Store is a family-friendly and great place to take visiting friends and relatives weekday afternoons." + {:small "http://cloudfront.net/1b313721-9de8-4f91-afe2-659873693387/small.jpg", + :medium "http://cloudfront.net/1b313721-9de8-4f91-afe2-659873693387/med.jpg", + :large "http://cloudfront.net/1b313721-9de8-4f91-afe2-659873693387/large.jpg"} + {:name "Mission Chinese Liquor Store", :categories ["Chinese" "Liquor Store"], :phone "415-906-6919", :id "00132b5b-31fc-46f0-a288-f547f23477ee"} + {:service "twitter", :mentions ["@mission_chinese_liquor_store"], :tags ["#chinese" "#liquor" "#store"], :username "jane"}] + ["Lucky's Low-Carb Coffee House is a great and decent place to conduct a business meeting when hungover." + {:small "http://cloudfront.net/e9e3efe7-1b9a-48c7-85d6-92a1322960b8/small.jpg", + :medium "http://cloudfront.net/e9e3efe7-1b9a-48c7-85d6-92a1322960b8/med.jpg", + :large "http://cloudfront.net/e9e3efe7-1b9a-48c7-85d6-92a1322960b8/large.jpg"} + {:name "Lucky's Low-Carb Coffee House", :categories ["Low-Carb" "Coffee House"], :phone "415-145-7107", :id "81b0f944-f0ce-45e5-b84e-a924c441064a"} + {:service "foursquare", :foursquare-photo-id "f7b66a97-8c94-4754-8938-6b4e3244b08e", :mayor "jane"}] + ["Lower Pac Heights Cage-Free Coffee House is a exclusive and fantastic place to take visiting friends and relatives with your pet dog." + {:small "http://cloudfront.net/b3321e36-0ffa-40be-bba4-f8ada008c0f0/small.jpg", + :medium "http://cloudfront.net/b3321e36-0ffa-40be-bba4-f8ada008c0f0/med.jpg", + :large "http://cloudfront.net/b3321e36-0ffa-40be-bba4-f8ada008c0f0/large.jpg"} + {:name "Lower Pac Heights Cage-Free Coffee House", :categories ["Cage-Free" "Coffee House"], :phone "415-697-9309", :id "02b1f618-41a0-406b-96dd-1a017f630b81"} + {:service "flare", :username "jane"}] + ["SoMa TaquerÃa Diner is a overrated and amazing place to have breakfast in June." + {:small "http://cloudfront.net/36b82a08-66c3-4c94-a76d-0026653fadf0/small.jpg", + :medium "http://cloudfront.net/36b82a08-66c3-4c94-a76d-0026653fadf0/med.jpg", + :large "http://cloudfront.net/36b82a08-66c3-4c94-a76d-0026653fadf0/large.jpg"} + {:name "SoMa TaquerÃa Diner", :categories ["TaquerÃa" "Diner"], :phone "415-947-9521", :id "f97ede4a-074f-4e24-babc-5c44f2be9c36"} + {:service "yelp", :yelp-photo-id "cf483e10-dd11-4611-90e0-bc0238b41b59", :categories ["TaquerÃa" "Diner"]}] + ["Cam's Mexican Gastro Pub is a swell and world-famous place to take visiting friends and relatives Friday nights." + {:small "http://cloudfront.net/c4414384-985d-4539-b6e5-1758bd4ec73a/small.jpg", + :medium "http://cloudfront.net/c4414384-985d-4539-b6e5-1758bd4ec73a/med.jpg", + :large "http://cloudfront.net/c4414384-985d-4539-b6e5-1758bd4ec73a/large.jpg"} + {:name "Cam's Mexican Gastro Pub", :categories ["Mexican" "Gastro Pub"], :phone "415-320-9123", :id "bb958ac5-758e-4f42-b984-6b0e13f25194"} + {:service "yelp", :yelp-photo-id "33a70c42-6c5a-4a29-af94-b7856cbc6cbb", :categories ["Mexican" "Gastro Pub"]}] + ["Alcatraz Pizza Churros is a horrible and underground place to sip a glass of expensive wine during winter." + {:small "http://cloudfront.net/a0d56ff5-9a10-4b98-bdcf-152d45019943/small.jpg", + :medium "http://cloudfront.net/a0d56ff5-9a10-4b98-bdcf-152d45019943/med.jpg", + :large "http://cloudfront.net/a0d56ff5-9a10-4b98-bdcf-152d45019943/large.jpg"} + {:name "Alcatraz Pizza Churros", :categories ["Pizza" "Churros"], :phone "415-754-7867", :id "df95e4f1-8719-42af-a15d-3ee00de6e04f"} + {:service "yelp", :yelp-photo-id "e6a2e47f-07b6-4ec9-a491-371fb1959975", :categories ["Pizza" "Churros"]}] + ["Marina Low-Carb Food Truck is a groovy and delicious place to watch the Giants game on Thursdays." + {:small "http://cloudfront.net/91c8de79-39fa-41b8-8022-fdfef267bb71/small.jpg", + :medium "http://cloudfront.net/91c8de79-39fa-41b8-8022-fdfef267bb71/med.jpg", + :large "http://cloudfront.net/91c8de79-39fa-41b8-8022-fdfef267bb71/large.jpg"} + {:name "Marina Low-Carb Food Truck", :categories ["Low-Carb" "Food Truck"], :phone "415-748-3513", :id "a13a5beb-19de-40ca-a334-02df3bdf5285"} + {:service "flare", :username "tupac"}] + ["Rasta's Paleo Churros is a acceptable and family-friendly place to have brunch weekday afternoons." + {:small "http://cloudfront.net/b3866479-4dfb-48e8-97fa-058d125e36e7/small.jpg", + :medium "http://cloudfront.net/b3866479-4dfb-48e8-97fa-058d125e36e7/med.jpg", + :large "http://cloudfront.net/b3866479-4dfb-48e8-97fa-058d125e36e7/large.jpg"} + {:name "Rasta's Paleo Churros", :categories ["Paleo" "Churros"], :phone "415-915-0309", :id "3bf48ec6-434b-43b1-be28-7644975ecaf9"} + {:service "facebook", :facebook-photo-id "0b3c791c-935c-4d01-84f8-9708c699eb1b", :url "http://facebook.com/photos/0b3c791c-935c-4d01-84f8-9708c699eb1b"}] + ["Tenderloin Red White & Blue Pizzeria is a swell and historical place to nurse a hangover Friday nights." + {:small "http://cloudfront.net/99dce602-9ba9-4a3c-b9c3-c86ef6f9bcfa/small.jpg", + :medium "http://cloudfront.net/99dce602-9ba9-4a3c-b9c3-c86ef6f9bcfa/med.jpg", + :large "http://cloudfront.net/99dce602-9ba9-4a3c-b9c3-c86ef6f9bcfa/large.jpg"} + {:name "Tenderloin Red White & Blue Pizzeria", :categories ["Red White & Blue" "Pizzeria"], :phone "415-719-8143", :id "eba3dbcd-100a-4f38-a701-e0dec157f437"} + {:service "yelp", :yelp-photo-id "c6aa321a-e8f7-484b-9fd1-ab8f6a1c5906", :categories ["Red White & Blue" "Pizzeria"]}] + ["Kyle's Free-Range Taqueria is a amazing and wonderful place to have breakfast in the fall." + {:small "http://cloudfront.net/613f6695-adee-4175-ad07-1af6b7753fa7/small.jpg", + :medium "http://cloudfront.net/613f6695-adee-4175-ad07-1af6b7753fa7/med.jpg", + :large "http://cloudfront.net/613f6695-adee-4175-ad07-1af6b7753fa7/large.jpg"} + {:name "Kyle's Free-Range Taqueria", :categories ["Free-Range" "Taqueria"], :phone "415-201-7832", :id "7aeb9416-4fe6-45f9-8849-6b8ba6d3f3b9"} + {:service "flare", :username "amy"}] + ["Lucky's Cage-Free Liquor Store is a delicious and acceptable place to drink a craft beer on public holidays." + {:small "http://cloudfront.net/9311a1df-401c-4c89-b4ee-d399635fd558/small.jpg", + :medium "http://cloudfront.net/9311a1df-401c-4c89-b4ee-d399635fd558/med.jpg", + :large "http://cloudfront.net/9311a1df-401c-4c89-b4ee-d399635fd558/large.jpg"} + {:name "Lucky's Cage-Free Liquor Store", :categories ["Cage-Free" "Liquor Store"], :phone "415-341-3219", :id "968eac8d-1fd0-443d-a7ff-b48810ab0f69"} + {:service "foursquare", :foursquare-photo-id "ef083c69-08d6-45be-8cfb-c71654b0a8fc", :mayor "cam_saul"}] + ["Lucky's Gluten-Free Café is a groovy and wonderful place to have a drink with friends." + {:small "http://cloudfront.net/85d2ae70-0381-4c67-9d5f-97f8068e64df/small.jpg", + :medium "http://cloudfront.net/85d2ae70-0381-4c67-9d5f-97f8068e64df/med.jpg", + :large "http://cloudfront.net/85d2ae70-0381-4c67-9d5f-97f8068e64df/large.jpg"} + {:name "Lucky's Gluten-Free Café", :categories ["Gluten-Free" "Café"], :phone "415-740-2328", :id "379af987-ad40-4a93-88a6-0233e1c14649"} + {:service "twitter", :mentions ["@luckys_gluten_free_café"], :tags ["#gluten-free" "#café"], :username "biggie"}] + ["Marina Modern Sushi is a wonderful and exclusive place to watch the Warriors game the second Saturday of the month." + {:small "http://cloudfront.net/ad6ceaf6-a43a-4c35-8f6c-ecef429c7408/small.jpg", + :medium "http://cloudfront.net/ad6ceaf6-a43a-4c35-8f6c-ecef429c7408/med.jpg", + :large "http://cloudfront.net/ad6ceaf6-a43a-4c35-8f6c-ecef429c7408/large.jpg"} + {:name "Marina Modern Sushi", :categories ["Modern" "Sushi"], :phone "415-393-7672", :id "21807c63-ca4c-4468-9844-d0c2620fbdfc"} + {:service "flare", :username "kyle"}] + ["Pacific Heights Red White & Blue Bar & Grill is a decent and delicious place to watch the Giants game when hungover." + {:small "http://cloudfront.net/7ae8b782-4cf9-4efd-af2f-12f3d921e44a/small.jpg", + :medium "http://cloudfront.net/7ae8b782-4cf9-4efd-af2f-12f3d921e44a/med.jpg", + :large "http://cloudfront.net/7ae8b782-4cf9-4efd-af2f-12f3d921e44a/large.jpg"} + {:name "Pacific Heights Red White & Blue Bar & Grill", :categories ["Red White & Blue" "Bar & Grill"], :phone "415-208-2550", :id "c7547aa1-94c1-44bd-bf5a-8655e4698ed8"} + {:service "foursquare", :foursquare-photo-id "55c948c7-8d08-46e1-98e6-a03ca330795c", :mayor "mandy"}] + ["Joe's Modern Coffee House is a acceptable and exclusive place to catch a bite to eat during summer." + {:small "http://cloudfront.net/9f063ae2-9db6-490e-8098-bac5a030b5d4/small.jpg", + :medium "http://cloudfront.net/9f063ae2-9db6-490e-8098-bac5a030b5d4/med.jpg", + :large "http://cloudfront.net/9f063ae2-9db6-490e-8098-bac5a030b5d4/large.jpg"} + {:name "Joe's Modern Coffee House", :categories ["Modern" "Coffee House"], :phone "415-331-5269", :id "cd9e3610-9ead-4f0b-ad93-cd53611d49fe"} + {:service "flare", :username "kyle"}] + ["Haight Gormet Pizzeria is a historical and modern place to pitch an investor Friday nights." + {:small "http://cloudfront.net/1484e4b7-fe56-4bed-8598-c6558202adeb/small.jpg", + :medium "http://cloudfront.net/1484e4b7-fe56-4bed-8598-c6558202adeb/med.jpg", + :large "http://cloudfront.net/1484e4b7-fe56-4bed-8598-c6558202adeb/large.jpg"} + {:name "Haight Gormet Pizzeria", :categories ["Gormet" "Pizzeria"], :phone "415-869-2197", :id "0425bdd0-3f57-4108-80e3-78335327355a"} + {:service "flare", :username "cam_saul"}] + ["Cam's Mexican Gastro Pub is a world-famous and fantastic place to take a date in July." + {:small "http://cloudfront.net/7663dd1e-d8d3-4add-9cdd-e6b7322e622c/small.jpg", + :medium "http://cloudfront.net/7663dd1e-d8d3-4add-9cdd-e6b7322e622c/med.jpg", + :large "http://cloudfront.net/7663dd1e-d8d3-4add-9cdd-e6b7322e622c/large.jpg"} + {:name "Cam's Mexican Gastro Pub", :categories ["Mexican" "Gastro Pub"], :phone "415-320-9123", :id "bb958ac5-758e-4f42-b984-6b0e13f25194"} + {:service "yelp", :yelp-photo-id "c76eeafe-a161-492d-8a84-f0d3eb729d2b", :categories ["Mexican" "Gastro Pub"]}] + ["Haight European Grill is a acceptable and underground place to have breakfast on a Tuesday afternoon." + {:small "http://cloudfront.net/833021dd-cdcf-419f-8891-cf680f8d124e/small.jpg", + :medium "http://cloudfront.net/833021dd-cdcf-419f-8891-cf680f8d124e/med.jpg", + :large "http://cloudfront.net/833021dd-cdcf-419f-8891-cf680f8d124e/large.jpg"} + {:name "Haight European Grill", :categories ["European" "Grill"], :phone "415-191-2778", :id "7e6281f7-5b17-4056-ada0-85453247bc8f"} + {:service "flare", :username "joe"}] + ["Tenderloin Cage-Free Sushi is a modern and atmospheric place to sip Champagne weekday afternoons." + {:small "http://cloudfront.net/4e14334f-fa3d-4c90-af94-7aa9c37a5dbd/small.jpg", + :medium "http://cloudfront.net/4e14334f-fa3d-4c90-af94-7aa9c37a5dbd/med.jpg", + :large "http://cloudfront.net/4e14334f-fa3d-4c90-af94-7aa9c37a5dbd/large.jpg"} + {:name "Tenderloin Cage-Free Sushi", :categories ["Cage-Free" "Sushi"], :phone "415-348-0644", :id "0b6c036f-82b0-4008-bdfe-5360dd93fb75"} + {:service "facebook", :facebook-photo-id "fd4144e1-bddb-4f4d-8985-de94a554a730", :url "http://facebook.com/photos/fd4144e1-bddb-4f4d-8985-de94a554a730"}] + ["Lower Pac Heights Deep-Dish Liquor Store is a horrible and decent place to pitch an investor on Taco Tuesday." + {:small "http://cloudfront.net/3d883dfb-23a7-4097-aed9-d69c4cbb67ef/small.jpg", + :medium "http://cloudfront.net/3d883dfb-23a7-4097-aed9-d69c4cbb67ef/med.jpg", + :large "http://cloudfront.net/3d883dfb-23a7-4097-aed9-d69c4cbb67ef/large.jpg"} + {:name "Lower Pac Heights Deep-Dish Liquor Store", :categories ["Deep-Dish" "Liquor Store"], :phone "415-497-3039", :id "4d4eabfc-ff1f-4bc6-88b0-2f55489ff666"} + {:service "yelp", :yelp-photo-id "50bdc257-fcc9-44d1-b121-87f5c3e45058", :categories ["Deep-Dish" "Liquor Store"]}] + ["Mission Free-Range Liquor Store is a popular and fantastic place to watch the Giants game on Taco Tuesday." + {:small "http://cloudfront.net/57374d95-ff55-4a20-b98e-191dfc08ce75/small.jpg", + :medium "http://cloudfront.net/57374d95-ff55-4a20-b98e-191dfc08ce75/med.jpg", + :large "http://cloudfront.net/57374d95-ff55-4a20-b98e-191dfc08ce75/large.jpg"} + {:name "Mission Free-Range Liquor Store", :categories ["Free-Range" "Liquor Store"], :phone "415-041-3816", :id "6e665924-8e2c-42ab-af58-23a27f017e37"} + {:service "foursquare", :foursquare-photo-id "90c4812a-a426-4776-aeb1-81e4770c7887", :mayor "sameer"}] + ["SoMa British Bakery is a wonderful and historical place to have a drink during winter." + {:small "http://cloudfront.net/7ee329cb-d9c3-48cf-8486-639e39df7329/small.jpg", + :medium "http://cloudfront.net/7ee329cb-d9c3-48cf-8486-639e39df7329/med.jpg", + :large "http://cloudfront.net/7ee329cb-d9c3-48cf-8486-639e39df7329/large.jpg"} + {:name "SoMa British Bakery", :categories ["British" "Bakery"], :phone "415-909-5728", :id "662cb0d0-8ee6-4db7-aaf1-89eb2530feda"} + {:service "yelp", :yelp-photo-id "d2643a6d-6669-48e5-890e-080aaff6d1e7", :categories ["British" "Bakery"]}] + ["Market St. Gluten-Free Café is a great and modern place to sip a glass of expensive wine Friday nights." + {:small "http://cloudfront.net/723c33fc-4901-4403-9f13-f451f98be98b/small.jpg", + :medium "http://cloudfront.net/723c33fc-4901-4403-9f13-f451f98be98b/med.jpg", + :large "http://cloudfront.net/723c33fc-4901-4403-9f13-f451f98be98b/large.jpg"} + {:name "Market St. Gluten-Free Café", :categories ["Gluten-Free" "Café"], :phone "415-697-9776", :id "ce4947d0-071a-4491-b5ac-f8b0241bd54c"} + {:service "foursquare", :foursquare-photo-id "7ae76e90-44db-483e-a5bc-2cb8ec65fa61", :mayor "jessica"}] + ["Marina No-MSG Sushi is a fantastic and classic place to have brunch with your pet dog." + {:small "http://cloudfront.net/c338a1ea-38e3-4409-9c0e-5099609318aa/small.jpg", + :medium "http://cloudfront.net/c338a1ea-38e3-4409-9c0e-5099609318aa/med.jpg", + :large "http://cloudfront.net/c338a1ea-38e3-4409-9c0e-5099609318aa/large.jpg"} + {:name "Marina No-MSG Sushi", :categories ["No-MSG" "Sushi"], :phone "415-856-5937", :id "d51013a3-8547-4705-a5f0-cb11d8206481"} + {:service "twitter", :mentions ["@marina_no_msg_sushi"], :tags ["#no-msg" "#sushi"], :username "jane"}] + ["Sunset American Churros is a underappreciated and world-famous place to have brunch with friends." + {:small "http://cloudfront.net/56669abd-119c-4a02-abfc-d9acdd1e84fc/small.jpg", + :medium "http://cloudfront.net/56669abd-119c-4a02-abfc-d9acdd1e84fc/med.jpg", + :large "http://cloudfront.net/56669abd-119c-4a02-abfc-d9acdd1e84fc/large.jpg"} + {:name "Sunset American Churros", :categories ["American" "Churros"], :phone "415-191-5018", :id "2e88c921-29fb-489b-a956-d3ba1182da73"} + {:service "foursquare", :foursquare-photo-id "1b34eb04-290c-409e-b63c-ee82fff83878", :mayor "mandy"}] + ["Sameer's Pizza Liquor Store is a decent and underground place to meet new friends the second Saturday of the month." + {:small "http://cloudfront.net/010b1a6c-8ddf-4ea8-9c30-ab990356a5eb/small.jpg", + :medium "http://cloudfront.net/010b1a6c-8ddf-4ea8-9c30-ab990356a5eb/med.jpg", + :large "http://cloudfront.net/010b1a6c-8ddf-4ea8-9c30-ab990356a5eb/large.jpg"} + {:name "Sameer's Pizza Liquor Store", :categories ["Pizza" "Liquor Store"], :phone "415-969-7474", :id "7b9c7dc3-d8f1-498d-843a-e62360449892"} + {:service "flare", :username "biggie"}] + ["SF Deep-Dish Eatery is a delicious and modern place to watch the Warriors game on Saturday night." + {:small "http://cloudfront.net/29b0f569-4023-455e-bfbf-4e2e799c6f6e/small.jpg", + :medium "http://cloudfront.net/29b0f569-4023-455e-bfbf-4e2e799c6f6e/med.jpg", + :large "http://cloudfront.net/29b0f569-4023-455e-bfbf-4e2e799c6f6e/large.jpg"} + {:name "SF Deep-Dish Eatery", :categories ["Deep-Dish" "Eatery"], :phone "415-476-9257", :id "ad41d3f6-c20c-46a7-9e5d-db602fff7d0d"} + {:service "yelp", :yelp-photo-id "79616795-0585-478a-9998-8c9b0169005e", :categories ["Deep-Dish" "Eatery"]}] + ["SF Afgan Restaurant is a fantastic and exclusive place to nurse a hangover in June." + {:small "http://cloudfront.net/9af6de8f-b24f-4f36-8991-0888e7dbad4e/small.jpg", + :medium "http://cloudfront.net/9af6de8f-b24f-4f36-8991-0888e7dbad4e/med.jpg", + :large "http://cloudfront.net/9af6de8f-b24f-4f36-8991-0888e7dbad4e/large.jpg"} + {:name "SF Afgan Restaurant", :categories ["Afgan" "Restaurant"], :phone "415-451-4697", :id "66ccc68a-db9a-470c-a17b-7764d23daced"} + {:service "foursquare", :foursquare-photo-id "c00e6384-fc26-4056-86fc-95df69092552", :mayor "kyle"}] + ["Pacific Heights Red White & Blue Bar & Grill is a swell and atmospheric place to conduct a business meeting the second Saturday of the month." + {:small "http://cloudfront.net/1c465d71-1c46-492f-883a-a069df6048ce/small.jpg", + :medium "http://cloudfront.net/1c465d71-1c46-492f-883a-a069df6048ce/med.jpg", + :large "http://cloudfront.net/1c465d71-1c46-492f-883a-a069df6048ce/large.jpg"} + {:name "Pacific Heights Red White & Blue Bar & Grill", :categories ["Red White & Blue" "Bar & Grill"], :phone "415-208-2550", :id "c7547aa1-94c1-44bd-bf5a-8655e4698ed8"} + {:service "facebook", :facebook-photo-id "d4c43b07-932f-42d3-b70f-29306c9f0746", :url "http://facebook.com/photos/d4c43b07-932f-42d3-b70f-29306c9f0746"}] + ["Lower Pac Heights Cage-Free Coffee House is a acceptable and family-friendly place to nurse a hangover during winter." + {:small "http://cloudfront.net/7cb07300-7f55-4634-b998-4999aa1b4fb8/small.jpg", + :medium "http://cloudfront.net/7cb07300-7f55-4634-b998-4999aa1b4fb8/med.jpg", + :large "http://cloudfront.net/7cb07300-7f55-4634-b998-4999aa1b4fb8/large.jpg"} + {:name "Lower Pac Heights Cage-Free Coffee House", :categories ["Cage-Free" "Coffee House"], :phone "415-697-9309", :id "02b1f618-41a0-406b-96dd-1a017f630b81"} + {:service "flare", :username "rasta_toucan"}] + ["Oakland American Grill is a amazing and swell place to catch a bite to eat the first Sunday of the month." + {:small "http://cloudfront.net/dd1aaaba-124f-4722-8a46-33f2bcb826a4/small.jpg", + :medium "http://cloudfront.net/dd1aaaba-124f-4722-8a46-33f2bcb826a4/med.jpg", + :large "http://cloudfront.net/dd1aaaba-124f-4722-8a46-33f2bcb826a4/large.jpg"} + {:name "Oakland American Grill", :categories ["American" "Grill"], :phone "415-660-0889", :id "856f907d-b669-4b9c-8337-bf9c88883746"} + {:service "yelp", :yelp-photo-id "38bc4d27-d28f-434b-b89d-5b88384a1c9b", :categories ["American" "Grill"]}] + ["Joe's No-MSG Sushi is a well-decorated and delicious place to catch a bite to eat on Thursdays." + {:small "http://cloudfront.net/27579436-073b-45b3-91c8-49f361fecefd/small.jpg", + :medium "http://cloudfront.net/27579436-073b-45b3-91c8-49f361fecefd/med.jpg", + :large "http://cloudfront.net/27579436-073b-45b3-91c8-49f361fecefd/large.jpg"} + {:name "Joe's No-MSG Sushi", :categories ["No-MSG" "Sushi"], :phone "415-739-8157", :id "9ff21570-cd5b-415e-933a-52144f551b86"} + {:service "twitter", :mentions ["@joes_no_msg_sushi"], :tags ["#no-msg" "#sushi"], :username "joe"}] + ["Rasta's British Food Truck is a historical and well-decorated place to take visiting friends and relatives on a Tuesday afternoon." + {:small "http://cloudfront.net/6274a518-20f4-4db9-bcad-473c4f452841/small.jpg", + :medium "http://cloudfront.net/6274a518-20f4-4db9-bcad-473c4f452841/med.jpg", + :large "http://cloudfront.net/6274a518-20f4-4db9-bcad-473c4f452841/large.jpg"} + {:name "Rasta's British Food Truck", :categories ["British" "Food Truck"], :phone "415-958-9031", :id "b6616c97-01d0-488f-a855-bcd6efe2b899"} + {:service "flare", :username "jessica"}] + ["Polk St. Deep-Dish Hotel & Restaurant is a groovy and horrible place to take visiting friends and relatives with your pet toucan." + {:small "http://cloudfront.net/c28e51fc-ac77-43ab-90d3-d660143a78fe/small.jpg", + :medium "http://cloudfront.net/c28e51fc-ac77-43ab-90d3-d660143a78fe/med.jpg", + :large "http://cloudfront.net/c28e51fc-ac77-43ab-90d3-d660143a78fe/large.jpg"} + {:name "Polk St. Deep-Dish Hotel & Restaurant", :categories ["Deep-Dish" "Hotel & Restaurant"], :phone "415-666-8681", :id "47f1698e-ae11-46f5-818b-85a59d0affba"} + {:service "flare", :username "tupac"}] + ["Market St. Gluten-Free Café is a delicious and delicious place to catch a bite to eat on a Tuesday afternoon." + {:small "http://cloudfront.net/cd5d0bf5-1341-4340-877d-b4f22ae989c9/small.jpg", + :medium "http://cloudfront.net/cd5d0bf5-1341-4340-877d-b4f22ae989c9/med.jpg", + :large "http://cloudfront.net/cd5d0bf5-1341-4340-877d-b4f22ae989c9/large.jpg"} + {:name "Market St. Gluten-Free Café", :categories ["Gluten-Free" "Café"], :phone "415-697-9776", :id "ce4947d0-071a-4491-b5ac-f8b0241bd54c"} + {:service "flare", :username "mandy"}] + ["Marina Modern Sushi is a underground and family-friendly place to sip Champagne Friday nights." + {:small "http://cloudfront.net/1c3be2ad-9aa2-4bea-8153-6130512cd9e8/small.jpg", + :medium "http://cloudfront.net/1c3be2ad-9aa2-4bea-8153-6130512cd9e8/med.jpg", + :large "http://cloudfront.net/1c3be2ad-9aa2-4bea-8153-6130512cd9e8/large.jpg"} + {:name "Marina Modern Sushi", :categories ["Modern" "Sushi"], :phone "415-393-7672", :id "21807c63-ca4c-4468-9844-d0c2620fbdfc"} + {:service "flare", :username "joe"}] + ["Pacific Heights Pizza Bakery is a underappreciated and popular place to sip a glass of expensive wine the first Sunday of the month." + {:small "http://cloudfront.net/ac8b7804-3e6b-4c2d-b78e-aa20530c9a35/small.jpg", + :medium "http://cloudfront.net/ac8b7804-3e6b-4c2d-b78e-aa20530c9a35/med.jpg", + :large "http://cloudfront.net/ac8b7804-3e6b-4c2d-b78e-aa20530c9a35/large.jpg"} + {:name "Pacific Heights Pizza Bakery", :categories ["Pizza" "Bakery"], :phone "415-006-0149", :id "7fda37a5-810f-4902-b571-54afe583f0dd"} + {:service "yelp", :yelp-photo-id "8e817a6f-5d3d-4fff-9c80-b5ed3e225095", :categories ["Pizza" "Bakery"]}] + ["Kyle's Japanese Hotel & Restaurant is a amazing and family-friendly place to have breakfast in June." + {:small "http://cloudfront.net/9967c7f3-3da3-4891-b00e-8296898bbeb6/small.jpg", + :medium "http://cloudfront.net/9967c7f3-3da3-4891-b00e-8296898bbeb6/med.jpg", + :large "http://cloudfront.net/9967c7f3-3da3-4891-b00e-8296898bbeb6/large.jpg"} + {:name "Kyle's Japanese Hotel & Restaurant", :categories ["Japanese" "Hotel & Restaurant"], :phone "415-337-5387", :id "eced4f41-b627-4553-a297-888871038b69"} + {:service "flare", :username "mandy"}] + ["Pacific Heights No-MSG Sushi is a groovy and groovy place to drink a craft beer when hungover." + {:small "http://cloudfront.net/6d5d0c28-8b65-4392-9fe9-3b85427a1cdb/small.jpg", + :medium "http://cloudfront.net/6d5d0c28-8b65-4392-9fe9-3b85427a1cdb/med.jpg", + :large "http://cloudfront.net/6d5d0c28-8b65-4392-9fe9-3b85427a1cdb/large.jpg"} + {:name "Pacific Heights No-MSG Sushi", :categories ["No-MSG" "Sushi"], :phone "415-354-9547", :id "b15b66a8-f3da-4b65-8156-80f5518fdd5a"} + {:service "facebook", :facebook-photo-id "265681c6-d000-4618-87fc-f8e35eef9ee3", :url "http://facebook.com/photos/265681c6-d000-4618-87fc-f8e35eef9ee3"}] + ["Joe's Homestyle Eatery is a decent and fantastic place to conduct a business meeting on public holidays." + {:small "http://cloudfront.net/0ad76a39-e0df-4212-a365-b5afb27bf7b6/small.jpg", + :medium "http://cloudfront.net/0ad76a39-e0df-4212-a365-b5afb27bf7b6/med.jpg", + :large "http://cloudfront.net/0ad76a39-e0df-4212-a365-b5afb27bf7b6/large.jpg"} + {:name "Joe's Homestyle Eatery", :categories ["Homestyle" "Eatery"], :phone "415-950-1337", :id "5cc18489-dfaf-417b-900f-5d1d61b961e8"} + {:service "yelp", :yelp-photo-id "69b17ca8-3a3e-4df2-a46b-a18d4d82283d", :categories ["Homestyle" "Eatery"]}] + ["Pacific Heights Irish Grill is a world-famous and delicious place to conduct a business meeting with friends." + {:small "http://cloudfront.net/4e773c36-2173-4583-967c-9d31f56b086d/small.jpg", + :medium "http://cloudfront.net/4e773c36-2173-4583-967c-9d31f56b086d/med.jpg", + :large "http://cloudfront.net/4e773c36-2173-4583-967c-9d31f56b086d/large.jpg"} + {:name "Pacific Heights Irish Grill", :categories ["Irish" "Grill"], :phone "415-491-2202", :id "d6b92dfc-56e9-4f65-b1d0-595f120043d9"} + {:service "flare", :username "jane"}] + ["Marina Japanese Liquor Store is a classic and groovy place to watch the Giants game Friday nights." + {:small "http://cloudfront.net/a3abdf98-ee64-4a7d-96a4-0983e7c6a616/small.jpg", + :medium "http://cloudfront.net/a3abdf98-ee64-4a7d-96a4-0983e7c6a616/med.jpg", + :large "http://cloudfront.net/a3abdf98-ee64-4a7d-96a4-0983e7c6a616/large.jpg"} + {:name "Marina Japanese Liquor Store", :categories ["Japanese" "Liquor Store"], :phone "415-587-9819", :id "08fd0138-35dd-41b0-836d-1c652e95ffcd"} + {:service "twitter", :mentions ["@marina_japanese_liquor_store"], :tags ["#japanese" "#liquor" "#store"], :username "bob"}] + ["Pacific Heights Soul Food Coffee House is a wonderful and underappreciated place to watch the Warriors game in July." + {:small "http://cloudfront.net/08e81253-a561-45d2-8715-e2e8a6d990a0/small.jpg", + :medium "http://cloudfront.net/08e81253-a561-45d2-8715-e2e8a6d990a0/med.jpg", + :large "http://cloudfront.net/08e81253-a561-45d2-8715-e2e8a6d990a0/large.jpg"} + {:name "Pacific Heights Soul Food Coffee House", :categories ["Soul Food" "Coffee House"], :phone "415-838-3464", :id "6aed1816-4c64-4f76-8e2a-619528e5b48d"} + {:service "flare", :username "amy"}] + ["Polk St. Korean Taqueria is a amazing and historical place to people-watch on Saturday night." + {:small "http://cloudfront.net/a9ad1e78-ebe9-4248-b265-048c12945997/small.jpg", + :medium "http://cloudfront.net/a9ad1e78-ebe9-4248-b265-048c12945997/med.jpg", + :large "http://cloudfront.net/a9ad1e78-ebe9-4248-b265-048c12945997/large.jpg"} + {:name "Polk St. Korean Taqueria", :categories ["Korean" "Taqueria"], :phone "415-511-5531", :id "ddb09b32-6f0b-4e54-98c7-14398f16ca4e"} + {:service "twitter", :mentions ["@polk_st._korean_taqueria"], :tags ["#korean" "#taqueria"], :username "bob"}] + ["Lucky's Cage-Free Liquor Store is a delicious and amazing place to catch a bite to eat on a Tuesday afternoon." + {:small "http://cloudfront.net/9e74ec8b-ecd9-4987-b2d8-63a84938d699/small.jpg", + :medium "http://cloudfront.net/9e74ec8b-ecd9-4987-b2d8-63a84938d699/med.jpg", + :large "http://cloudfront.net/9e74ec8b-ecd9-4987-b2d8-63a84938d699/large.jpg"} + {:name "Lucky's Cage-Free Liquor Store", :categories ["Cage-Free" "Liquor Store"], :phone "415-341-3219", :id "968eac8d-1fd0-443d-a7ff-b48810ab0f69"} + {:service "facebook", :facebook-photo-id "8ce61a42-cb9c-4304-bada-3a8fb245bb64", :url "http://facebook.com/photos/8ce61a42-cb9c-4304-bada-3a8fb245bb64"}] + ["SoMa Japanese Churros is a wonderful and modern place to conduct a business meeting during winter." + {:small "http://cloudfront.net/41114760-4e0c-4555-a67c-9018ec34ef73/small.jpg", + :medium "http://cloudfront.net/41114760-4e0c-4555-a67c-9018ec34ef73/med.jpg", + :large "http://cloudfront.net/41114760-4e0c-4555-a67c-9018ec34ef73/large.jpg"} + {:name "SoMa Japanese Churros", :categories ["Japanese" "Churros"], :phone "415-404-1510", :id "373858b2-e634-45d0-973d-4d0fed8c438b"} + {:service "facebook", :facebook-photo-id "4e3d4ad7-54ba-4f55-baf7-4ad223bcfaf6", :url "http://facebook.com/photos/4e3d4ad7-54ba-4f55-baf7-4ad223bcfaf6"}] + ["SoMa Old-Fashioned Pizzeria is a well-decorated and horrible place to meet new friends in the fall." + {:small "http://cloudfront.net/dd0eb180-3e40-4ac6-becb-653a32f2d43d/small.jpg", + :medium "http://cloudfront.net/dd0eb180-3e40-4ac6-becb-653a32f2d43d/med.jpg", + :large "http://cloudfront.net/dd0eb180-3e40-4ac6-becb-653a32f2d43d/large.jpg"} + {:name "SoMa Old-Fashioned Pizzeria", :categories ["Old-Fashioned" "Pizzeria"], :phone "415-966-8856", :id "deb8997b-734d-402b-a181-bd888214bc86"} + {:service "facebook", :facebook-photo-id "91e06ea0-df70-48e2-bb43-6acfe787d203", :url "http://facebook.com/photos/91e06ea0-df70-48e2-bb43-6acfe787d203"}] + ["Oakland European Liquor Store is a popular and historical place to sip Champagne with your pet toucan." + {:small "http://cloudfront.net/9c28ae3e-e583-4ec2-9646-a58af7de6e5a/small.jpg", + :medium "http://cloudfront.net/9c28ae3e-e583-4ec2-9646-a58af7de6e5a/med.jpg", + :large "http://cloudfront.net/9c28ae3e-e583-4ec2-9646-a58af7de6e5a/large.jpg"} + {:name "Oakland European Liquor Store", :categories ["European" "Liquor Store"], :phone "415-559-1516", :id "e342e7b7-e82d-475d-a822-b2df9c84850d"} + {:service "flare", :username "cam_saul"}] + ["Pacific Heights Pizza Bakery is a classic and classic place to have a birthday party on public holidays." + {:small "http://cloudfront.net/114749c9-8bf7-4a65-9757-ac8e42be72bb/small.jpg", + :medium "http://cloudfront.net/114749c9-8bf7-4a65-9757-ac8e42be72bb/med.jpg", + :large "http://cloudfront.net/114749c9-8bf7-4a65-9757-ac8e42be72bb/large.jpg"} + {:name "Pacific Heights Pizza Bakery", :categories ["Pizza" "Bakery"], :phone "415-006-0149", :id "7fda37a5-810f-4902-b571-54afe583f0dd"} + {:service "twitter", :mentions ["@pacific_heights_pizza_bakery"], :tags ["#pizza" "#bakery"], :username "mandy"}] + ["Sameer's GMO-Free Restaurant is a decent and family-friendly place to take visiting friends and relatives during winter." + {:small "http://cloudfront.net/ce7cc9e2-9948-4ca3-9803-87d565168cbb/small.jpg", + :medium "http://cloudfront.net/ce7cc9e2-9948-4ca3-9803-87d565168cbb/med.jpg", + :large "http://cloudfront.net/ce7cc9e2-9948-4ca3-9803-87d565168cbb/large.jpg"} + {:name "Sameer's GMO-Free Restaurant", :categories ["GMO-Free" "Restaurant"], :phone "415-128-9430", :id "7ac8a7dd-c07f-45a6-92ba-bdb1b1280af2"} + {:service "flare", :username "tupac"}] + ["Pacific Heights Pizza Bakery is a great and swell place to take a date on Thursdays." + {:small "http://cloudfront.net/76d68d0c-7118-41eb-9c5b-b45c1096b791/small.jpg", + :medium "http://cloudfront.net/76d68d0c-7118-41eb-9c5b-b45c1096b791/med.jpg", + :large "http://cloudfront.net/76d68d0c-7118-41eb-9c5b-b45c1096b791/large.jpg"} + {:name "Pacific Heights Pizza Bakery", :categories ["Pizza" "Bakery"], :phone "415-006-0149", :id "7fda37a5-810f-4902-b571-54afe583f0dd"} + {:service "facebook", :facebook-photo-id "f420e2a8-13ce-4753-b30e-493d6e59f7ce", :url "http://facebook.com/photos/f420e2a8-13ce-4753-b30e-493d6e59f7ce"}] + ["Lucky's Gluten-Free Gastro Pub is a exclusive and overrated place to nurse a hangover in June." + {:small "http://cloudfront.net/8651fa50-125f-47bf-99ec-2e950facd1fd/small.jpg", + :medium "http://cloudfront.net/8651fa50-125f-47bf-99ec-2e950facd1fd/med.jpg", + :large "http://cloudfront.net/8651fa50-125f-47bf-99ec-2e950facd1fd/large.jpg"} + {:name "Lucky's Gluten-Free Gastro Pub", :categories ["Gluten-Free" "Gastro Pub"], :phone "415-391-6443", :id "7ccf8bbb-74b4-48f7-aa7c-43872f63cb1b"} + {:service "yelp", :yelp-photo-id "22fac240-8066-4a4d-a3a4-08c268bca17d", :categories ["Gluten-Free" "Gastro Pub"]}] + ["Oakland Low-Carb Bakery is a classic and modern place to have brunch weekday afternoons." + {:small "http://cloudfront.net/ecb14fc2-cbc6-4f8f-b879-c6884bec5fb0/small.jpg", + :medium "http://cloudfront.net/ecb14fc2-cbc6-4f8f-b879-c6884bec5fb0/med.jpg", + :large "http://cloudfront.net/ecb14fc2-cbc6-4f8f-b879-c6884bec5fb0/large.jpg"} + {:name "Oakland Low-Carb Bakery", :categories ["Low-Carb" "Bakery"], :phone "415-546-0101", :id "da7dd72d-60fb-495b-a2c0-1e2ae73a1a86"} + {:service "flare", :username "jane"}] + ["Tenderloin Gormet Restaurant is a underground and classic place to have breakfast weekend mornings." + {:small "http://cloudfront.net/d40b4507-d30a-4c0c-a1da-bf78e510607d/small.jpg", + :medium "http://cloudfront.net/d40b4507-d30a-4c0c-a1da-bf78e510607d/med.jpg", + :large "http://cloudfront.net/d40b4507-d30a-4c0c-a1da-bf78e510607d/large.jpg"} + {:name "Tenderloin Gormet Restaurant", :categories ["Gormet" "Restaurant"], :phone "415-127-4197", :id "54a9eac8-d80d-4af8-b6d7-34651a60e59c"} + {:service "flare", :username "joe"}] + ["SoMa TaquerÃa Diner is a historical and world-famous place to watch the Warriors game on Saturday night." + {:small "http://cloudfront.net/1e4cb1bb-ec7a-45b4-bcd8-230a32350a51/small.jpg", + :medium "http://cloudfront.net/1e4cb1bb-ec7a-45b4-bcd8-230a32350a51/med.jpg", + :large "http://cloudfront.net/1e4cb1bb-ec7a-45b4-bcd8-230a32350a51/large.jpg"} + {:name "SoMa TaquerÃa Diner", :categories ["TaquerÃa" "Diner"], :phone "415-947-9521", :id "f97ede4a-074f-4e24-babc-5c44f2be9c36"} + {:service "flare", :username "lucky_pigeon"}] + ["Kyle's Low-Carb Grill is a world-famous and amazing place to nurse a hangover on Thursdays." + {:small "http://cloudfront.net/7cf33495-cc12-499d-b243-53100bd76df6/small.jpg", + :medium "http://cloudfront.net/7cf33495-cc12-499d-b243-53100bd76df6/med.jpg", + :large "http://cloudfront.net/7cf33495-cc12-499d-b243-53100bd76df6/large.jpg"} + {:name "Kyle's Low-Carb Grill", :categories ["Low-Carb" "Grill"], :phone "415-992-8278", :id "b27f50c6-55eb-48b0-9fee-17a6ef5243bd"} + {:service "facebook", :facebook-photo-id "9edda8de-49c1-41f7-8c22-06c0fc7cfc10", :url "http://facebook.com/photos/9edda8de-49c1-41f7-8c22-06c0fc7cfc10"}] + ["Haight Soul Food Pop-Up Food Stand is a modern and acceptable place to watch the Warriors game in the spring." + {:small "http://cloudfront.net/be0f6011-ee1b-47ae-9d71-d7e8561e25a1/small.jpg", + :medium "http://cloudfront.net/be0f6011-ee1b-47ae-9d71-d7e8561e25a1/med.jpg", + :large "http://cloudfront.net/be0f6011-ee1b-47ae-9d71-d7e8561e25a1/large.jpg"} + {:name "Haight Soul Food Pop-Up Food Stand", :categories ["Soul Food" "Pop-Up Food Stand"], :phone "415-741-8726", :id "9735184b-1299-410f-a98e-10d9c548af42"} + {:service "facebook", :facebook-photo-id "234f4d52-f5b6-4214-9fbc-13ae88e25cdb", :url "http://facebook.com/photos/234f4d52-f5b6-4214-9fbc-13ae88e25cdb"}] + ["Rasta's Paleo Churros is a fantastic and classic place to nurse a hangover during summer." + {:small "http://cloudfront.net/51726cf2-f9af-476c-9eea-e8ef16a03cc4/small.jpg", + :medium "http://cloudfront.net/51726cf2-f9af-476c-9eea-e8ef16a03cc4/med.jpg", + :large "http://cloudfront.net/51726cf2-f9af-476c-9eea-e8ef16a03cc4/large.jpg"} + {:name "Rasta's Paleo Churros", :categories ["Paleo" "Churros"], :phone "415-915-0309", :id "3bf48ec6-434b-43b1-be28-7644975ecaf9"} + {:service "foursquare", :foursquare-photo-id "ced4f00d-21ed-4b10-90f6-8d5db5cc24e7", :mayor "bob"}] + ["Rasta's Paleo Churros is a fantastic and delicious place to take a date when hungover." + {:small "http://cloudfront.net/3ebb84e2-a51e-4e2d-913b-3e20974f0ed5/small.jpg", + :medium "http://cloudfront.net/3ebb84e2-a51e-4e2d-913b-3e20974f0ed5/med.jpg", + :large "http://cloudfront.net/3ebb84e2-a51e-4e2d-913b-3e20974f0ed5/large.jpg"} + {:name "Rasta's Paleo Churros", :categories ["Paleo" "Churros"], :phone "415-915-0309", :id "3bf48ec6-434b-43b1-be28-7644975ecaf9"} + {:service "flare", :username "mandy"}] + ["SF Afgan Restaurant is a groovy and family-friendly place to have a after-work cocktail weekend mornings." + {:small "http://cloudfront.net/6ad9e353-e342-4aa3-beca-dff8a2c80506/small.jpg", + :medium "http://cloudfront.net/6ad9e353-e342-4aa3-beca-dff8a2c80506/med.jpg", + :large "http://cloudfront.net/6ad9e353-e342-4aa3-beca-dff8a2c80506/large.jpg"} + {:name "SF Afgan Restaurant", :categories ["Afgan" "Restaurant"], :phone "415-451-4697", :id "66ccc68a-db9a-470c-a17b-7764d23daced"} + {:service "flare", :username "biggie"}] + ["Mission Homestyle Churros is a historical and well-decorated place to catch a bite to eat weekend evenings." + {:small "http://cloudfront.net/f00dd865-2e18-4924-aa16-fb50cc0829de/small.jpg", + :medium "http://cloudfront.net/f00dd865-2e18-4924-aa16-fb50cc0829de/med.jpg", + :large "http://cloudfront.net/f00dd865-2e18-4924-aa16-fb50cc0829de/large.jpg"} + {:name "Mission Homestyle Churros", :categories ["Homestyle" "Churros"], :phone "415-343-4489", :id "21d903d3-8bdb-4b7d-b288-6063ad48af44"} + {:service "twitter", :mentions ["@mission_homestyle_churros"], :tags ["#homestyle" "#churros"], :username "biggie"}] + ["Pacific Heights Free-Range Eatery is a groovy and overrated place to sip a glass of expensive wine in the spring." + {:small "http://cloudfront.net/7ca05f25-488c-47ec-b308-9ac9aa506f3a/small.jpg", + :medium "http://cloudfront.net/7ca05f25-488c-47ec-b308-9ac9aa506f3a/med.jpg", + :large "http://cloudfront.net/7ca05f25-488c-47ec-b308-9ac9aa506f3a/large.jpg"} + {:name "Pacific Heights Free-Range Eatery", :categories ["Free-Range" "Eatery"], :phone "415-901-6541", :id "88b361c8-ce69-4b2e-b0f2-9deedd574af6"} + {:service "yelp", :yelp-photo-id "8ebf793a-f6e8-4113-8d3b-769d6eb1a0bf", :categories ["Free-Range" "Eatery"]}] + ["Marina Cage-Free Liquor Store is a delicious and fantastic place to nurse a hangover during summer." + {:small "http://cloudfront.net/39c91431-8787-4240-b706-911793d6826c/small.jpg", + :medium "http://cloudfront.net/39c91431-8787-4240-b706-911793d6826c/med.jpg", + :large "http://cloudfront.net/39c91431-8787-4240-b706-911793d6826c/large.jpg"} + {:name "Marina Cage-Free Liquor Store", :categories ["Cage-Free" "Liquor Store"], :phone "415-571-0783", :id "ad68f549-3000-407e-ab98-7f314cfa4653"} + {:service "foursquare", :foursquare-photo-id "1a1168ce-aa45-485f-a427-309eeb536510", :mayor "cam_saul"}] + ["Tenderloin Red White & Blue Pizzeria is a atmospheric and atmospheric place to nurse a hangover during winter." + {:small "http://cloudfront.net/fcf38a30-edfb-4dd0-b2e1-91ddfae20425/small.jpg", + :medium "http://cloudfront.net/fcf38a30-edfb-4dd0-b2e1-91ddfae20425/med.jpg", + :large "http://cloudfront.net/fcf38a30-edfb-4dd0-b2e1-91ddfae20425/large.jpg"} + {:name "Tenderloin Red White & Blue Pizzeria", :categories ["Red White & Blue" "Pizzeria"], :phone "415-719-8143", :id "eba3dbcd-100a-4f38-a701-e0dec157f437"} + {:service "yelp", :yelp-photo-id "989113fd-f95f-4e62-ac21-a428e2b72334", :categories ["Red White & Blue" "Pizzeria"]}] + ["Polk St. Mexican Coffee House is a classic and swell place to watch the Giants game weekend mornings." + {:small "http://cloudfront.net/ffe98db7-7824-4685-99ec-eaebf63d9248/small.jpg", + :medium "http://cloudfront.net/ffe98db7-7824-4685-99ec-eaebf63d9248/med.jpg", + :large "http://cloudfront.net/ffe98db7-7824-4685-99ec-eaebf63d9248/large.jpg"} + {:name "Polk St. Mexican Coffee House", :categories ["Mexican" "Coffee House"], :phone "415-144-7901", :id "396d36d7-13ad-41fd-86b5-8b70b6ecdabf"} + {:service "facebook", :facebook-photo-id "4384296d-a41c-44c8-bff9-a5d64bb01f44", :url "http://facebook.com/photos/4384296d-a41c-44c8-bff9-a5d64bb01f44"}] + ["Mission Chinese Liquor Store is a horrible and atmospheric place to have a birthday party when hungover." + {:small "http://cloudfront.net/9c3f49c7-3924-4213-b09d-aaa9db7993f3/small.jpg", + :medium "http://cloudfront.net/9c3f49c7-3924-4213-b09d-aaa9db7993f3/med.jpg", + :large "http://cloudfront.net/9c3f49c7-3924-4213-b09d-aaa9db7993f3/large.jpg"} + {:name "Mission Chinese Liquor Store", :categories ["Chinese" "Liquor Store"], :phone "415-906-6919", :id "00132b5b-31fc-46f0-a288-f547f23477ee"} + {:service "yelp", :yelp-photo-id "4623b716-fb61-4349-8f79-cef3706d6b0d", :categories ["Chinese" "Liquor Store"]}] + ["Tenderloin Gormet Restaurant is a overrated and swell place to nurse a hangover in June." + {:small "http://cloudfront.net/817b3f0f-3cee-48ac-bfec-9b5ad18d8350/small.jpg", + :medium "http://cloudfront.net/817b3f0f-3cee-48ac-bfec-9b5ad18d8350/med.jpg", + :large "http://cloudfront.net/817b3f0f-3cee-48ac-bfec-9b5ad18d8350/large.jpg"} + {:name "Tenderloin Gormet Restaurant", :categories ["Gormet" "Restaurant"], :phone "415-127-4197", :id "54a9eac8-d80d-4af8-b6d7-34651a60e59c"} + {:service "yelp", :yelp-photo-id "a4e6c1c8-afaf-48ed-b18d-755f9c6dd4b6", :categories ["Gormet" "Restaurant"]}] + ["Tenderloin Paleo Hotel & Restaurant is a modern and groovy place to pitch an investor the second Saturday of the month." + {:small "http://cloudfront.net/8357d4c5-a502-4b71-ab70-25a5090ac16c/small.jpg", + :medium "http://cloudfront.net/8357d4c5-a502-4b71-ab70-25a5090ac16c/med.jpg", + :large "http://cloudfront.net/8357d4c5-a502-4b71-ab70-25a5090ac16c/large.jpg"} + {:name "Tenderloin Paleo Hotel & Restaurant", :categories ["Paleo" "Hotel & Restaurant"], :phone "415-402-1652", :id "4dea27b4-6d89-4b86-80a8-5631e171da8d"} + {:service "flare", :username "biggie"}] + ["Pacific Heights Free-Range Eatery is a underground and family-friendly place to have a after-work cocktail with friends." + {:small "http://cloudfront.net/7a4260f9-efef-4e28-8c78-4b2d2f36d30d/small.jpg", + :medium "http://cloudfront.net/7a4260f9-efef-4e28-8c78-4b2d2f36d30d/med.jpg", + :large "http://cloudfront.net/7a4260f9-efef-4e28-8c78-4b2d2f36d30d/large.jpg"} + {:name "Pacific Heights Free-Range Eatery", :categories ["Free-Range" "Eatery"], :phone "415-901-6541", :id "88b361c8-ce69-4b2e-b0f2-9deedd574af6"} + {:service "yelp", :yelp-photo-id "cc9465c4-b3af-45f1-aa0d-8b38c5a6a366", :categories ["Free-Range" "Eatery"]}] + ["Lower Pac Heights Deep-Dish Ice Cream Truck is a classic and atmospheric place to meet new friends weekend mornings." + {:small "http://cloudfront.net/e7bf1145-72d9-4229-b464-d33209e3549a/small.jpg", + :medium "http://cloudfront.net/e7bf1145-72d9-4229-b464-d33209e3549a/med.jpg", + :large "http://cloudfront.net/e7bf1145-72d9-4229-b464-d33209e3549a/large.jpg"} + {:name "Lower Pac Heights Deep-Dish Ice Cream Truck", :categories ["Deep-Dish" "Ice Cream Truck"], :phone "415-495-1414", :id "d5efa2f2-496d-41b2-8b85-3d002a22a2bc"} + {:service "yelp", :yelp-photo-id "6b5975f9-77f6-4375-be86-5fd047a5493a", :categories ["Deep-Dish" "Ice Cream Truck"]}] + ["Mission BBQ Churros is a family-friendly and delicious place to people-watch on Saturday night." + {:small "http://cloudfront.net/24d757ff-5f11-4864-8395-744c78e36ade/small.jpg", + :medium "http://cloudfront.net/24d757ff-5f11-4864-8395-744c78e36ade/med.jpg", + :large "http://cloudfront.net/24d757ff-5f11-4864-8395-744c78e36ade/large.jpg"} + {:name "Mission BBQ Churros", :categories ["BBQ" "Churros"], :phone "415-406-5374", :id "429ea81a-02c5-449f-bfa7-03a11b227f1f"} + {:service "yelp", :yelp-photo-id "dd02f753-593e-4bc5-b370-08a6fe46de96", :categories ["BBQ" "Churros"]}] + ["Pacific Heights Soul Food Coffee House is a family-friendly and wonderful place to pitch an investor weekday afternoons." + {:small "http://cloudfront.net/24928248-5bf6-4a49-a2aa-2331d9d83990/small.jpg", + :medium "http://cloudfront.net/24928248-5bf6-4a49-a2aa-2331d9d83990/med.jpg", + :large "http://cloudfront.net/24928248-5bf6-4a49-a2aa-2331d9d83990/large.jpg"} + {:name "Pacific Heights Soul Food Coffee House", :categories ["Soul Food" "Coffee House"], :phone "415-838-3464", :id "6aed1816-4c64-4f76-8e2a-619528e5b48d"} + {:service "twitter", :mentions ["@pacific_heights_soul_food_coffee_house"], :tags ["#soul" "#food" "#coffee" "#house"], :username "mandy"}] + ["Mission Soul Food Pizzeria is a groovy and underground place to take visiting friends and relatives in July." + {:small "http://cloudfront.net/c60e9321-eae7-43a9-aa60-654d6be34177/small.jpg", + :medium "http://cloudfront.net/c60e9321-eae7-43a9-aa60-654d6be34177/med.jpg", + :large "http://cloudfront.net/c60e9321-eae7-43a9-aa60-654d6be34177/large.jpg"} + {:name "Mission Soul Food Pizzeria", :categories ["Soul Food" "Pizzeria"], :phone "415-437-3479", :id "9905fe61-44cb-4626-843b-5d725c7949bb"} + {:service "yelp", :yelp-photo-id "bf759d74-af3f-4a57-addb-1349f2b2f8c9", :categories ["Soul Food" "Pizzeria"]}] + ["Chinatown American Bakery is a decent and great place to have brunch during winter." + {:small "http://cloudfront.net/301b02c7-4539-4713-9d5f-3f478ca2b2c3/small.jpg", + :medium "http://cloudfront.net/301b02c7-4539-4713-9d5f-3f478ca2b2c3/med.jpg", + :large "http://cloudfront.net/301b02c7-4539-4713-9d5f-3f478ca2b2c3/large.jpg"} + {:name "Chinatown American Bakery", :categories ["American" "Bakery"], :phone "415-658-7393", :id "cf55cdbd-c614-4be1-8496-0e11b195d16f"} + {:service "flare", :username "rasta_toucan"}] + ["Cam's Old-Fashioned Coffee House is a groovy and popular place to have a birthday party in the fall." + {:small "http://cloudfront.net/1aed40ef-77a4-4207-9bc5-c657fad18f61/small.jpg", + :medium "http://cloudfront.net/1aed40ef-77a4-4207-9bc5-c657fad18f61/med.jpg", + :large "http://cloudfront.net/1aed40ef-77a4-4207-9bc5-c657fad18f61/large.jpg"} + {:name "Cam's Old-Fashioned Coffee House", :categories ["Old-Fashioned" "Coffee House"], :phone "415-868-2973", :id "27592c2b-e682-44bb-be28-8e9a622becca"} + {:service "twitter", :mentions ["@cams_old_fashioned_coffee_house"], :tags ["#old-fashioned" "#coffee" "#house"], :username "lucky_pigeon"}] + ["Pacific Heights No-MSG Sushi is a well-decorated and amazing place to conduct a business meeting in the fall." + {:small "http://cloudfront.net/4e1edf59-97fa-456b-94ff-bc82c7282d86/small.jpg", + :medium "http://cloudfront.net/4e1edf59-97fa-456b-94ff-bc82c7282d86/med.jpg", + :large "http://cloudfront.net/4e1edf59-97fa-456b-94ff-bc82c7282d86/large.jpg"} + {:name "Pacific Heights No-MSG Sushi", :categories ["No-MSG" "Sushi"], :phone "415-354-9547", :id "b15b66a8-f3da-4b65-8156-80f5518fdd5a"} + {:service "facebook", :facebook-photo-id "251a7bf8-73d3-44b8-9f55-2ee0048158d1", :url "http://facebook.com/photos/251a7bf8-73d3-44b8-9f55-2ee0048158d1"}] + ["Mission Chinese Liquor Store is a underappreciated and acceptable place to have breakfast with friends." + {:small "http://cloudfront.net/4276a5e5-3821-4fba-bf48-32c8d9fb2ba1/small.jpg", + :medium "http://cloudfront.net/4276a5e5-3821-4fba-bf48-32c8d9fb2ba1/med.jpg", + :large "http://cloudfront.net/4276a5e5-3821-4fba-bf48-32c8d9fb2ba1/large.jpg"} + {:name "Mission Chinese Liquor Store", :categories ["Chinese" "Liquor Store"], :phone "415-906-6919", :id "00132b5b-31fc-46f0-a288-f547f23477ee"} + {:service "facebook", :facebook-photo-id "b2fa77dd-6e6d-440e-8f12-caa3ea7c120c", :url "http://facebook.com/photos/b2fa77dd-6e6d-440e-8f12-caa3ea7c120c"}] + ["Kyle's Old-Fashioned Pop-Up Food Stand is a underappreciated and groovy place to catch a bite to eat on a Tuesday afternoon." + {:small "http://cloudfront.net/36d9d88d-a911-4e0e-911a-16645a2d939e/small.jpg", + :medium "http://cloudfront.net/36d9d88d-a911-4e0e-911a-16645a2d939e/med.jpg", + :large "http://cloudfront.net/36d9d88d-a911-4e0e-911a-16645a2d939e/large.jpg"} + {:name "Kyle's Old-Fashioned Pop-Up Food Stand", :categories ["Old-Fashioned" "Pop-Up Food Stand"], :phone "415-638-8972", :id "7da187e8-bd01-48ca-ad93-7a02a442d9eb"} + {:service "flare", :username "sameer"}] + ["Sameer's GMO-Free Restaurant is a groovy and modern place to people-watch after baseball games." + {:small "http://cloudfront.net/74209bf9-650b-4130-8095-95ce2caf22bf/small.jpg", + :medium "http://cloudfront.net/74209bf9-650b-4130-8095-95ce2caf22bf/med.jpg", + :large "http://cloudfront.net/74209bf9-650b-4130-8095-95ce2caf22bf/large.jpg"} + {:name "Sameer's GMO-Free Restaurant", :categories ["GMO-Free" "Restaurant"], :phone "415-128-9430", :id "7ac8a7dd-c07f-45a6-92ba-bdb1b1280af2"} + {:service "twitter", :mentions ["@sameers_gmo_free_restaurant"], :tags ["#gmo-free" "#restaurant"], :username "tupac"}] + ["Haight Soul Food Pop-Up Food Stand is a underground and modern place to have breakfast on a Tuesday afternoon." + {:small "http://cloudfront.net/8f613909-550f-4d79-96f6-dc498ff65d1b/small.jpg", + :medium "http://cloudfront.net/8f613909-550f-4d79-96f6-dc498ff65d1b/med.jpg", + :large "http://cloudfront.net/8f613909-550f-4d79-96f6-dc498ff65d1b/large.jpg"} + {:name "Haight Soul Food Pop-Up Food Stand", :categories ["Soul Food" "Pop-Up Food Stand"], :phone "415-741-8726", :id "9735184b-1299-410f-a98e-10d9c548af42"} + {:service "twitter", :mentions ["@haight_soul_food_pop_up_food_stand"], :tags ["#soul" "#food" "#pop-up" "#food" "#stand"], :username "kyle"}] + ["Pacific Heights Irish Grill is a acceptable and world-famous place to have a after-work cocktail on Saturday night." + {:small "http://cloudfront.net/a7c43f41-345f-43ce-8bb9-48b4717d1cfd/small.jpg", + :medium "http://cloudfront.net/a7c43f41-345f-43ce-8bb9-48b4717d1cfd/med.jpg", + :large "http://cloudfront.net/a7c43f41-345f-43ce-8bb9-48b4717d1cfd/large.jpg"} + {:name "Pacific Heights Irish Grill", :categories ["Irish" "Grill"], :phone "415-491-2202", :id "d6b92dfc-56e9-4f65-b1d0-595f120043d9"} + {:service "flare", :username "mandy"}] + ["Mission Homestyle Churros is a decent and horrible place to pitch an investor on Taco Tuesday." + {:small "http://cloudfront.net/da92cb40-5920-49cf-b065-4cb558f201df/small.jpg", + :medium "http://cloudfront.net/da92cb40-5920-49cf-b065-4cb558f201df/med.jpg", + :large "http://cloudfront.net/da92cb40-5920-49cf-b065-4cb558f201df/large.jpg"} + {:name "Mission Homestyle Churros", :categories ["Homestyle" "Churros"], :phone "415-343-4489", :id "21d903d3-8bdb-4b7d-b288-6063ad48af44"} + {:service "flare", :username "sameer"}] + ["Haight Soul Food Sushi is a well-decorated and wonderful place to drink a craft beer with friends." + {:small "http://cloudfront.net/918a829c-61a2-439f-8607-b5c42f39345c/small.jpg", + :medium "http://cloudfront.net/918a829c-61a2-439f-8607-b5c42f39345c/med.jpg", + :large "http://cloudfront.net/918a829c-61a2-439f-8607-b5c42f39345c/large.jpg"} + {:name "Haight Soul Food Sushi", :categories ["Soul Food" "Sushi"], :phone "415-371-8026", :id "b4df5eb7-d8cd-431d-9d43-381984ec81ae"} + {:service "foursquare", :foursquare-photo-id "95609cc6-1197-4713-bc72-7e3e799dbb6c", :mayor "jessica"}] + ["Market St. Homestyle Pop-Up Food Stand is a historical and great place to nurse a hangover weekday afternoons." + {:small "http://cloudfront.net/8f9473e3-1128-4383-8f9d-3a2009b97f56/small.jpg", + :medium "http://cloudfront.net/8f9473e3-1128-4383-8f9d-3a2009b97f56/med.jpg", + :large "http://cloudfront.net/8f9473e3-1128-4383-8f9d-3a2009b97f56/large.jpg"} + {:name "Market St. Homestyle Pop-Up Food Stand", :categories ["Homestyle" "Pop-Up Food Stand"], :phone "415-213-3030", :id "2d873280-e43d-449e-9940-af96ae7df718"} + {:service "foursquare", :foursquare-photo-id "002d03c2-9c91-4160-a9e8-4603ae977245", :mayor "sameer"}] + ["Sameer's European Sushi is a atmospheric and horrible place to take a date on Taco Tuesday." + {:small "http://cloudfront.net/e0767b0f-2938-4e84-be6f-9c7e6ba8ff82/small.jpg", + :medium "http://cloudfront.net/e0767b0f-2938-4e84-be6f-9c7e6ba8ff82/med.jpg", + :large "http://cloudfront.net/e0767b0f-2938-4e84-be6f-9c7e6ba8ff82/large.jpg"} + {:name "Sameer's European Sushi", :categories ["European" "Sushi"], :phone "415-035-2474", :id "7de6b3ef-7b53-4831-bf76-43123874f8ce"} + {:service "yelp", :yelp-photo-id "6ccfda4c-84d2-4108-8cb1-699d28cd1574", :categories ["European" "Sushi"]}] + ["Polk St. Korean Taqueria is a family-friendly and horrible place to take a date in the spring." + {:small "http://cloudfront.net/53a88ef8-505f-4923-9a13-eb686c6e7f25/small.jpg", + :medium "http://cloudfront.net/53a88ef8-505f-4923-9a13-eb686c6e7f25/med.jpg", + :large "http://cloudfront.net/53a88ef8-505f-4923-9a13-eb686c6e7f25/large.jpg"} + {:name "Polk St. Korean Taqueria", :categories ["Korean" "Taqueria"], :phone "415-511-5531", :id "ddb09b32-6f0b-4e54-98c7-14398f16ca4e"} + {:service "flare", :username "bob"}] + ["Polk St. Red White & Blue Café is a popular and swell place to have a birthday party in June." + {:small "http://cloudfront.net/eed1df2e-ed28-4a7c-a9b4-9b6511837414/small.jpg", + :medium "http://cloudfront.net/eed1df2e-ed28-4a7c-a9b4-9b6511837414/med.jpg", + :large "http://cloudfront.net/eed1df2e-ed28-4a7c-a9b4-9b6511837414/large.jpg"} + {:name "Polk St. Red White & Blue Café", :categories ["Red White & Blue" "Café"], :phone "415-986-0661", :id "6eb9d2db-5015-49f0-bd18-f4c4938b7e5a"} + {:service "facebook", :facebook-photo-id "276865a5-e7d3-40f6-a2b2-62172caf3024", :url "http://facebook.com/photos/276865a5-e7d3-40f6-a2b2-62172caf3024"}] + ["Lucky's Gluten-Free Gastro Pub is a underappreciated and historical place to watch the Warriors game with friends." + {:small "http://cloudfront.net/4020585b-9a66-4cb1-b5f2-2e7e3a1d26c2/small.jpg", + :medium "http://cloudfront.net/4020585b-9a66-4cb1-b5f2-2e7e3a1d26c2/med.jpg", + :large "http://cloudfront.net/4020585b-9a66-4cb1-b5f2-2e7e3a1d26c2/large.jpg"} + {:name "Lucky's Gluten-Free Gastro Pub", :categories ["Gluten-Free" "Gastro Pub"], :phone "415-391-6443", :id "7ccf8bbb-74b4-48f7-aa7c-43872f63cb1b"} + {:service "flare", :username "kyle"}] + ["Rasta's Paleo Café is a decent and swell place to have a drink weekend evenings." + {:small "http://cloudfront.net/7bdece4f-bc76-46a3-bbf6-c8a9c2012e79/small.jpg", + :medium "http://cloudfront.net/7bdece4f-bc76-46a3-bbf6-c8a9c2012e79/med.jpg", + :large "http://cloudfront.net/7bdece4f-bc76-46a3-bbf6-c8a9c2012e79/large.jpg"} + {:name "Rasta's Paleo Café", :categories ["Paleo" "Café"], :phone "415-392-6341", :id "4f9e69be-f06c-46a0-bb8b-f3ddd8218ca1"} + {:service "facebook", :facebook-photo-id "26cff009-fce9-4439-a263-39df23a3b143", :url "http://facebook.com/photos/26cff009-fce9-4439-a263-39df23a3b143"}] + ["Polk St. Mexican Coffee House is a modern and historical place to have brunch when hungover." + {:small "http://cloudfront.net/34084c76-3ff7-4bf0-a0f9-bcf9a3767f03/small.jpg", + :medium "http://cloudfront.net/34084c76-3ff7-4bf0-a0f9-bcf9a3767f03/med.jpg", + :large "http://cloudfront.net/34084c76-3ff7-4bf0-a0f9-bcf9a3767f03/large.jpg"} + {:name "Polk St. Mexican Coffee House", :categories ["Mexican" "Coffee House"], :phone "415-144-7901", :id "396d36d7-13ad-41fd-86b5-8b70b6ecdabf"} + {:service "yelp", :yelp-photo-id "b82466db-dddb-451c-ba09-47d520df8730", :categories ["Mexican" "Coffee House"]}] + ["Polk St. Korean Taqueria is a amazing and classic place to take visiting friends and relatives weekend evenings." + {:small "http://cloudfront.net/95f2673c-fa99-4429-918d-45095a699691/small.jpg", + :medium "http://cloudfront.net/95f2673c-fa99-4429-918d-45095a699691/med.jpg", + :large "http://cloudfront.net/95f2673c-fa99-4429-918d-45095a699691/large.jpg"} + {:name "Polk St. Korean Taqueria", :categories ["Korean" "Taqueria"], :phone "415-511-5531", :id "ddb09b32-6f0b-4e54-98c7-14398f16ca4e"} + {:service "foursquare", :foursquare-photo-id "686a4c8a-51ca-4f6b-b216-d4b53dc11944", :mayor "rasta_toucan"}] + ["Pacific Heights Free-Range Eatery is a groovy and swell place to sip a glass of expensive wine during summer." + {:small "http://cloudfront.net/10f63b40-459d-446a-9bc9-a7a6a078d77a/small.jpg", + :medium "http://cloudfront.net/10f63b40-459d-446a-9bc9-a7a6a078d77a/med.jpg", + :large "http://cloudfront.net/10f63b40-459d-446a-9bc9-a7a6a078d77a/large.jpg"} + {:name "Pacific Heights Free-Range Eatery", :categories ["Free-Range" "Eatery"], :phone "415-901-6541", :id "88b361c8-ce69-4b2e-b0f2-9deedd574af6"} + {:service "flare", :username "sameer"}] + ["Market St. Low-Carb Taqueria is a delicious and groovy place to watch the Warriors game with friends." + {:small "http://cloudfront.net/1d458234-44b2-47f7-9038-1110a9a0dc0d/small.jpg", + :medium "http://cloudfront.net/1d458234-44b2-47f7-9038-1110a9a0dc0d/med.jpg", + :large "http://cloudfront.net/1d458234-44b2-47f7-9038-1110a9a0dc0d/large.jpg"} + {:name "Market St. Low-Carb Taqueria", :categories ["Low-Carb" "Taqueria"], :phone "415-751-6525", :id "f30eb85b-f048-4d8c-8008-3c2876125061"} + {:service "facebook", :facebook-photo-id "b136f58c-6b1f-433b-bce8-c803040ff9bd", :url "http://facebook.com/photos/b136f58c-6b1f-433b-bce8-c803040ff9bd"}] + ["Pacific Heights Pizza Bakery is a classic and historical place to meet new friends during winter." + {:small "http://cloudfront.net/903495d4-56b9-4ac6-a141-fd30ad37f00c/small.jpg", + :medium "http://cloudfront.net/903495d4-56b9-4ac6-a141-fd30ad37f00c/med.jpg", + :large "http://cloudfront.net/903495d4-56b9-4ac6-a141-fd30ad37f00c/large.jpg"} + {:name "Pacific Heights Pizza Bakery", :categories ["Pizza" "Bakery"], :phone "415-006-0149", :id "7fda37a5-810f-4902-b571-54afe583f0dd"} + {:service "facebook", :facebook-photo-id "024c7142-3009-4fcc-9094-f7975b939e80", :url "http://facebook.com/photos/024c7142-3009-4fcc-9094-f7975b939e80"}] + ["Polk St. Mexican Coffee House is a horrible and underground place to have brunch with friends." + {:small "http://cloudfront.net/4c06d251-8f13-4195-b5d6-68857091b26f/small.jpg", + :medium "http://cloudfront.net/4c06d251-8f13-4195-b5d6-68857091b26f/med.jpg", + :large "http://cloudfront.net/4c06d251-8f13-4195-b5d6-68857091b26f/large.jpg"} + {:name "Polk St. Mexican Coffee House", :categories ["Mexican" "Coffee House"], :phone "415-144-7901", :id "396d36d7-13ad-41fd-86b5-8b70b6ecdabf"} + {:service "flare", :username "lucky_pigeon"}] + ["Marina Low-Carb Food Truck is a swell and world-famous place to watch the Warriors game with friends." + {:small "http://cloudfront.net/95b4c506-0290-4042-ba97-de6d46d23cbe/small.jpg", + :medium "http://cloudfront.net/95b4c506-0290-4042-ba97-de6d46d23cbe/med.jpg", + :large "http://cloudfront.net/95b4c506-0290-4042-ba97-de6d46d23cbe/large.jpg"} + {:name "Marina Low-Carb Food Truck", :categories ["Low-Carb" "Food Truck"], :phone "415-748-3513", :id "a13a5beb-19de-40ca-a334-02df3bdf5285"} + {:service "facebook", :facebook-photo-id "10073113-dee8-47d0-a994-eb84a3099ad4", :url "http://facebook.com/photos/10073113-dee8-47d0-a994-eb84a3099ad4"}] + ["Haight Soul Food Pop-Up Food Stand is a exclusive and amazing place to nurse a hangover during summer." + {:small "http://cloudfront.net/fd764dce-022d-45ca-80ac-6603f807de11/small.jpg", + :medium "http://cloudfront.net/fd764dce-022d-45ca-80ac-6603f807de11/med.jpg", + :large "http://cloudfront.net/fd764dce-022d-45ca-80ac-6603f807de11/large.jpg"} + {:name "Haight Soul Food Pop-Up Food Stand", :categories ["Soul Food" "Pop-Up Food Stand"], :phone "415-741-8726", :id "9735184b-1299-410f-a98e-10d9c548af42"} + {:service "yelp", :yelp-photo-id "59df02f3-5581-43bb-8875-ce689355302d", :categories ["Soul Food" "Pop-Up Food Stand"]}] + ["Polk St. Red White & Blue Café is a family-friendly and wonderful place to have brunch weekday afternoons." + {:small "http://cloudfront.net/0e61715d-c180-4fd0-942a-1917392c5b18/small.jpg", + :medium "http://cloudfront.net/0e61715d-c180-4fd0-942a-1917392c5b18/med.jpg", + :large "http://cloudfront.net/0e61715d-c180-4fd0-942a-1917392c5b18/large.jpg"} + {:name "Polk St. Red White & Blue Café", :categories ["Red White & Blue" "Café"], :phone "415-986-0661", :id "6eb9d2db-5015-49f0-bd18-f4c4938b7e5a"} + {:service "facebook", :facebook-photo-id "061a7423-a561-4ed6-9b75-20053659738c", :url "http://facebook.com/photos/061a7423-a561-4ed6-9b75-20053659738c"}] + ["Rasta's Paleo Churros is a classic and overrated place to catch a bite to eat in June." + {:small "http://cloudfront.net/6298f645-2abd-4b53-821a-6d3d1dd5eab7/small.jpg", + :medium "http://cloudfront.net/6298f645-2abd-4b53-821a-6d3d1dd5eab7/med.jpg", + :large "http://cloudfront.net/6298f645-2abd-4b53-821a-6d3d1dd5eab7/large.jpg"} + {:name "Rasta's Paleo Churros", :categories ["Paleo" "Churros"], :phone "415-915-0309", :id "3bf48ec6-434b-43b1-be28-7644975ecaf9"} + {:service "yelp", :yelp-photo-id "3326a855-2959-486f-a0f3-21c21211361f", :categories ["Paleo" "Churros"]}] + ["Nob Hill Korean Taqueria is a great and popular place to take a date when hungover." + {:small "http://cloudfront.net/0717cf07-cc25-4938-82ad-582fbb27753c/small.jpg", + :medium "http://cloudfront.net/0717cf07-cc25-4938-82ad-582fbb27753c/med.jpg", + :large "http://cloudfront.net/0717cf07-cc25-4938-82ad-582fbb27753c/large.jpg"} + {:name "Nob Hill Korean Taqueria", :categories ["Korean" "Taqueria"], :phone "415-107-7332", :id "a43c184c-90f5-488c-bb3b-00ea2666d90e"} + {:service "flare", :username "rasta_toucan"}] + ["Tenderloin Japanese Ice Cream Truck is a swell and acceptable place to nurse a hangover on Thursdays." + {:small "http://cloudfront.net/aa6e3b77-7eba-4817-8510-e1e19e96ae2a/small.jpg", + :medium "http://cloudfront.net/aa6e3b77-7eba-4817-8510-e1e19e96ae2a/med.jpg", + :large "http://cloudfront.net/aa6e3b77-7eba-4817-8510-e1e19e96ae2a/large.jpg"} + {:name "Tenderloin Japanese Ice Cream Truck", :categories ["Japanese" "Ice Cream Truck"], :phone "415-856-0371", :id "5ce47baa-bbef-4bc7-adf6-57842913ea8a"} + {:service "yelp", :yelp-photo-id "cc977fda-1fb0-4e78-8699-1558c5f42e69", :categories ["Japanese" "Ice Cream Truck"]}] + ["Haight Soul Food Sushi is a decent and swell place to conduct a business meeting in the spring." + {:small "http://cloudfront.net/677835f0-ba4f-40b1-bd8b-993760fa3117/small.jpg", + :medium "http://cloudfront.net/677835f0-ba4f-40b1-bd8b-993760fa3117/med.jpg", + :large "http://cloudfront.net/677835f0-ba4f-40b1-bd8b-993760fa3117/large.jpg"} + {:name "Haight Soul Food Sushi", :categories ["Soul Food" "Sushi"], :phone "415-371-8026", :id "b4df5eb7-d8cd-431d-9d43-381984ec81ae"} + {:service "yelp", :yelp-photo-id "40052a53-f10e-4d38-806f-d88d2e77faf2", :categories ["Soul Food" "Sushi"]}] + ["Kyle's Low-Carb Grill is a classic and wonderful place to have a drink when hungover." + {:small "http://cloudfront.net/7f021f2d-479e-4115-a0ad-18115f906184/small.jpg", + :medium "http://cloudfront.net/7f021f2d-479e-4115-a0ad-18115f906184/med.jpg", + :large "http://cloudfront.net/7f021f2d-479e-4115-a0ad-18115f906184/large.jpg"} + {:name "Kyle's Low-Carb Grill", :categories ["Low-Carb" "Grill"], :phone "415-992-8278", :id "b27f50c6-55eb-48b0-9fee-17a6ef5243bd"} + {:service "flare", :username "mandy"}] + ["Rasta's Paleo Churros is a modern and horrible place to watch the Giants game on Taco Tuesday." + {:small "http://cloudfront.net/cb993d22-054e-4848-ac71-ad7071d1df7f/small.jpg", + :medium "http://cloudfront.net/cb993d22-054e-4848-ac71-ad7071d1df7f/med.jpg", + :large "http://cloudfront.net/cb993d22-054e-4848-ac71-ad7071d1df7f/large.jpg"} + {:name "Rasta's Paleo Churros", :categories ["Paleo" "Churros"], :phone "415-915-0309", :id "3bf48ec6-434b-43b1-be28-7644975ecaf9"} + {:service "foursquare", :foursquare-photo-id "ed2871fe-d033-4d81-87b1-86550a879d9c", :mayor "rasta_toucan"}] + ["Kyle's Chinese Restaurant is a delicious and well-decorated place to pitch an investor the first Sunday of the month." + {:small "http://cloudfront.net/750fb652-b5a7-4107-8c46-8e28bc3e86f6/small.jpg", + :medium "http://cloudfront.net/750fb652-b5a7-4107-8c46-8e28bc3e86f6/med.jpg", + :large "http://cloudfront.net/750fb652-b5a7-4107-8c46-8e28bc3e86f6/large.jpg"} + {:name "Kyle's Chinese Restaurant", :categories ["Chinese" "Restaurant"], :phone "415-298-9499", :id "de08b3c7-9929-40d8-8c20-dd9317613c17"} + {:service "yelp", :yelp-photo-id "964cf5f3-9d68-4fa5-8fff-93d09fe23f13", :categories ["Chinese" "Restaurant"]}] + ["Rasta's Paleo Café is a exclusive and popular place to sip Champagne on Saturday night." + {:small "http://cloudfront.net/5faf0fda-041a-446d-aba4-54b7a6ceaa18/small.jpg", + :medium "http://cloudfront.net/5faf0fda-041a-446d-aba4-54b7a6ceaa18/med.jpg", + :large "http://cloudfront.net/5faf0fda-041a-446d-aba4-54b7a6ceaa18/large.jpg"} + {:name "Rasta's Paleo Café", :categories ["Paleo" "Café"], :phone "415-392-6341", :id "4f9e69be-f06c-46a0-bb8b-f3ddd8218ca1"} + {:service "facebook", :facebook-photo-id "0fc82427-48c6-468d-8813-9dca3289e519", :url "http://facebook.com/photos/0fc82427-48c6-468d-8813-9dca3289e519"}] + ["Kyle's Free-Range Taqueria is a horrible and exclusive place to sip a glass of expensive wine with your pet toucan." + {:small "http://cloudfront.net/865964d4-35c0-40e2-9029-584fc6f393ce/small.jpg", + :medium "http://cloudfront.net/865964d4-35c0-40e2-9029-584fc6f393ce/med.jpg", + :large "http://cloudfront.net/865964d4-35c0-40e2-9029-584fc6f393ce/large.jpg"} + {:name "Kyle's Free-Range Taqueria", :categories ["Free-Range" "Taqueria"], :phone "415-201-7832", :id "7aeb9416-4fe6-45f9-8849-6b8ba6d3f3b9"} + {:service "foursquare", :foursquare-photo-id "6c35de08-10de-4ca4-acb8-a659d005f73c", :mayor "bob"}] + ["Sunset Deep-Dish Hotel & Restaurant is a modern and acceptable place to take visiting friends and relatives the second Saturday of the month." + {:small "http://cloudfront.net/896eef7b-8a70-4706-a70f-f8fbf75acffc/small.jpg", + :medium "http://cloudfront.net/896eef7b-8a70-4706-a70f-f8fbf75acffc/med.jpg", + :large "http://cloudfront.net/896eef7b-8a70-4706-a70f-f8fbf75acffc/large.jpg"} + {:name "Sunset Deep-Dish Hotel & Restaurant", :categories ["Deep-Dish" "Hotel & Restaurant"], :phone "415-332-0978", :id "a80745c7-af74-4579-8932-70dd488269e6"} + {:service "foursquare", :foursquare-photo-id "6b2657cc-f404-4507-99d0-89df941c8c19", :mayor "jessica"}] + ["Rasta's European Taqueria is a fantastic and great place to watch the Warriors game weekend mornings." + {:small "http://cloudfront.net/11b7f47b-1a68-43a9-b3be-0228fa19c70c/small.jpg", + :medium "http://cloudfront.net/11b7f47b-1a68-43a9-b3be-0228fa19c70c/med.jpg", + :large "http://cloudfront.net/11b7f47b-1a68-43a9-b3be-0228fa19c70c/large.jpg"} + {:name "Rasta's European Taqueria", :categories ["European" "Taqueria"], :phone "415-631-1599", :id "cb472880-ee6e-46e3-bd58-22cf33109aba"} + {:service "yelp", :yelp-photo-id "d2f7fc44-73f5-4c46-b6ac-b4407bb0df0e", :categories ["European" "Taqueria"]}] + ["Joe's Homestyle Eatery is a amazing and delicious place to have a drink when hungover." + {:small "http://cloudfront.net/90957931-5a61-49dd-ab52-bdcf648e370f/small.jpg", + :medium "http://cloudfront.net/90957931-5a61-49dd-ab52-bdcf648e370f/med.jpg", + :large "http://cloudfront.net/90957931-5a61-49dd-ab52-bdcf648e370f/large.jpg"} + {:name "Joe's Homestyle Eatery", :categories ["Homestyle" "Eatery"], :phone "415-950-1337", :id "5cc18489-dfaf-417b-900f-5d1d61b961e8"} + {:service "yelp", :yelp-photo-id "e5070b25-6933-44e9-a96b-bc7273c13616", :categories ["Homestyle" "Eatery"]}] + ["SoMa Japanese Churros is a acceptable and horrible place to have a birthday party with your pet dog." + {:small "http://cloudfront.net/91e5dd78-7a6d-4384-8252-943b8a860a2d/small.jpg", + :medium "http://cloudfront.net/91e5dd78-7a6d-4384-8252-943b8a860a2d/med.jpg", + :large "http://cloudfront.net/91e5dd78-7a6d-4384-8252-943b8a860a2d/large.jpg"} + {:name "SoMa Japanese Churros", :categories ["Japanese" "Churros"], :phone "415-404-1510", :id "373858b2-e634-45d0-973d-4d0fed8c438b"} + {:service "flare", :username "tupac"}] + ["Haight Soul Food Pop-Up Food Stand is a groovy and swell place to have breakfast when hungover." + {:small "http://cloudfront.net/4337bc10-99dc-4f86-a9c2-a64e9d1804da/small.jpg", + :medium "http://cloudfront.net/4337bc10-99dc-4f86-a9c2-a64e9d1804da/med.jpg", + :large "http://cloudfront.net/4337bc10-99dc-4f86-a9c2-a64e9d1804da/large.jpg"} + {:name "Haight Soul Food Pop-Up Food Stand", :categories ["Soul Food" "Pop-Up Food Stand"], :phone "415-741-8726", :id "9735184b-1299-410f-a98e-10d9c548af42"} + {:service "flare", :username "amy"}] + ["Mission Free-Range Liquor Store is a exclusive and horrible place to have a birthday party Friday nights." + {:small "http://cloudfront.net/ba5c2732-f133-41c7-b3aa-4b627710733b/small.jpg", + :medium "http://cloudfront.net/ba5c2732-f133-41c7-b3aa-4b627710733b/med.jpg", + :large "http://cloudfront.net/ba5c2732-f133-41c7-b3aa-4b627710733b/large.jpg"} + {:name "Mission Free-Range Liquor Store", :categories ["Free-Range" "Liquor Store"], :phone "415-041-3816", :id "6e665924-8e2c-42ab-af58-23a27f017e37"} + {:service "foursquare", :foursquare-photo-id "532267f2-2b66-4adc-8c51-b8ee4509e548", :mayor "kyle"}] + ["Rasta's Paleo Café is a delicious and delicious place to drink a craft beer in June." + {:small "http://cloudfront.net/c8302d26-6053-438a-9ac4-979699496d0f/small.jpg", + :medium "http://cloudfront.net/c8302d26-6053-438a-9ac4-979699496d0f/med.jpg", + :large "http://cloudfront.net/c8302d26-6053-438a-9ac4-979699496d0f/large.jpg"} + {:name "Rasta's Paleo Café", :categories ["Paleo" "Café"], :phone "415-392-6341", :id "4f9e69be-f06c-46a0-bb8b-f3ddd8218ca1"} + {:service "facebook", :facebook-photo-id "7b6c29d2-078d-4feb-af95-88f10dc8937b", :url "http://facebook.com/photos/7b6c29d2-078d-4feb-af95-88f10dc8937b"}] + ["Mission Soul Food Pizzeria is a family-friendly and acceptable place to sip Champagne in June." + {:small "http://cloudfront.net/1ec9c182-6624-4444-8a7f-71c2058c0b0d/small.jpg", + :medium "http://cloudfront.net/1ec9c182-6624-4444-8a7f-71c2058c0b0d/med.jpg", + :large "http://cloudfront.net/1ec9c182-6624-4444-8a7f-71c2058c0b0d/large.jpg"} + {:name "Mission Soul Food Pizzeria", :categories ["Soul Food" "Pizzeria"], :phone "415-437-3479", :id "9905fe61-44cb-4626-843b-5d725c7949bb"} + {:service "twitter", :mentions ["@mission_soul_food_pizzeria"], :tags ["#soul" "#food" "#pizzeria"], :username "cam_saul"}] + ["Tenderloin Japanese Ice Cream Truck is a horrible and decent place to pitch an investor on Saturday night." + {:small "http://cloudfront.net/f818171e-344f-45e2-b0b7-d1ef9fc75866/small.jpg", + :medium "http://cloudfront.net/f818171e-344f-45e2-b0b7-d1ef9fc75866/med.jpg", + :large "http://cloudfront.net/f818171e-344f-45e2-b0b7-d1ef9fc75866/large.jpg"} + {:name "Tenderloin Japanese Ice Cream Truck", :categories ["Japanese" "Ice Cream Truck"], :phone "415-856-0371", :id "5ce47baa-bbef-4bc7-adf6-57842913ea8a"} + {:service "flare", :username "bob"}] + ["Tenderloin Paleo Hotel & Restaurant is a decent and great place to drink a craft beer on Thursdays." + {:small "http://cloudfront.net/33738a8e-ee82-45da-919d-e8739f7600e8/small.jpg", + :medium "http://cloudfront.net/33738a8e-ee82-45da-919d-e8739f7600e8/med.jpg", + :large "http://cloudfront.net/33738a8e-ee82-45da-919d-e8739f7600e8/large.jpg"} + {:name "Tenderloin Paleo Hotel & Restaurant", :categories ["Paleo" "Hotel & Restaurant"], :phone "415-402-1652", :id "4dea27b4-6d89-4b86-80a8-5631e171da8d"} + {:service "flare", :username "amy"}] + ["Lucky's Low-Carb Coffee House is a horrible and historical place to take a date on a Tuesday afternoon." + {:small "http://cloudfront.net/e81790b7-b61d-4c97-a11d-d29414d5f3f6/small.jpg", + :medium "http://cloudfront.net/e81790b7-b61d-4c97-a11d-d29414d5f3f6/med.jpg", + :large "http://cloudfront.net/e81790b7-b61d-4c97-a11d-d29414d5f3f6/large.jpg"} + {:name "Lucky's Low-Carb Coffee House", :categories ["Low-Carb" "Coffee House"], :phone "415-145-7107", :id "81b0f944-f0ce-45e5-b84e-a924c441064a"} + {:service "flare", :username "kyle"}] + ["Nob Hill Korean Taqueria is a atmospheric and horrible place to sip Champagne on Saturday night." + {:small "http://cloudfront.net/6444cc6c-ec22-4e3c-acfe-d1cf028d85b4/small.jpg", + :medium "http://cloudfront.net/6444cc6c-ec22-4e3c-acfe-d1cf028d85b4/med.jpg", + :large "http://cloudfront.net/6444cc6c-ec22-4e3c-acfe-d1cf028d85b4/large.jpg"} + {:name "Nob Hill Korean Taqueria", :categories ["Korean" "Taqueria"], :phone "415-107-7332", :id "a43c184c-90f5-488c-bb3b-00ea2666d90e"} + {:service "flare", :username "bob"}] + ["Rasta's Mexican Sushi is a popular and amazing place to nurse a hangover on a Tuesday afternoon." + {:small "http://cloudfront.net/586597a8-4d55-4d75-9146-55988033886c/small.jpg", + :medium "http://cloudfront.net/586597a8-4d55-4d75-9146-55988033886c/med.jpg", + :large "http://cloudfront.net/586597a8-4d55-4d75-9146-55988033886c/large.jpg"} + {:name "Rasta's Mexican Sushi", :categories ["Mexican" "Sushi"], :phone "415-387-1284", :id "e4912a22-e6ac-4806-8377-6497bf533a21"} + {:service "foursquare", :foursquare-photo-id "0bb00482-d502-4381-9efb-975487a53dc8", :mayor "cam_saul"}] + ["Haight Mexican Restaurant is a popular and underground place to drink a craft beer with your pet dog." + {:small "http://cloudfront.net/62670450-c60f-4292-b667-4aed5e1ea266/small.jpg", + :medium "http://cloudfront.net/62670450-c60f-4292-b667-4aed5e1ea266/med.jpg", + :large "http://cloudfront.net/62670450-c60f-4292-b667-4aed5e1ea266/large.jpg"} + {:name "Haight Mexican Restaurant", :categories ["Mexican" "Restaurant"], :phone "415-758-8690", :id "a5e3f0ac-f6e8-4e71-a0a2-3f10f48b4ab1"} + {:service "foursquare", :foursquare-photo-id "9c94e761-e464-419e-8696-4670f1d9672c", :mayor "amy"}] + ["SoMa British Bakery is a swell and fantastic place to take visiting friends and relatives on Saturday night." + {:small "http://cloudfront.net/4dc5fa04-8931-456e-b05c-3056fb4b4678/small.jpg", + :medium "http://cloudfront.net/4dc5fa04-8931-456e-b05c-3056fb4b4678/med.jpg", + :large "http://cloudfront.net/4dc5fa04-8931-456e-b05c-3056fb4b4678/large.jpg"} + {:name "SoMa British Bakery", :categories ["British" "Bakery"], :phone "415-909-5728", :id "662cb0d0-8ee6-4db7-aaf1-89eb2530feda"} + {:service "flare", :username "jessica"}] + ["Market St. Gluten-Free Café is a historical and acceptable place to meet new friends during winter." + {:small "http://cloudfront.net/69b12faf-ddb6-491a-986b-a432c44ec881/small.jpg", + :medium "http://cloudfront.net/69b12faf-ddb6-491a-986b-a432c44ec881/med.jpg", + :large "http://cloudfront.net/69b12faf-ddb6-491a-986b-a432c44ec881/large.jpg"} + {:name "Market St. Gluten-Free Café", :categories ["Gluten-Free" "Café"], :phone "415-697-9776", :id "ce4947d0-071a-4491-b5ac-f8b0241bd54c"} + {:service "twitter", :mentions ["@market_st._gluten_free_café"], :tags ["#gluten-free" "#café"], :username "mandy"}] + ["Haight Soul Food Hotel & Restaurant is a amazing and underground place to watch the Warriors game in the spring." + {:small "http://cloudfront.net/8ed4e7fd-b105-4b49-8785-01fb01b56408/small.jpg", + :medium "http://cloudfront.net/8ed4e7fd-b105-4b49-8785-01fb01b56408/med.jpg", + :large "http://cloudfront.net/8ed4e7fd-b105-4b49-8785-01fb01b56408/large.jpg"} + {:name "Haight Soul Food Hotel & Restaurant", :categories ["Soul Food" "Hotel & Restaurant"], :phone "415-786-9541", :id "11a72eb3-9e96-4703-9a01-e8c2a9469046"} + {:service "twitter", :mentions ["@haight_soul_food_hotel_&_restaurant"], :tags ["#soul" "#food" "#hotel" "#&" "#restaurant"], :username "joe"}] + ["Haight Chinese Gastro Pub is a world-famous and popular place to conduct a business meeting in July." + {:small "http://cloudfront.net/65330afd-5c4c-4647-87c6-ac687ea7b542/small.jpg", + :medium "http://cloudfront.net/65330afd-5c4c-4647-87c6-ac687ea7b542/med.jpg", + :large "http://cloudfront.net/65330afd-5c4c-4647-87c6-ac687ea7b542/large.jpg"} + {:name "Haight Chinese Gastro Pub", :categories ["Chinese" "Gastro Pub"], :phone "415-521-5825", :id "12a8dc6e-1b2c-47e2-9c18-3ae220e4806f"} + {:service "yelp", :yelp-photo-id "d6adf74a-ac43-4a0d-8895-8a026f4577d6", :categories ["Chinese" "Gastro Pub"]}] + ["Polk St. Red White & Blue Café is a groovy and world-famous place to nurse a hangover on Taco Tuesday." + {:small "http://cloudfront.net/74df80a0-ead8-4389-a23c-b7126f5898fe/small.jpg", + :medium "http://cloudfront.net/74df80a0-ead8-4389-a23c-b7126f5898fe/med.jpg", + :large "http://cloudfront.net/74df80a0-ead8-4389-a23c-b7126f5898fe/large.jpg"} + {:name "Polk St. Red White & Blue Café", :categories ["Red White & Blue" "Café"], :phone "415-986-0661", :id "6eb9d2db-5015-49f0-bd18-f4c4938b7e5a"} + {:service "yelp", :yelp-photo-id "c4683200-8e6c-4e0f-801b-8b5c69263c82", :categories ["Red White & Blue" "Café"]}] + ["Rasta's Old-Fashioned Pop-Up Food Stand is a modern and great place to have a after-work cocktail after baseball games." + {:small "http://cloudfront.net/104a8586-58c9-4591-a46e-0b37a6a22078/small.jpg", + :medium "http://cloudfront.net/104a8586-58c9-4591-a46e-0b37a6a22078/med.jpg", + :large "http://cloudfront.net/104a8586-58c9-4591-a46e-0b37a6a22078/large.jpg"} + {:name "Rasta's Old-Fashioned Pop-Up Food Stand", :categories ["Old-Fashioned" "Pop-Up Food Stand"], :phone "415-942-1875", :id "9fd8b920-a877-4888-86bf-578b2724ac4e"} + {:service "facebook", :facebook-photo-id "0a9c5d13-ab77-4b81-9404-78519055f74f", :url "http://facebook.com/photos/0a9c5d13-ab77-4b81-9404-78519055f74f"}] + ["Rasta's European Taqueria is a acceptable and overrated place to have brunch during winter." + {:small "http://cloudfront.net/62cbd734-78f4-4309-afbd-4399f6c19325/small.jpg", + :medium "http://cloudfront.net/62cbd734-78f4-4309-afbd-4399f6c19325/med.jpg", + :large "http://cloudfront.net/62cbd734-78f4-4309-afbd-4399f6c19325/large.jpg"} + {:name "Rasta's European Taqueria", :categories ["European" "Taqueria"], :phone "415-631-1599", :id "cb472880-ee6e-46e3-bd58-22cf33109aba"} + {:service "flare", :username "lucky_pigeon"}] + ["Haight Soul Food Café is a overrated and historical place to watch the Warriors game when hungover." + {:small "http://cloudfront.net/85edb88a-a7b8-4c77-aa67-cb937406f07e/small.jpg", + :medium "http://cloudfront.net/85edb88a-a7b8-4c77-aa67-cb937406f07e/med.jpg", + :large "http://cloudfront.net/85edb88a-a7b8-4c77-aa67-cb937406f07e/large.jpg"} + {:name "Haight Soul Food Café", :categories ["Soul Food" "Café"], :phone "415-257-1769", :id "a1796c4b-da2b-474f-9fd6-4fa96c1eac70"} + {:service "facebook", :facebook-photo-id "38bdb459-90a7-45d2-815b-ac50f4b945b2", :url "http://facebook.com/photos/38bdb459-90a7-45d2-815b-ac50f4b945b2"}] + ["Lower Pac Heights Cage-Free Coffee House is a exclusive and amazing place to pitch an investor with your pet dog." + {:small "http://cloudfront.net/2d605283-ac21-4fbb-8fcc-b0c237d95998/small.jpg", + :medium "http://cloudfront.net/2d605283-ac21-4fbb-8fcc-b0c237d95998/med.jpg", + :large "http://cloudfront.net/2d605283-ac21-4fbb-8fcc-b0c237d95998/large.jpg"} + {:name "Lower Pac Heights Cage-Free Coffee House", :categories ["Cage-Free" "Coffee House"], :phone "415-697-9309", :id "02b1f618-41a0-406b-96dd-1a017f630b81"} + {:service "yelp", :yelp-photo-id "28eec82b-1a47-4e75-970f-ae118d14fcc0", :categories ["Cage-Free" "Coffee House"]}] + ["Haight Gormet Pizzeria is a overrated and popular place to have a after-work cocktail with your pet dog." + {:small "http://cloudfront.net/36ea3dc3-e9f5-4306-8b38-267eb0080754/small.jpg", + :medium "http://cloudfront.net/36ea3dc3-e9f5-4306-8b38-267eb0080754/med.jpg", + :large "http://cloudfront.net/36ea3dc3-e9f5-4306-8b38-267eb0080754/large.jpg"} + {:name "Haight Gormet Pizzeria", :categories ["Gormet" "Pizzeria"], :phone "415-869-2197", :id "0425bdd0-3f57-4108-80e3-78335327355a"} + {:service "foursquare", :foursquare-photo-id "7ad05d8f-924b-4e77-8222-54704df41a83", :mayor "amy"}] + ["SF Deep-Dish Eatery is a exclusive and classic place to have a after-work cocktail the first Sunday of the month." + {:small "http://cloudfront.net/ade9dbce-8bef-4aa4-836c-3dc0ef024976/small.jpg", + :medium "http://cloudfront.net/ade9dbce-8bef-4aa4-836c-3dc0ef024976/med.jpg", + :large "http://cloudfront.net/ade9dbce-8bef-4aa4-836c-3dc0ef024976/large.jpg"} + {:name "SF Deep-Dish Eatery", :categories ["Deep-Dish" "Eatery"], :phone "415-476-9257", :id "ad41d3f6-c20c-46a7-9e5d-db602fff7d0d"} + {:service "facebook", :facebook-photo-id "e149be3f-de3b-4f90-a3d5-fa5a51362a45", :url "http://facebook.com/photos/e149be3f-de3b-4f90-a3d5-fa5a51362a45"}] + ["Cam's Soul Food Ice Cream Truck is a overrated and underground place to sip a glass of expensive wine on public holidays." + {:small "http://cloudfront.net/30c66e10-c0dc-4439-9d3a-485ebec2b984/small.jpg", + :medium "http://cloudfront.net/30c66e10-c0dc-4439-9d3a-485ebec2b984/med.jpg", + :large "http://cloudfront.net/30c66e10-c0dc-4439-9d3a-485ebec2b984/large.jpg"} + {:name "Cam's Soul Food Ice Cream Truck", :categories ["Soul Food" "Ice Cream Truck"], :phone "415-270-8888", :id "f474e587-1801-43ea-93d5-4c4fd96460b8"} + {:service "yelp", :yelp-photo-id "75345d37-3a17-48ac-926c-707fdd1ed073", :categories ["Soul Food" "Ice Cream Truck"]}] + ["Haight Soul Food Hotel & Restaurant is a horrible and underappreciated place to have brunch during summer." + {:small "http://cloudfront.net/80b32b4f-9d75-4494-bec3-0e4d1c77e592/small.jpg", + :medium "http://cloudfront.net/80b32b4f-9d75-4494-bec3-0e4d1c77e592/med.jpg", + :large "http://cloudfront.net/80b32b4f-9d75-4494-bec3-0e4d1c77e592/large.jpg"} + {:name "Haight Soul Food Hotel & Restaurant", :categories ["Soul Food" "Hotel & Restaurant"], :phone "415-786-9541", :id "11a72eb3-9e96-4703-9a01-e8c2a9469046"} + {:service "twitter", :mentions ["@haight_soul_food_hotel_&_restaurant"], :tags ["#soul" "#food" "#hotel" "#&" "#restaurant"], :username "jane"}] + ["Kyle's Japanese Hotel & Restaurant is a well-decorated and underappreciated place to drink a craft beer weekday afternoons." + {:small "http://cloudfront.net/375174fb-6d46-4034-aa46-e69d8501c16f/small.jpg", + :medium "http://cloudfront.net/375174fb-6d46-4034-aa46-e69d8501c16f/med.jpg", + :large "http://cloudfront.net/375174fb-6d46-4034-aa46-e69d8501c16f/large.jpg"} + {:name "Kyle's Japanese Hotel & Restaurant", :categories ["Japanese" "Hotel & Restaurant"], :phone "415-337-5387", :id "eced4f41-b627-4553-a297-888871038b69"} + {:service "facebook", :facebook-photo-id "7c8ab6a5-f832-4d16-9f5a-e5bb64991845", :url "http://facebook.com/photos/7c8ab6a5-f832-4d16-9f5a-e5bb64991845"}] + ["Sameer's GMO-Free Restaurant is a underappreciated and well-decorated place to take a date Friday nights." + {:small "http://cloudfront.net/f513fc07-1819-4e27-8459-621ee8daa89b/small.jpg", + :medium "http://cloudfront.net/f513fc07-1819-4e27-8459-621ee8daa89b/med.jpg", + :large "http://cloudfront.net/f513fc07-1819-4e27-8459-621ee8daa89b/large.jpg"} + {:name "Sameer's GMO-Free Restaurant", :categories ["GMO-Free" "Restaurant"], :phone "415-128-9430", :id "7ac8a7dd-c07f-45a6-92ba-bdb1b1280af2"} + {:service "flare", :username "rasta_toucan"}] + ["Kyle's Free-Range Taqueria is a groovy and classic place to pitch an investor the second Saturday of the month." + {:small "http://cloudfront.net/3496607d-7f71-4f4d-a3fb-38a93cb7dc9f/small.jpg", + :medium "http://cloudfront.net/3496607d-7f71-4f4d-a3fb-38a93cb7dc9f/med.jpg", + :large "http://cloudfront.net/3496607d-7f71-4f4d-a3fb-38a93cb7dc9f/large.jpg"} + {:name "Kyle's Free-Range Taqueria", :categories ["Free-Range" "Taqueria"], :phone "415-201-7832", :id "7aeb9416-4fe6-45f9-8849-6b8ba6d3f3b9"} + {:service "flare", :username "cam_saul"}] + ["Lucky's Gluten-Free Gastro Pub is a atmospheric and wonderful place to drink a craft beer on Thursdays." + {:small "http://cloudfront.net/5e5e3ec3-45af-41e3-90fe-aa4923bfff0c/small.jpg", + :medium "http://cloudfront.net/5e5e3ec3-45af-41e3-90fe-aa4923bfff0c/med.jpg", + :large "http://cloudfront.net/5e5e3ec3-45af-41e3-90fe-aa4923bfff0c/large.jpg"} + {:name "Lucky's Gluten-Free Gastro Pub", :categories ["Gluten-Free" "Gastro Pub"], :phone "415-391-6443", :id "7ccf8bbb-74b4-48f7-aa7c-43872f63cb1b"} + {:service "yelp", :yelp-photo-id "94ee2c9f-bfcc-462e-9893-6a740cd89edf", :categories ["Gluten-Free" "Gastro Pub"]}] + ["Cam's Mexican Gastro Pub is a fantastic and great place to nurse a hangover the second Saturday of the month." + {:small "http://cloudfront.net/1682e808-cd81-455c-a146-310951ee9a48/small.jpg", + :medium "http://cloudfront.net/1682e808-cd81-455c-a146-310951ee9a48/med.jpg", + :large "http://cloudfront.net/1682e808-cd81-455c-a146-310951ee9a48/large.jpg"} + {:name "Cam's Mexican Gastro Pub", :categories ["Mexican" "Gastro Pub"], :phone "415-320-9123", :id "bb958ac5-758e-4f42-b984-6b0e13f25194"} + {:service "twitter", :mentions ["@cams_mexican_gastro_pub"], :tags ["#mexican" "#gastro" "#pub"], :username "cam_saul"}] + ["Marina Cage-Free Liquor Store is a classic and family-friendly place to watch the Giants game weekend evenings." + {:small "http://cloudfront.net/4ad99b2b-e8c3-4ee0-b689-4bf4e5f6afc4/small.jpg", + :medium "http://cloudfront.net/4ad99b2b-e8c3-4ee0-b689-4bf4e5f6afc4/med.jpg", + :large "http://cloudfront.net/4ad99b2b-e8c3-4ee0-b689-4bf4e5f6afc4/large.jpg"} + {:name "Marina Cage-Free Liquor Store", :categories ["Cage-Free" "Liquor Store"], :phone "415-571-0783", :id "ad68f549-3000-407e-ab98-7f314cfa4653"} + {:service "yelp", :yelp-photo-id "3bd9b0c5-4bb3-492f-9f79-b95540dce0a5", :categories ["Cage-Free" "Liquor Store"]}] + ["Lucky's Cage-Free Liquor Store is a exclusive and classic place to catch a bite to eat with your pet dog." + {:small "http://cloudfront.net/ea6196c8-f2da-4765-9705-24e9d74314a0/small.jpg", + :medium "http://cloudfront.net/ea6196c8-f2da-4765-9705-24e9d74314a0/med.jpg", + :large "http://cloudfront.net/ea6196c8-f2da-4765-9705-24e9d74314a0/large.jpg"} + {:name "Lucky's Cage-Free Liquor Store", :categories ["Cage-Free" "Liquor Store"], :phone "415-341-3219", :id "968eac8d-1fd0-443d-a7ff-b48810ab0f69"} + {:service "facebook", :facebook-photo-id "ed8f13c6-cb45-4dd5-ab2b-9e5261fe3e7b", :url "http://facebook.com/photos/ed8f13c6-cb45-4dd5-ab2b-9e5261fe3e7b"}] + ["Tenderloin Cage-Free Sushi is a well-decorated and popular place to have brunch on Saturday night." + {:small "http://cloudfront.net/4e19c71c-f413-4e9e-a802-df85a3ea1ca8/small.jpg", + :medium "http://cloudfront.net/4e19c71c-f413-4e9e-a802-df85a3ea1ca8/med.jpg", + :large "http://cloudfront.net/4e19c71c-f413-4e9e-a802-df85a3ea1ca8/large.jpg"} + {:name "Tenderloin Cage-Free Sushi", :categories ["Cage-Free" "Sushi"], :phone "415-348-0644", :id "0b6c036f-82b0-4008-bdfe-5360dd93fb75"} + {:service "flare", :username "amy"}] + ["SoMa Old-Fashioned Pizzeria is a decent and well-decorated place to have a birthday party after baseball games." + {:small "http://cloudfront.net/2e81d020-d65b-43ac-abad-32c74d5890b8/small.jpg", + :medium "http://cloudfront.net/2e81d020-d65b-43ac-abad-32c74d5890b8/med.jpg", + :large "http://cloudfront.net/2e81d020-d65b-43ac-abad-32c74d5890b8/large.jpg"} + {:name "SoMa Old-Fashioned Pizzeria", :categories ["Old-Fashioned" "Pizzeria"], :phone "415-966-8856", :id "deb8997b-734d-402b-a181-bd888214bc86"} + {:service "twitter", :mentions ["@soma_old_fashioned_pizzeria"], :tags ["#old-fashioned" "#pizzeria"], :username "bob"}] + ["Marina Homestyle Pop-Up Food Stand is a groovy and great place to conduct a business meeting with your pet toucan." + {:small "http://cloudfront.net/2cf13084-184a-4a3b-9c64-9732b00b2410/small.jpg", + :medium "http://cloudfront.net/2cf13084-184a-4a3b-9c64-9732b00b2410/med.jpg", + :large "http://cloudfront.net/2cf13084-184a-4a3b-9c64-9732b00b2410/large.jpg"} + {:name "Marina Homestyle Pop-Up Food Stand", :categories ["Homestyle" "Pop-Up Food Stand"], :phone "415-094-4567", :id "88a7ae3c-8b36-4901-a0c5-b82342cba6cd"} + {:service "flare", :username "sameer"}] + ["Sameer's Pizza Liquor Store is a world-famous and underappreciated place to have breakfast the first Sunday of the month." + {:small "http://cloudfront.net/2805f64d-9ee0-41e7-b07e-cffe37193efb/small.jpg", + :medium "http://cloudfront.net/2805f64d-9ee0-41e7-b07e-cffe37193efb/med.jpg", + :large "http://cloudfront.net/2805f64d-9ee0-41e7-b07e-cffe37193efb/large.jpg"} + {:name "Sameer's Pizza Liquor Store", :categories ["Pizza" "Liquor Store"], :phone "415-969-7474", :id "7b9c7dc3-d8f1-498d-843a-e62360449892"} + {:service "foursquare", :foursquare-photo-id "a06600a6-ab71-4c47-bf7d-9334f0de14a6", :mayor "jessica"}] + ["Mission BBQ Churros is a underground and overrated place to have breakfast Friday nights." + {:small "http://cloudfront.net/e2e3cd36-f1f4-4a23-b1db-166d87e4bba2/small.jpg", + :medium "http://cloudfront.net/e2e3cd36-f1f4-4a23-b1db-166d87e4bba2/med.jpg", + :large "http://cloudfront.net/e2e3cd36-f1f4-4a23-b1db-166d87e4bba2/large.jpg"} + {:name "Mission BBQ Churros", :categories ["BBQ" "Churros"], :phone "415-406-5374", :id "429ea81a-02c5-449f-bfa7-03a11b227f1f"} + {:service "yelp", :yelp-photo-id "500335c8-a480-4d0a-b2e5-fd44f240c668", :categories ["BBQ" "Churros"]}] + ["Kyle's Old-Fashioned Pop-Up Food Stand is a amazing and underappreciated place to catch a bite to eat with friends." + {:small "http://cloudfront.net/ee0eca87-45da-43e0-b8ad-15e80d4fd00a/small.jpg", + :medium "http://cloudfront.net/ee0eca87-45da-43e0-b8ad-15e80d4fd00a/med.jpg", + :large "http://cloudfront.net/ee0eca87-45da-43e0-b8ad-15e80d4fd00a/large.jpg"} + {:name "Kyle's Old-Fashioned Pop-Up Food Stand", :categories ["Old-Fashioned" "Pop-Up Food Stand"], :phone "415-638-8972", :id "7da187e8-bd01-48ca-ad93-7a02a442d9eb"} + {:service "foursquare", :foursquare-photo-id "dab0b46c-7062-4a40-8098-290e126c47df", :mayor "lucky_pigeon"}] + ["Nob Hill Free-Range Ice Cream Truck is a decent and classic place to meet new friends during winter." + {:small "http://cloudfront.net/98aa0ae9-fc5c-4dad-a643-8d3415b7a95d/small.jpg", + :medium "http://cloudfront.net/98aa0ae9-fc5c-4dad-a643-8d3415b7a95d/med.jpg", + :large "http://cloudfront.net/98aa0ae9-fc5c-4dad-a643-8d3415b7a95d/large.jpg"} + {:name "Nob Hill Free-Range Ice Cream Truck", :categories ["Free-Range" "Ice Cream Truck"], :phone "415-787-4049", :id "08d1e93c-105f-4abf-a9ec-b2e3cd30747e"} + {:service "flare", :username "rasta_toucan"}] + ["Haight Chinese Gastro Pub is a world-famous and acceptable place to have a birthday party with your pet dog." + {:small "http://cloudfront.net/faeeb454-e78f-4f14-84db-4b7acaafe5bb/small.jpg", + :medium "http://cloudfront.net/faeeb454-e78f-4f14-84db-4b7acaafe5bb/med.jpg", + :large "http://cloudfront.net/faeeb454-e78f-4f14-84db-4b7acaafe5bb/large.jpg"} + {:name "Haight Chinese Gastro Pub", :categories ["Chinese" "Gastro Pub"], :phone "415-521-5825", :id "12a8dc6e-1b2c-47e2-9c18-3ae220e4806f"} + {:service "foursquare", :foursquare-photo-id "a061d2b6-8c85-4bf4-bc25-9450136340ca", :mayor "rasta_toucan"}] + ["Sameer's Pizza Liquor Store is a world-famous and historical place to catch a bite to eat after baseball games." + {:small "http://cloudfront.net/ff6df82c-49f2-4061-9369-51ff3d043f61/small.jpg", + :medium "http://cloudfront.net/ff6df82c-49f2-4061-9369-51ff3d043f61/med.jpg", + :large "http://cloudfront.net/ff6df82c-49f2-4061-9369-51ff3d043f61/large.jpg"} + {:name "Sameer's Pizza Liquor Store", :categories ["Pizza" "Liquor Store"], :phone "415-969-7474", :id "7b9c7dc3-d8f1-498d-843a-e62360449892"} + {:service "foursquare", :foursquare-photo-id "a45a7d9f-e866-4edf-9594-4229dc893000", :mayor "biggie"}] + ["Market St. Low-Carb Taqueria is a well-decorated and modern place to have breakfast weekday afternoons." + {:small "http://cloudfront.net/86ecd1d3-5d67-4ff2-9839-4c5858ac1013/small.jpg", + :medium "http://cloudfront.net/86ecd1d3-5d67-4ff2-9839-4c5858ac1013/med.jpg", + :large "http://cloudfront.net/86ecd1d3-5d67-4ff2-9839-4c5858ac1013/large.jpg"} + {:name "Market St. Low-Carb Taqueria", :categories ["Low-Carb" "Taqueria"], :phone "415-751-6525", :id "f30eb85b-f048-4d8c-8008-3c2876125061"} + {:service "facebook", :facebook-photo-id "b02f088b-0b6e-4543-bb64-fc46173488c6", :url "http://facebook.com/photos/b02f088b-0b6e-4543-bb64-fc46173488c6"}] + ["Joe's Homestyle Eatery is a horrible and historical place to sip a glass of expensive wine Friday nights." + {:small "http://cloudfront.net/1f29c79b-7119-4f34-aec1-bc3863b6f392/small.jpg", + :medium "http://cloudfront.net/1f29c79b-7119-4f34-aec1-bc3863b6f392/med.jpg", + :large "http://cloudfront.net/1f29c79b-7119-4f34-aec1-bc3863b6f392/large.jpg"} + {:name "Joe's Homestyle Eatery", :categories ["Homestyle" "Eatery"], :phone "415-950-1337", :id "5cc18489-dfaf-417b-900f-5d1d61b961e8"} + {:service "twitter", :mentions ["@joes_homestyle_eatery"], :tags ["#homestyle" "#eatery"], :username "jane"}] + ["Market St. Homestyle Pop-Up Food Stand is a popular and delicious place to conduct a business meeting weekday afternoons." + {:small "http://cloudfront.net/6caf3094-8900-4461-8b3a-f6c43b3cea3d/small.jpg", + :medium "http://cloudfront.net/6caf3094-8900-4461-8b3a-f6c43b3cea3d/med.jpg", + :large "http://cloudfront.net/6caf3094-8900-4461-8b3a-f6c43b3cea3d/large.jpg"} + {:name "Market St. Homestyle Pop-Up Food Stand", :categories ["Homestyle" "Pop-Up Food Stand"], :phone "415-213-3030", :id "2d873280-e43d-449e-9940-af96ae7df718"} + {:service "flare", :username "cam_saul"}] + ["Lucky's Afgan Sushi is a historical and fantastic place to catch a bite to eat in the spring." + {:small "http://cloudfront.net/f3fb00c6-96fe-4b76-ba66-118e26328fd6/small.jpg", + :medium "http://cloudfront.net/f3fb00c6-96fe-4b76-ba66-118e26328fd6/med.jpg", + :large "http://cloudfront.net/f3fb00c6-96fe-4b76-ba66-118e26328fd6/large.jpg"} + {:name "Lucky's Afgan Sushi", :categories ["Afgan" "Sushi"], :phone "415-188-3506", :id "4a47d0d2-0123-4bb9-b941-38702f0697e9"} + {:service "facebook", :facebook-photo-id "97487fd1-61b3-4492-9d4f-45ceba863252", :url "http://facebook.com/photos/97487fd1-61b3-4492-9d4f-45ceba863252"}] + ["Market St. Homestyle Pop-Up Food Stand is a overrated and wonderful place to drink a craft beer with your pet toucan." + {:small "http://cloudfront.net/af633118-4ae8-450f-b48b-d02d86913e22/small.jpg", + :medium "http://cloudfront.net/af633118-4ae8-450f-b48b-d02d86913e22/med.jpg", + :large "http://cloudfront.net/af633118-4ae8-450f-b48b-d02d86913e22/large.jpg"} + {:name "Market St. Homestyle Pop-Up Food Stand", :categories ["Homestyle" "Pop-Up Food Stand"], :phone "415-213-3030", :id "2d873280-e43d-449e-9940-af96ae7df718"} + {:service "flare", :username "tupac"}] + ["Sunset Homestyle Grill is a acceptable and classic place to meet new friends in July." + {:small "http://cloudfront.net/e495076c-d822-460c-8e1e-eb76d9378433/small.jpg", + :medium "http://cloudfront.net/e495076c-d822-460c-8e1e-eb76d9378433/med.jpg", + :large "http://cloudfront.net/e495076c-d822-460c-8e1e-eb76d9378433/large.jpg"} + {:name "Sunset Homestyle Grill", :categories ["Homestyle" "Grill"], :phone "415-356-7052", :id "c57673cd-f2d0-4bbc-aed0-6c166d7cf2c3"} + {:service "twitter", :mentions ["@sunset_homestyle_grill"], :tags ["#homestyle" "#grill"], :username "mandy"}] + ["Polk St. Mexican Coffee House is a overrated and great place to meet new friends on Thursdays." + {:small "http://cloudfront.net/7f2cad4a-5bae-4860-af42-59af2b94c90f/small.jpg", + :medium "http://cloudfront.net/7f2cad4a-5bae-4860-af42-59af2b94c90f/med.jpg", + :large "http://cloudfront.net/7f2cad4a-5bae-4860-af42-59af2b94c90f/large.jpg"} + {:name "Polk St. Mexican Coffee House", :categories ["Mexican" "Coffee House"], :phone "415-144-7901", :id "396d36d7-13ad-41fd-86b5-8b70b6ecdabf"} + {:service "twitter", :mentions ["@polk_st._mexican_coffee_house"], :tags ["#mexican" "#coffee" "#house"], :username "rasta_toucan"}] + ["Marina Low-Carb Food Truck is a exclusive and acceptable place to have breakfast on a Tuesday afternoon." + {:small "http://cloudfront.net/b4147277-a95e-4c14-b8de-cdd330292af2/small.jpg", + :medium "http://cloudfront.net/b4147277-a95e-4c14-b8de-cdd330292af2/med.jpg", + :large "http://cloudfront.net/b4147277-a95e-4c14-b8de-cdd330292af2/large.jpg"} + {:name "Marina Low-Carb Food Truck", :categories ["Low-Carb" "Food Truck"], :phone "415-748-3513", :id "a13a5beb-19de-40ca-a334-02df3bdf5285"} + {:service "facebook", :facebook-photo-id "83ad3825-1e95-4078-9a3e-902802396114", :url "http://facebook.com/photos/83ad3825-1e95-4078-9a3e-902802396114"}] + ["Polk St. Korean Taqueria is a family-friendly and amazing place to conduct a business meeting the second Saturday of the month." + {:small "http://cloudfront.net/9eb15f11-015b-497c-86ee-ad3680ab00a8/small.jpg", + :medium "http://cloudfront.net/9eb15f11-015b-497c-86ee-ad3680ab00a8/med.jpg", + :large "http://cloudfront.net/9eb15f11-015b-497c-86ee-ad3680ab00a8/large.jpg"} + {:name "Polk St. Korean Taqueria", :categories ["Korean" "Taqueria"], :phone "415-511-5531", :id "ddb09b32-6f0b-4e54-98c7-14398f16ca4e"} + {:service "foursquare", :foursquare-photo-id "ade689fc-821b-4c22-8e66-cc48a2e4d7b9", :mayor "sameer"}] + ["Kyle's Japanese Hotel & Restaurant is a world-famous and world-famous place to have a drink Friday nights." + {:small "http://cloudfront.net/8a2fc34f-49ce-4705-85f7-94d0f5847656/small.jpg", + :medium "http://cloudfront.net/8a2fc34f-49ce-4705-85f7-94d0f5847656/med.jpg", + :large "http://cloudfront.net/8a2fc34f-49ce-4705-85f7-94d0f5847656/large.jpg"} + {:name "Kyle's Japanese Hotel & Restaurant", :categories ["Japanese" "Hotel & Restaurant"], :phone "415-337-5387", :id "eced4f41-b627-4553-a297-888871038b69"} + {:service "facebook", :facebook-photo-id "e6a59e30-0ffd-4a6f-b4bc-f45ba8c214cf", :url "http://facebook.com/photos/e6a59e30-0ffd-4a6f-b4bc-f45ba8c214cf"}] + ["Mission Soul Food Pizzeria is a swell and swell place to pitch an investor with your pet toucan." + {:small "http://cloudfront.net/108c11d1-e973-45c2-8c99-86ae460dfb1d/small.jpg", + :medium "http://cloudfront.net/108c11d1-e973-45c2-8c99-86ae460dfb1d/med.jpg", + :large "http://cloudfront.net/108c11d1-e973-45c2-8c99-86ae460dfb1d/large.jpg"} + {:name "Mission Soul Food Pizzeria", :categories ["Soul Food" "Pizzeria"], :phone "415-437-3479", :id "9905fe61-44cb-4626-843b-5d725c7949bb"} + {:service "yelp", :yelp-photo-id "0e7ee5ce-7a90-4431-959f-948658ec504c", :categories ["Soul Food" "Pizzeria"]}] + ["Alcatraz Pizza Churros is a fantastic and world-famous place to watch the Giants game in the spring." + {:small "http://cloudfront.net/c97060df-c900-4330-bc2d-8277ca9844b2/small.jpg", + :medium "http://cloudfront.net/c97060df-c900-4330-bc2d-8277ca9844b2/med.jpg", + :large "http://cloudfront.net/c97060df-c900-4330-bc2d-8277ca9844b2/large.jpg"} + {:name "Alcatraz Pizza Churros", :categories ["Pizza" "Churros"], :phone "415-754-7867", :id "df95e4f1-8719-42af-a15d-3ee00de6e04f"} + {:service "foursquare", :foursquare-photo-id "645a21e2-d948-4e57-a2c2-a29c6a6a32d8", :mayor "lucky_pigeon"}] + ["Marina No-MSG Sushi is a wonderful and family-friendly place to have a birthday party in July." + {:small "http://cloudfront.net/7ad88c11-910e-4e48-b42c-3a2050f6f52b/small.jpg", + :medium "http://cloudfront.net/7ad88c11-910e-4e48-b42c-3a2050f6f52b/med.jpg", + :large "http://cloudfront.net/7ad88c11-910e-4e48-b42c-3a2050f6f52b/large.jpg"} + {:name "Marina No-MSG Sushi", :categories ["No-MSG" "Sushi"], :phone "415-856-5937", :id "d51013a3-8547-4705-a5f0-cb11d8206481"} + {:service "twitter", :mentions ["@marina_no_msg_sushi"], :tags ["#no-msg" "#sushi"], :username "sameer"}] + ["Haight Soul Food Sushi is a groovy and historical place to have a drink weekend evenings." + {:small "http://cloudfront.net/87a2f10c-18f9-4382-ab09-c5c8b92835b9/small.jpg", + :medium "http://cloudfront.net/87a2f10c-18f9-4382-ab09-c5c8b92835b9/med.jpg", + :large "http://cloudfront.net/87a2f10c-18f9-4382-ab09-c5c8b92835b9/large.jpg"} + {:name "Haight Soul Food Sushi", :categories ["Soul Food" "Sushi"], :phone "415-371-8026", :id "b4df5eb7-d8cd-431d-9d43-381984ec81ae"} + {:service "flare", :username "tupac"}] + ["SF British Pop-Up Food Stand is a horrible and amazing place to drink a craft beer with your pet dog." + {:small "http://cloudfront.net/25cf609f-25e8-4575-9ce0-e3037368fc53/small.jpg", + :medium "http://cloudfront.net/25cf609f-25e8-4575-9ce0-e3037368fc53/med.jpg", + :large "http://cloudfront.net/25cf609f-25e8-4575-9ce0-e3037368fc53/large.jpg"} + {:name "SF British Pop-Up Food Stand", :categories ["British" "Pop-Up Food Stand"], :phone "415-441-3725", :id "19eac087-7b1c-4668-a26c-d7c02cbcd3f6"} + {:service "yelp", :yelp-photo-id "e60013b6-4660-4767-8500-d55ccf154362", :categories ["British" "Pop-Up Food Stand"]}] + ["Haight Soul Food Café is a amazing and family-friendly place to take visiting friends and relatives on Saturday night." + {:small "http://cloudfront.net/a6b38881-d547-43f5-8a6f-429b73c92321/small.jpg", + :medium "http://cloudfront.net/a6b38881-d547-43f5-8a6f-429b73c92321/med.jpg", + :large "http://cloudfront.net/a6b38881-d547-43f5-8a6f-429b73c92321/large.jpg"} + {:name "Haight Soul Food Café", :categories ["Soul Food" "Café"], :phone "415-257-1769", :id "a1796c4b-da2b-474f-9fd6-4fa96c1eac70"} + {:service "flare", :username "sameer"}] + ["Oakland European Liquor Store is a wonderful and underappreciated place to people-watch in July." + {:small "http://cloudfront.net/09b212d7-4cfe-42d7-92bd-2be6b0c4239a/small.jpg", + :medium "http://cloudfront.net/09b212d7-4cfe-42d7-92bd-2be6b0c4239a/med.jpg", + :large "http://cloudfront.net/09b212d7-4cfe-42d7-92bd-2be6b0c4239a/large.jpg"} + {:name "Oakland European Liquor Store", :categories ["European" "Liquor Store"], :phone "415-559-1516", :id "e342e7b7-e82d-475d-a822-b2df9c84850d"} + {:service "flare", :username "rasta_toucan"}] + ["Nob Hill Free-Range Ice Cream Truck is a atmospheric and world-famous place to have brunch on Saturday night." + {:small "http://cloudfront.net/fcf20e3b-3966-467a-9a91-0a1be39e0c86/small.jpg", + :medium "http://cloudfront.net/fcf20e3b-3966-467a-9a91-0a1be39e0c86/med.jpg", + :large "http://cloudfront.net/fcf20e3b-3966-467a-9a91-0a1be39e0c86/large.jpg"} + {:name "Nob Hill Free-Range Ice Cream Truck", :categories ["Free-Range" "Ice Cream Truck"], :phone "415-787-4049", :id "08d1e93c-105f-4abf-a9ec-b2e3cd30747e"} + {:service "flare", :username "jessica"}] + ["Lucky's Gluten-Free Gastro Pub is a family-friendly and family-friendly place to watch the Warriors game weekday afternoons." + {:small "http://cloudfront.net/4929ecb6-e987-4a98-be9b-aa01ace293b1/small.jpg", + :medium "http://cloudfront.net/4929ecb6-e987-4a98-be9b-aa01ace293b1/med.jpg", + :large "http://cloudfront.net/4929ecb6-e987-4a98-be9b-aa01ace293b1/large.jpg"} + {:name "Lucky's Gluten-Free Gastro Pub", :categories ["Gluten-Free" "Gastro Pub"], :phone "415-391-6443", :id "7ccf8bbb-74b4-48f7-aa7c-43872f63cb1b"} + {:service "facebook", :facebook-photo-id "1be63daf-2a0c-4514-a7a3-d20264346b9d", :url "http://facebook.com/photos/1be63daf-2a0c-4514-a7a3-d20264346b9d"}] + ["Sunset American Churros is a well-decorated and horrible place to watch the Giants game on public holidays." + {:small "http://cloudfront.net/98e6d78e-7926-44d8-b94d-12fa9903485c/small.jpg", + :medium "http://cloudfront.net/98e6d78e-7926-44d8-b94d-12fa9903485c/med.jpg", + :large "http://cloudfront.net/98e6d78e-7926-44d8-b94d-12fa9903485c/large.jpg"} + {:name "Sunset American Churros", :categories ["American" "Churros"], :phone "415-191-5018", :id "2e88c921-29fb-489b-a956-d3ba1182da73"} + {:service "yelp", :yelp-photo-id "2852b37d-d922-4259-be12-744fa4bfee02", :categories ["American" "Churros"]}] + ["Kyle's European Churros is a family-friendly and fantastic place to have brunch in July." + {:small "http://cloudfront.net/33370b35-d26b-48be-a6f0-135d09d82dd6/small.jpg", + :medium "http://cloudfront.net/33370b35-d26b-48be-a6f0-135d09d82dd6/med.jpg", + :large "http://cloudfront.net/33370b35-d26b-48be-a6f0-135d09d82dd6/large.jpg"} + {:name "Kyle's European Churros", :categories ["European" "Churros"], :phone "415-233-8392", :id "5270240c-6e6e-4512-9344-3dc497d6ea49"} + {:service "yelp", :yelp-photo-id "c151203a-b7d1-45c7-89c5-f874054cb524", :categories ["European" "Churros"]}] + ["Tenderloin Cage-Free Sushi is a world-famous and underground place to drink a craft beer on a Tuesday afternoon." + {:small "http://cloudfront.net/6ec6cac2-7492-4432-af7f-291301d03031/small.jpg", + :medium "http://cloudfront.net/6ec6cac2-7492-4432-af7f-291301d03031/med.jpg", + :large "http://cloudfront.net/6ec6cac2-7492-4432-af7f-291301d03031/large.jpg"} + {:name "Tenderloin Cage-Free Sushi", :categories ["Cage-Free" "Sushi"], :phone "415-348-0644", :id "0b6c036f-82b0-4008-bdfe-5360dd93fb75"} + {:service "twitter", :mentions ["@tenderloin_cage_free_sushi"], :tags ["#cage-free" "#sushi"], :username "sameer"}] + ["Kyle's Japanese Hotel & Restaurant is a wonderful and decent place to catch a bite to eat with friends." + {:small "http://cloudfront.net/d4806e59-38d0-4dc2-87b9-49c4ef406fd1/small.jpg", + :medium "http://cloudfront.net/d4806e59-38d0-4dc2-87b9-49c4ef406fd1/med.jpg", + :large "http://cloudfront.net/d4806e59-38d0-4dc2-87b9-49c4ef406fd1/large.jpg"} + {:name "Kyle's Japanese Hotel & Restaurant", :categories ["Japanese" "Hotel & Restaurant"], :phone "415-337-5387", :id "eced4f41-b627-4553-a297-888871038b69"} + {:service "foursquare", :foursquare-photo-id "7efa0b90-41f8-42b4-b148-b981eb3d2c0f", :mayor "cam_saul"}] + ["Haight Soul Food Pop-Up Food Stand is a decent and atmospheric place to nurse a hangover weekend mornings." + {:small "http://cloudfront.net/73c98d12-5bf9-40a1-b119-708f84d0fc74/small.jpg", + :medium "http://cloudfront.net/73c98d12-5bf9-40a1-b119-708f84d0fc74/med.jpg", + :large "http://cloudfront.net/73c98d12-5bf9-40a1-b119-708f84d0fc74/large.jpg"} + {:name "Haight Soul Food Pop-Up Food Stand", :categories ["Soul Food" "Pop-Up Food Stand"], :phone "415-741-8726", :id "9735184b-1299-410f-a98e-10d9c548af42"} + {:service "foursquare", :foursquare-photo-id "87cbe196-2aa6-45d1-87b8-bf9027c655c1", :mayor "cam_saul"}] + ["Tenderloin Gluten-Free Bar & Grill is a popular and wonderful place to sip Champagne on Saturday night." + {:small "http://cloudfront.net/03a096b6-2326-4e11-9326-29793d4a03d5/small.jpg", + :medium "http://cloudfront.net/03a096b6-2326-4e11-9326-29793d4a03d5/med.jpg", + :large "http://cloudfront.net/03a096b6-2326-4e11-9326-29793d4a03d5/large.jpg"} + {:name "Tenderloin Gluten-Free Bar & Grill", :categories ["Gluten-Free" "Bar & Grill"], :phone "415-904-0956", :id "0d7e235a-eea8-45b3-aaa7-23b4ea2b50f2"} + {:service "facebook", :facebook-photo-id "07b3bf42-0187-4f24-9d2a-b6cb5419fe6b", :url "http://facebook.com/photos/07b3bf42-0187-4f24-9d2a-b6cb5419fe6b"}] + ["Lower Pac Heights Deep-Dish Ice Cream Truck is a amazing and family-friendly place to have a birthday party Friday nights." + {:small "http://cloudfront.net/a93dc0cc-7974-4a90-a899-fdcd468c5f8d/small.jpg", + :medium "http://cloudfront.net/a93dc0cc-7974-4a90-a899-fdcd468c5f8d/med.jpg", + :large "http://cloudfront.net/a93dc0cc-7974-4a90-a899-fdcd468c5f8d/large.jpg"} + {:name "Lower Pac Heights Deep-Dish Ice Cream Truck", :categories ["Deep-Dish" "Ice Cream Truck"], :phone "415-495-1414", :id "d5efa2f2-496d-41b2-8b85-3d002a22a2bc"} + {:service "yelp", :yelp-photo-id "d8cfa65d-018c-4a07-baa5-ef38e019b53e", :categories ["Deep-Dish" "Ice Cream Truck"]}] + ["Sunset American Churros is a historical and swell place to catch a bite to eat on a Tuesday afternoon." + {:small "http://cloudfront.net/f7e89230-3e54-487f-aa62-c941fa30e833/small.jpg", + :medium "http://cloudfront.net/f7e89230-3e54-487f-aa62-c941fa30e833/med.jpg", + :large "http://cloudfront.net/f7e89230-3e54-487f-aa62-c941fa30e833/large.jpg"} + {:name "Sunset American Churros", :categories ["American" "Churros"], :phone "415-191-5018", :id "2e88c921-29fb-489b-a956-d3ba1182da73"} + {:service "yelp", :yelp-photo-id "e73b3ca1-da16-42f7-99ed-11e978abae41", :categories ["American" "Churros"]}] + ["Kyle's Low-Carb Grill is a decent and great place to watch the Warriors game in the fall." + {:small "http://cloudfront.net/b24e4e79-6771-4b12-b614-eb702200e544/small.jpg", + :medium "http://cloudfront.net/b24e4e79-6771-4b12-b614-eb702200e544/med.jpg", + :large "http://cloudfront.net/b24e4e79-6771-4b12-b614-eb702200e544/large.jpg"} + {:name "Kyle's Low-Carb Grill", :categories ["Low-Carb" "Grill"], :phone "415-992-8278", :id "b27f50c6-55eb-48b0-9fee-17a6ef5243bd"} + {:service "foursquare", :foursquare-photo-id "abf79d51-a26b-48f8-8267-28af692327b7", :mayor "lucky_pigeon"}] + ["Pacific Heights No-MSG Sushi is a world-famous and groovy place to have breakfast during summer." + {:small "http://cloudfront.net/3ee22abf-acd2-433f-9d18-00747bb907b3/small.jpg", + :medium "http://cloudfront.net/3ee22abf-acd2-433f-9d18-00747bb907b3/med.jpg", + :large "http://cloudfront.net/3ee22abf-acd2-433f-9d18-00747bb907b3/large.jpg"} + {:name "Pacific Heights No-MSG Sushi", :categories ["No-MSG" "Sushi"], :phone "415-354-9547", :id "b15b66a8-f3da-4b65-8156-80f5518fdd5a"} + {:service "facebook", :facebook-photo-id "24bc49af-3dd9-4d57-bd9c-204aacd68eea", :url "http://facebook.com/photos/24bc49af-3dd9-4d57-bd9c-204aacd68eea"}] + ["Lower Pac Heights Deep-Dish Liquor Store is a delicious and underappreciated place to have a after-work cocktail in the fall." + {:small "http://cloudfront.net/585e25e5-5ae1-403b-acdf-a5069173d053/small.jpg", + :medium "http://cloudfront.net/585e25e5-5ae1-403b-acdf-a5069173d053/med.jpg", + :large "http://cloudfront.net/585e25e5-5ae1-403b-acdf-a5069173d053/large.jpg"} + {:name "Lower Pac Heights Deep-Dish Liquor Store", :categories ["Deep-Dish" "Liquor Store"], :phone "415-497-3039", :id "4d4eabfc-ff1f-4bc6-88b0-2f55489ff666"} + {:service "foursquare", :foursquare-photo-id "c57cddd8-4867-43cf-8f71-892e87788b73", :mayor "jessica"}] + ["Pacific Heights Free-Range Eatery is a decent and acceptable place to take visiting friends and relatives Friday nights." + {:small "http://cloudfront.net/d0f3968e-c660-45eb-9d17-bcd3b9229956/small.jpg", + :medium "http://cloudfront.net/d0f3968e-c660-45eb-9d17-bcd3b9229956/med.jpg", + :large "http://cloudfront.net/d0f3968e-c660-45eb-9d17-bcd3b9229956/large.jpg"} + {:name "Pacific Heights Free-Range Eatery", :categories ["Free-Range" "Eatery"], :phone "415-901-6541", :id "88b361c8-ce69-4b2e-b0f2-9deedd574af6"} + {:service "twitter", :mentions ["@pacific_heights_free_range_eatery"], :tags ["#free-range" "#eatery"], :username "mandy"}] + ["Marina Cage-Free Liquor Store is a popular and wonderful place to take visiting friends and relatives weekend mornings." + {:small "http://cloudfront.net/88852ac8-d7e1-4cba-b4e3-ec810088b8a9/small.jpg", + :medium "http://cloudfront.net/88852ac8-d7e1-4cba-b4e3-ec810088b8a9/med.jpg", + :large "http://cloudfront.net/88852ac8-d7e1-4cba-b4e3-ec810088b8a9/large.jpg"} + {:name "Marina Cage-Free Liquor Store", :categories ["Cage-Free" "Liquor Store"], :phone "415-571-0783", :id "ad68f549-3000-407e-ab98-7f314cfa4653"} + {:service "yelp", :yelp-photo-id "6301ba32-41f7-44ff-872e-3c6f96e34ebb", :categories ["Cage-Free" "Liquor Store"]}] + ["Mission Japanese Coffee House is a popular and swell place to have a drink on public holidays." + {:small "http://cloudfront.net/5b8270f9-29aa-4d47-8a64-23b26dc7e688/small.jpg", + :medium "http://cloudfront.net/5b8270f9-29aa-4d47-8a64-23b26dc7e688/med.jpg", + :large "http://cloudfront.net/5b8270f9-29aa-4d47-8a64-23b26dc7e688/large.jpg"} + {:name "Mission Japanese Coffee House", :categories ["Japanese" "Coffee House"], :phone "415-561-0506", :id "60dd274e-0cbf-4521-946d-8a4e0f151150"} + {:service "yelp", :yelp-photo-id "7c9bc27a-7478-4bec-b6e1-99657346b360", :categories ["Japanese" "Coffee House"]}] + ["Joe's Homestyle Eatery is a swell and groovy place to take a date in June." + {:small "http://cloudfront.net/6b7c484a-4839-4f61-b32d-690fbffe08d8/small.jpg", + :medium "http://cloudfront.net/6b7c484a-4839-4f61-b32d-690fbffe08d8/med.jpg", + :large "http://cloudfront.net/6b7c484a-4839-4f61-b32d-690fbffe08d8/large.jpg"} + {:name "Joe's Homestyle Eatery", :categories ["Homestyle" "Eatery"], :phone "415-950-1337", :id "5cc18489-dfaf-417b-900f-5d1d61b961e8"} + {:service "twitter", :mentions ["@joes_homestyle_eatery"], :tags ["#homestyle" "#eatery"], :username "sameer"}] + ["Nob Hill Free-Range Ice Cream Truck is a well-decorated and modern place to watch the Giants game after baseball games." + {:small "http://cloudfront.net/f2c105a6-db11-44c4-a0a7-5adac712a5f1/small.jpg", + :medium "http://cloudfront.net/f2c105a6-db11-44c4-a0a7-5adac712a5f1/med.jpg", + :large "http://cloudfront.net/f2c105a6-db11-44c4-a0a7-5adac712a5f1/large.jpg"} + {:name "Nob Hill Free-Range Ice Cream Truck", :categories ["Free-Range" "Ice Cream Truck"], :phone "415-787-4049", :id "08d1e93c-105f-4abf-a9ec-b2e3cd30747e"} + {:service "twitter", :mentions ["@nob_hill_free_range_ice_cream_truck"], :tags ["#free-range" "#ice" "#cream" "#truck"], :username "jane"}] + ["Lucky's Gluten-Free Café is a horrible and historical place to conduct a business meeting on Saturday night." + {:small "http://cloudfront.net/e1af0686-6fdb-415a-baa1-2e076dd1af2a/small.jpg", + :medium "http://cloudfront.net/e1af0686-6fdb-415a-baa1-2e076dd1af2a/med.jpg", + :large "http://cloudfront.net/e1af0686-6fdb-415a-baa1-2e076dd1af2a/large.jpg"} + {:name "Lucky's Gluten-Free Café", :categories ["Gluten-Free" "Café"], :phone "415-740-2328", :id "379af987-ad40-4a93-88a6-0233e1c14649"} + {:service "facebook", :facebook-photo-id "f37c703b-0e19-4b91-9fc0-3945524300ed", :url "http://facebook.com/photos/f37c703b-0e19-4b91-9fc0-3945524300ed"}] + ["Kyle's Low-Carb Grill is a decent and underground place to have brunch on Taco Tuesday." + {:small "http://cloudfront.net/b29b4f66-ef21-47e1-be7e-a3cd319c2110/small.jpg", + :medium "http://cloudfront.net/b29b4f66-ef21-47e1-be7e-a3cd319c2110/med.jpg", + :large "http://cloudfront.net/b29b4f66-ef21-47e1-be7e-a3cd319c2110/large.jpg"} + {:name "Kyle's Low-Carb Grill", :categories ["Low-Carb" "Grill"], :phone "415-992-8278", :id "b27f50c6-55eb-48b0-9fee-17a6ef5243bd"} + {:service "flare", :username "kyle"}] + ["Rasta's Paleo Churros is a popular and family-friendly place to conduct a business meeting on a Tuesday afternoon." + {:small "http://cloudfront.net/ab6f99aa-a1b9-4f3b-904a-fdc16c363827/small.jpg", + :medium "http://cloudfront.net/ab6f99aa-a1b9-4f3b-904a-fdc16c363827/med.jpg", + :large "http://cloudfront.net/ab6f99aa-a1b9-4f3b-904a-fdc16c363827/large.jpg"} + {:name "Rasta's Paleo Churros", :categories ["Paleo" "Churros"], :phone "415-915-0309", :id "3bf48ec6-434b-43b1-be28-7644975ecaf9"} + {:service "flare", :username "amy"}] + ["Kyle's Old-Fashioned Pop-Up Food Stand is a world-famous and family-friendly place to have a drink with your pet dog." + {:small "http://cloudfront.net/7333d3c2-37c6-4a9d-96ab-019ac1174b91/small.jpg", + :medium "http://cloudfront.net/7333d3c2-37c6-4a9d-96ab-019ac1174b91/med.jpg", + :large "http://cloudfront.net/7333d3c2-37c6-4a9d-96ab-019ac1174b91/large.jpg"} + {:name "Kyle's Old-Fashioned Pop-Up Food Stand", :categories ["Old-Fashioned" "Pop-Up Food Stand"], :phone "415-638-8972", :id "7da187e8-bd01-48ca-ad93-7a02a442d9eb"} + {:service "foursquare", :foursquare-photo-id "e99162db-5fc6-4164-9537-a35aee30f51c", :mayor "kyle"}] + ["Joe's Homestyle Eatery is a horrible and exclusive place to watch the Giants game the first Sunday of the month." + {:small "http://cloudfront.net/bd701a19-53c9-4cc2-aecd-f1e3c499493e/small.jpg", + :medium "http://cloudfront.net/bd701a19-53c9-4cc2-aecd-f1e3c499493e/med.jpg", + :large "http://cloudfront.net/bd701a19-53c9-4cc2-aecd-f1e3c499493e/large.jpg"} + {:name "Joe's Homestyle Eatery", :categories ["Homestyle" "Eatery"], :phone "415-950-1337", :id "5cc18489-dfaf-417b-900f-5d1d61b961e8"} + {:service "facebook", :facebook-photo-id "87163e8d-0bcf-4d20-b3a8-ab895ddfef15", :url "http://facebook.com/photos/87163e8d-0bcf-4d20-b3a8-ab895ddfef15"}] + ["Lucky's Deep-Dish Gastro Pub is a horrible and underground place to have a birthday party with your pet dog." + {:small "http://cloudfront.net/30ca52de-38cc-4194-9afb-f8879e55cbf5/small.jpg", + :medium "http://cloudfront.net/30ca52de-38cc-4194-9afb-f8879e55cbf5/med.jpg", + :large "http://cloudfront.net/30ca52de-38cc-4194-9afb-f8879e55cbf5/large.jpg"} + {:name "Lucky's Deep-Dish Gastro Pub", :categories ["Deep-Dish" "Gastro Pub"], :phone "415-487-4085", :id "0136c454-0968-41cd-a237-ceec5724cab8"} + {:service "foursquare", :foursquare-photo-id "08c45fbc-d085-4e2d-8b69-3e2d89f457a9", :mayor "amy"}] + ["Sameer's GMO-Free Pop-Up Food Stand is a swell and world-famous place to take visiting friends and relatives during summer." + {:small "http://cloudfront.net/271d9185-e724-4afd-b3a2-b8d9eee096eb/small.jpg", + :medium "http://cloudfront.net/271d9185-e724-4afd-b3a2-b8d9eee096eb/med.jpg", + :large "http://cloudfront.net/271d9185-e724-4afd-b3a2-b8d9eee096eb/large.jpg"} + {:name "Sameer's GMO-Free Pop-Up Food Stand", :categories ["GMO-Free" "Pop-Up Food Stand"], :phone "415-217-7891", :id "a829efc7-7e03-4e73-b072-83d10d1e3953"} + {:service "twitter", :mentions ["@sameers_gmo_free_pop_up_food_stand"], :tags ["#gmo-free" "#pop-up" "#food" "#stand"], :username "joe"}] + ["Nob Hill Korean Taqueria is a classic and classic place to take a date after baseball games." + {:small "http://cloudfront.net/8f8cfa52-8973-4af0-9226-1df9eda589d9/small.jpg", + :medium "http://cloudfront.net/8f8cfa52-8973-4af0-9226-1df9eda589d9/med.jpg", + :large "http://cloudfront.net/8f8cfa52-8973-4af0-9226-1df9eda589d9/large.jpg"} + {:name "Nob Hill Korean Taqueria", :categories ["Korean" "Taqueria"], :phone "415-107-7332", :id "a43c184c-90f5-488c-bb3b-00ea2666d90e"} + {:service "twitter", :mentions ["@nob_hill_korean_taqueria"], :tags ["#korean" "#taqueria"], :username "amy"}] + ["Lucky's Afgan Sushi is a swell and amazing place to drink a craft beer the second Saturday of the month." + {:small "http://cloudfront.net/77cdaadd-b53a-48f5-b434-8b9b2262e5cf/small.jpg", + :medium "http://cloudfront.net/77cdaadd-b53a-48f5-b434-8b9b2262e5cf/med.jpg", + :large "http://cloudfront.net/77cdaadd-b53a-48f5-b434-8b9b2262e5cf/large.jpg"} + {:name "Lucky's Afgan Sushi", :categories ["Afgan" "Sushi"], :phone "415-188-3506", :id "4a47d0d2-0123-4bb9-b941-38702f0697e9"} + {:service "facebook", :facebook-photo-id "ba479234-45d6-4e25-aba9-5f820f0ba318", :url "http://facebook.com/photos/ba479234-45d6-4e25-aba9-5f820f0ba318"}] + ["Lucky's Deep-Dish Gastro Pub is a delicious and amazing place to sip a glass of expensive wine in June." + {:small "http://cloudfront.net/9c59add8-379d-4f9a-81a8-7346e88b25d5/small.jpg", + :medium "http://cloudfront.net/9c59add8-379d-4f9a-81a8-7346e88b25d5/med.jpg", + :large "http://cloudfront.net/9c59add8-379d-4f9a-81a8-7346e88b25d5/large.jpg"} + {:name "Lucky's Deep-Dish Gastro Pub", :categories ["Deep-Dish" "Gastro Pub"], :phone "415-487-4085", :id "0136c454-0968-41cd-a237-ceec5724cab8"} + {:service "foursquare", :foursquare-photo-id "8c374317-71fe-4112-ab02-53450a771d48", :mayor "joe"}] + ["Pacific Heights Free-Range Eatery is a exclusive and wonderful place to pitch an investor in July." + {:small "http://cloudfront.net/fe3f8af1-a494-48af-b6c2-a4d16f99c506/small.jpg", + :medium "http://cloudfront.net/fe3f8af1-a494-48af-b6c2-a4d16f99c506/med.jpg", + :large "http://cloudfront.net/fe3f8af1-a494-48af-b6c2-a4d16f99c506/large.jpg"} + {:name "Pacific Heights Free-Range Eatery", :categories ["Free-Range" "Eatery"], :phone "415-901-6541", :id "88b361c8-ce69-4b2e-b0f2-9deedd574af6"} + {:service "flare", :username "cam_saul"}] + ["Tenderloin Cage-Free Sushi is a fantastic and modern place to take a date during summer." + {:small "http://cloudfront.net/546084bf-adf8-4f19-9a77-09b06e67c07d/small.jpg", + :medium "http://cloudfront.net/546084bf-adf8-4f19-9a77-09b06e67c07d/med.jpg", + :large "http://cloudfront.net/546084bf-adf8-4f19-9a77-09b06e67c07d/large.jpg"} + {:name "Tenderloin Cage-Free Sushi", :categories ["Cage-Free" "Sushi"], :phone "415-348-0644", :id "0b6c036f-82b0-4008-bdfe-5360dd93fb75"} + {:service "twitter", :mentions ["@tenderloin_cage_free_sushi"], :tags ["#cage-free" "#sushi"], :username "sameer"}] + ["Polk St. Japanese Liquor Store is a delicious and decent place to drink a craft beer the first Sunday of the month." + {:small "http://cloudfront.net/3f9ee642-6113-4d6c-ba97-2e51be75474f/small.jpg", + :medium "http://cloudfront.net/3f9ee642-6113-4d6c-ba97-2e51be75474f/med.jpg", + :large "http://cloudfront.net/3f9ee642-6113-4d6c-ba97-2e51be75474f/large.jpg"} + {:name "Polk St. Japanese Liquor Store", :categories ["Japanese" "Liquor Store"], :phone "415-726-7986", :id "b57ceac5-328d-4b65-9909-a1f9abc93015"} + {:service "facebook", :facebook-photo-id "4e3e3372-d489-4ec6-9b80-c3615d7bb110", :url "http://facebook.com/photos/4e3e3372-d489-4ec6-9b80-c3615d7bb110"}] + ["Sunset Homestyle Grill is a overrated and popular place to have a after-work cocktail weekday afternoons." + {:small "http://cloudfront.net/27bffcc4-0017-4759-aeb9-5b78d1bf7ec3/small.jpg", + :medium "http://cloudfront.net/27bffcc4-0017-4759-aeb9-5b78d1bf7ec3/med.jpg", + :large "http://cloudfront.net/27bffcc4-0017-4759-aeb9-5b78d1bf7ec3/large.jpg"} + {:name "Sunset Homestyle Grill", :categories ["Homestyle" "Grill"], :phone "415-356-7052", :id "c57673cd-f2d0-4bbc-aed0-6c166d7cf2c3"} + {:service "facebook", :facebook-photo-id "e40f9858-278e-4b0f-aa49-57295cdad381", :url "http://facebook.com/photos/e40f9858-278e-4b0f-aa49-57295cdad381"}] + ["Cam's Old-Fashioned Coffee House is a groovy and popular place to conduct a business meeting weekend evenings." + {:small "http://cloudfront.net/ef480d7a-3256-450b-9631-b38e9848a3f3/small.jpg", + :medium "http://cloudfront.net/ef480d7a-3256-450b-9631-b38e9848a3f3/med.jpg", + :large "http://cloudfront.net/ef480d7a-3256-450b-9631-b38e9848a3f3/large.jpg"} + {:name "Cam's Old-Fashioned Coffee House", :categories ["Old-Fashioned" "Coffee House"], :phone "415-868-2973", :id "27592c2b-e682-44bb-be28-8e9a622becca"} + {:service "flare", :username "jane"}] + ["Joe's Modern Coffee House is a swell and swell place to catch a bite to eat in the spring." + {:small "http://cloudfront.net/95a7cf90-5437-4f75-9bee-e61c08daca75/small.jpg", + :medium "http://cloudfront.net/95a7cf90-5437-4f75-9bee-e61c08daca75/med.jpg", + :large "http://cloudfront.net/95a7cf90-5437-4f75-9bee-e61c08daca75/large.jpg"} + {:name "Joe's Modern Coffee House", :categories ["Modern" "Coffee House"], :phone "415-331-5269", :id "cd9e3610-9ead-4f0b-ad93-cd53611d49fe"} + {:service "foursquare", :foursquare-photo-id "a81b8c97-cde1-40d8-8a54-05fc2c6074b4", :mayor "rasta_toucan"}] + ["Marina Homestyle Pop-Up Food Stand is a exclusive and modern place to have brunch the second Saturday of the month." + {:small "http://cloudfront.net/a9541f11-91c0-4a36-bb2f-5b41fcd7d6f9/small.jpg", + :medium "http://cloudfront.net/a9541f11-91c0-4a36-bb2f-5b41fcd7d6f9/med.jpg", + :large "http://cloudfront.net/a9541f11-91c0-4a36-bb2f-5b41fcd7d6f9/large.jpg"} + {:name "Marina Homestyle Pop-Up Food Stand", :categories ["Homestyle" "Pop-Up Food Stand"], :phone "415-094-4567", :id "88a7ae3c-8b36-4901-a0c5-b82342cba6cd"} + {:service "twitter", :mentions ["@marina_homestyle_pop_up_food_stand"], :tags ["#homestyle" "#pop-up" "#food" "#stand"], :username "biggie"}] + ["Mission Japanese Coffee House is a amazing and decent place to catch a bite to eat on public holidays." + {:small "http://cloudfront.net/4af17c4d-013b-47b6-83af-46fce1f5ceba/small.jpg", + :medium "http://cloudfront.net/4af17c4d-013b-47b6-83af-46fce1f5ceba/med.jpg", + :large "http://cloudfront.net/4af17c4d-013b-47b6-83af-46fce1f5ceba/large.jpg"} + {:name "Mission Japanese Coffee House", :categories ["Japanese" "Coffee House"], :phone "415-561-0506", :id "60dd274e-0cbf-4521-946d-8a4e0f151150"} + {:service "foursquare", :foursquare-photo-id "c628a149-96fc-4ece-a89c-d07f0d57bc83", :mayor "biggie"}] + ["Kyle's Free-Range Taqueria is a underground and groovy place to have a drink during summer." + {:small "http://cloudfront.net/c7533c61-98b3-4592-b02c-15d34d4b2da6/small.jpg", + :medium "http://cloudfront.net/c7533c61-98b3-4592-b02c-15d34d4b2da6/med.jpg", + :large "http://cloudfront.net/c7533c61-98b3-4592-b02c-15d34d4b2da6/large.jpg"} + {:name "Kyle's Free-Range Taqueria", :categories ["Free-Range" "Taqueria"], :phone "415-201-7832", :id "7aeb9416-4fe6-45f9-8849-6b8ba6d3f3b9"} + {:service "foursquare", :foursquare-photo-id "29bdfc7c-9766-463f-a8c0-4568fa583604", :mayor "bob"}] + ["Sameer's GMO-Free Pop-Up Food Stand is a delicious and family-friendly place to people-watch weekday afternoons." + {:small "http://cloudfront.net/dffdba53-41d7-4b09-bacd-9dacd98f576d/small.jpg", + :medium "http://cloudfront.net/dffdba53-41d7-4b09-bacd-9dacd98f576d/med.jpg", + :large "http://cloudfront.net/dffdba53-41d7-4b09-bacd-9dacd98f576d/large.jpg"} + {:name "Sameer's GMO-Free Pop-Up Food Stand", :categories ["GMO-Free" "Pop-Up Food Stand"], :phone "415-217-7891", :id "a829efc7-7e03-4e73-b072-83d10d1e3953"} + {:service "facebook", :facebook-photo-id "8a7c76a5-9099-4e2e-b6d8-8eebe10acf72", :url "http://facebook.com/photos/8a7c76a5-9099-4e2e-b6d8-8eebe10acf72"}] + ["Mission Japanese Coffee House is a underappreciated and delicious place to drink a craft beer on public holidays." + {:small "http://cloudfront.net/6326e639-414c-4a23-8997-d6165cf61ea5/small.jpg", + :medium "http://cloudfront.net/6326e639-414c-4a23-8997-d6165cf61ea5/med.jpg", + :large "http://cloudfront.net/6326e639-414c-4a23-8997-d6165cf61ea5/large.jpg"} + {:name "Mission Japanese Coffee House", :categories ["Japanese" "Coffee House"], :phone "415-561-0506", :id "60dd274e-0cbf-4521-946d-8a4e0f151150"} + {:service "yelp", :yelp-photo-id "ec651838-e2d7-45c1-8b8a-70f80f87fae5", :categories ["Japanese" "Coffee House"]}] + ["Cam's Mexican Gastro Pub is a historical and underappreciated place to conduct a business meeting with friends." + {:small "http://cloudfront.net/6e3a5256-275f-4056-b61a-25990b4bb484/small.jpg", + :medium "http://cloudfront.net/6e3a5256-275f-4056-b61a-25990b4bb484/med.jpg", + :large "http://cloudfront.net/6e3a5256-275f-4056-b61a-25990b4bb484/large.jpg"} + {:name "Cam's Mexican Gastro Pub", :categories ["Mexican" "Gastro Pub"], :phone "415-320-9123", :id "bb958ac5-758e-4f42-b984-6b0e13f25194"} + {:service "twitter", :mentions ["@cams_mexican_gastro_pub"], :tags ["#mexican" "#gastro" "#pub"], :username "kyle"}] + ["Haight Mexican Restaurant is a well-decorated and popular place to have breakfast Friday nights." + {:small "http://cloudfront.net/6c89c785-1e69-449f-9eb8-81f5adaa40a3/small.jpg", + :medium "http://cloudfront.net/6c89c785-1e69-449f-9eb8-81f5adaa40a3/med.jpg", + :large "http://cloudfront.net/6c89c785-1e69-449f-9eb8-81f5adaa40a3/large.jpg"} + {:name "Haight Mexican Restaurant", :categories ["Mexican" "Restaurant"], :phone "415-758-8690", :id "a5e3f0ac-f6e8-4e71-a0a2-3f10f48b4ab1"} + {:service "twitter", :mentions ["@haight_mexican_restaurant"], :tags ["#mexican" "#restaurant"], :username "bob"}] + ["Chinatown Paleo Food Truck is a great and underappreciated place to take a date in the spring." + {:small "http://cloudfront.net/57814010-b3cc-424f-ba74-4e2cc39e838d/small.jpg", + :medium "http://cloudfront.net/57814010-b3cc-424f-ba74-4e2cc39e838d/med.jpg", + :large "http://cloudfront.net/57814010-b3cc-424f-ba74-4e2cc39e838d/large.jpg"} + {:name "Chinatown Paleo Food Truck", :categories ["Paleo" "Food Truck"], :phone "415-583-4380", :id "aa9b5ce9-db74-470e-8573-f2faca24d546"} + {:service "twitter", :mentions ["@chinatown_paleo_food_truck"], :tags ["#paleo" "#food" "#truck"], :username "bob"}] + ["Lucky's Cage-Free Liquor Store is a overrated and fantastic place to have a birthday party in the fall." + {:small "http://cloudfront.net/b1686642-2751-4c46-9bbf-cf37fdb35fd2/small.jpg", + :medium "http://cloudfront.net/b1686642-2751-4c46-9bbf-cf37fdb35fd2/med.jpg", + :large "http://cloudfront.net/b1686642-2751-4c46-9bbf-cf37fdb35fd2/large.jpg"} + {:name "Lucky's Cage-Free Liquor Store", :categories ["Cage-Free" "Liquor Store"], :phone "415-341-3219", :id "968eac8d-1fd0-443d-a7ff-b48810ab0f69"} + {:service "flare", :username "sameer"}] + ["Pacific Heights No-MSG Sushi is a horrible and horrible place to people-watch the second Saturday of the month." + {:small "http://cloudfront.net/dd186a13-fd25-4114-9f7e-ea2e045638b8/small.jpg", + :medium "http://cloudfront.net/dd186a13-fd25-4114-9f7e-ea2e045638b8/med.jpg", + :large "http://cloudfront.net/dd186a13-fd25-4114-9f7e-ea2e045638b8/large.jpg"} + {:name "Pacific Heights No-MSG Sushi", :categories ["No-MSG" "Sushi"], :phone "415-354-9547", :id "b15b66a8-f3da-4b65-8156-80f5518fdd5a"} + {:service "facebook", :facebook-photo-id "362ae2f8-3bca-4a80-8b34-708aaac74139", :url "http://facebook.com/photos/362ae2f8-3bca-4a80-8b34-708aaac74139"}] + ["Alcatraz Pizza Churros is a swell and acceptable place to catch a bite to eat Friday nights." + {:small "http://cloudfront.net/59cd5ba5-a68d-4d65-b104-9c0b7cc090f3/small.jpg", + :medium "http://cloudfront.net/59cd5ba5-a68d-4d65-b104-9c0b7cc090f3/med.jpg", + :large "http://cloudfront.net/59cd5ba5-a68d-4d65-b104-9c0b7cc090f3/large.jpg"} + {:name "Alcatraz Pizza Churros", :categories ["Pizza" "Churros"], :phone "415-754-7867", :id "df95e4f1-8719-42af-a15d-3ee00de6e04f"} + {:service "facebook", :facebook-photo-id "8ab454e9-36ae-4824-a60b-bcacaaa56a76", :url "http://facebook.com/photos/8ab454e9-36ae-4824-a60b-bcacaaa56a76"}] + ["Marina Modern Bar & Grill is a exclusive and world-famous place to catch a bite to eat weekend evenings." + {:small "http://cloudfront.net/98d2956f-a799-49c8-9e39-cefd74ae6280/small.jpg", + :medium "http://cloudfront.net/98d2956f-a799-49c8-9e39-cefd74ae6280/med.jpg", + :large "http://cloudfront.net/98d2956f-a799-49c8-9e39-cefd74ae6280/large.jpg"} + {:name "Marina Modern Bar & Grill", :categories ["Modern" "Bar & Grill"], :phone "415-203-8530", :id "806144f1-bb7a-4271-8fcb-fc6550f51676"} + {:service "flare", :username "amy"}] + ["Rasta's British Food Truck is a amazing and great place to sip Champagne on a Tuesday afternoon." + {:small "http://cloudfront.net/d0061644-021c-4ee1-a2e2-1c0da0c0348a/small.jpg", + :medium "http://cloudfront.net/d0061644-021c-4ee1-a2e2-1c0da0c0348a/med.jpg", + :large "http://cloudfront.net/d0061644-021c-4ee1-a2e2-1c0da0c0348a/large.jpg"} + {:name "Rasta's British Food Truck", :categories ["British" "Food Truck"], :phone "415-958-9031", :id "b6616c97-01d0-488f-a855-bcd6efe2b899"} + {:service "foursquare", :foursquare-photo-id "bf141876-b610-4fcb-a1c2-d292d8989929", :mayor "amy"}] + ["Lucky's Deep-Dish Gastro Pub is a classic and well-decorated place to have a birthday party in the spring." + {:small "http://cloudfront.net/eb4cef56-fac6-4407-b952-ed5da677a144/small.jpg", + :medium "http://cloudfront.net/eb4cef56-fac6-4407-b952-ed5da677a144/med.jpg", + :large "http://cloudfront.net/eb4cef56-fac6-4407-b952-ed5da677a144/large.jpg"} + {:name "Lucky's Deep-Dish Gastro Pub", :categories ["Deep-Dish" "Gastro Pub"], :phone "415-487-4085", :id "0136c454-0968-41cd-a237-ceec5724cab8"} + {:service "twitter", :mentions ["@luckys_deep_dish_gastro_pub"], :tags ["#deep-dish" "#gastro" "#pub"], :username "cam_saul"}] + ["Sameer's Chinese Restaurant is a underappreciated and popular place to watch the Warriors game Friday nights." + {:small "http://cloudfront.net/d201302e-4e74-4db8-84f2-bd3dec57c1d8/small.jpg", + :medium "http://cloudfront.net/d201302e-4e74-4db8-84f2-bd3dec57c1d8/med.jpg", + :large "http://cloudfront.net/d201302e-4e74-4db8-84f2-bd3dec57c1d8/large.jpg"} + {:name "Sameer's Chinese Restaurant", :categories ["Chinese" "Restaurant"], :phone "415-707-3659", :id "51a9545e-7e1e-40f1-b550-09067b648f20"} + {:service "foursquare", :foursquare-photo-id "f7875634-d317-4cb5-a04a-cf6babd14144", :mayor "rasta_toucan"}] + ["Nob Hill Korean Taqueria is a well-decorated and underappreciated place to catch a bite to eat on Thursdays." + {:small "http://cloudfront.net/4e782847-17da-4f87-bc31-fb61f22f3928/small.jpg", + :medium "http://cloudfront.net/4e782847-17da-4f87-bc31-fb61f22f3928/med.jpg", + :large "http://cloudfront.net/4e782847-17da-4f87-bc31-fb61f22f3928/large.jpg"} + {:name "Nob Hill Korean Taqueria", :categories ["Korean" "Taqueria"], :phone "415-107-7332", :id "a43c184c-90f5-488c-bb3b-00ea2666d90e"} + {:service "foursquare", :foursquare-photo-id "ffbd6d15-24cf-4dd3-a168-40ac03cd2f18", :mayor "rasta_toucan"}] + ["SoMa British Bakery is a fantastic and modern place to take a date on public holidays." + {:small "http://cloudfront.net/cda97264-0585-4313-91c5-2d022eae6048/small.jpg", + :medium "http://cloudfront.net/cda97264-0585-4313-91c5-2d022eae6048/med.jpg", + :large "http://cloudfront.net/cda97264-0585-4313-91c5-2d022eae6048/large.jpg"} + {:name "SoMa British Bakery", :categories ["British" "Bakery"], :phone "415-909-5728", :id "662cb0d0-8ee6-4db7-aaf1-89eb2530feda"} + {:service "facebook", :facebook-photo-id "9ecd820a-7017-4be2-a4b1-18ab0da5e233", :url "http://facebook.com/photos/9ecd820a-7017-4be2-a4b1-18ab0da5e233"}] + ["Haight Mexican Restaurant is a exclusive and swell place to nurse a hangover on a Tuesday afternoon." + {:small "http://cloudfront.net/a8b906e6-9b89-4e89-b997-1476ba37e137/small.jpg", + :medium "http://cloudfront.net/a8b906e6-9b89-4e89-b997-1476ba37e137/med.jpg", + :large "http://cloudfront.net/a8b906e6-9b89-4e89-b997-1476ba37e137/large.jpg"} + {:name "Haight Mexican Restaurant", :categories ["Mexican" "Restaurant"], :phone "415-758-8690", :id "a5e3f0ac-f6e8-4e71-a0a2-3f10f48b4ab1"} + {:service "foursquare", :foursquare-photo-id "4a084c4a-884f-4907-9b96-390b543cb03f", :mayor "biggie"}] + ["Sameer's Chinese Restaurant is a horrible and well-decorated place to have a drink on public holidays." + {:small "http://cloudfront.net/658de6d7-a7c1-4130-a5e5-d65946445068/small.jpg", + :medium "http://cloudfront.net/658de6d7-a7c1-4130-a5e5-d65946445068/med.jpg", + :large "http://cloudfront.net/658de6d7-a7c1-4130-a5e5-d65946445068/large.jpg"} + {:name "Sameer's Chinese Restaurant", :categories ["Chinese" "Restaurant"], :phone "415-707-3659", :id "51a9545e-7e1e-40f1-b550-09067b648f20"} + {:service "facebook", :facebook-photo-id "2f1c35f8-83fb-4c81-a92c-00a81d9c93de", :url "http://facebook.com/photos/2f1c35f8-83fb-4c81-a92c-00a81d9c93de"}] + ["Chinatown Paleo Food Truck is a groovy and decent place to drink a craft beer during summer." + {:small "http://cloudfront.net/8fc01dbf-d0ff-4a50-b460-6e92367ced21/small.jpg", + :medium "http://cloudfront.net/8fc01dbf-d0ff-4a50-b460-6e92367ced21/med.jpg", + :large "http://cloudfront.net/8fc01dbf-d0ff-4a50-b460-6e92367ced21/large.jpg"} + {:name "Chinatown Paleo Food Truck", :categories ["Paleo" "Food Truck"], :phone "415-583-4380", :id "aa9b5ce9-db74-470e-8573-f2faca24d546"} + {:service "foursquare", :foursquare-photo-id "62fdb59c-0629-4adb-b4e1-6a6cb7c7e280", :mayor "mandy"}] + ["Rasta's Mexican Sushi is a acceptable and exclusive place to have a drink weekday afternoons." + {:small "http://cloudfront.net/74795ea3-50d8-450a-b82b-33e6a1bb2e2c/small.jpg", + :medium "http://cloudfront.net/74795ea3-50d8-450a-b82b-33e6a1bb2e2c/med.jpg", + :large "http://cloudfront.net/74795ea3-50d8-450a-b82b-33e6a1bb2e2c/large.jpg"} + {:name "Rasta's Mexican Sushi", :categories ["Mexican" "Sushi"], :phone "415-387-1284", :id "e4912a22-e6ac-4806-8377-6497bf533a21"} + {:service "foursquare", :foursquare-photo-id "089ff188-9374-4b13-93e6-005c9767dabb", :mayor "bob"}] + ["Haight Mexican Restaurant is a modern and groovy place to watch the Warriors game on a Tuesday afternoon." + {:small "http://cloudfront.net/5e14844c-74af-4fdd-861d-293877f4505c/small.jpg", + :medium "http://cloudfront.net/5e14844c-74af-4fdd-861d-293877f4505c/med.jpg", + :large "http://cloudfront.net/5e14844c-74af-4fdd-861d-293877f4505c/large.jpg"} + {:name "Haight Mexican Restaurant", :categories ["Mexican" "Restaurant"], :phone "415-758-8690", :id "a5e3f0ac-f6e8-4e71-a0a2-3f10f48b4ab1"} + {:service "yelp", :yelp-photo-id "42494912-52c5-4492-83c8-76454f465d99", :categories ["Mexican" "Restaurant"]}] + ["Nob Hill Gluten-Free Coffee House is a horrible and classic place to people-watch during summer." + {:small "http://cloudfront.net/b3276e40-75ad-497a-a2d9-a3d3dfdf39cc/small.jpg", + :medium "http://cloudfront.net/b3276e40-75ad-497a-a2d9-a3d3dfdf39cc/med.jpg", + :large "http://cloudfront.net/b3276e40-75ad-497a-a2d9-a3d3dfdf39cc/large.jpg"} + {:name "Nob Hill Gluten-Free Coffee House", :categories ["Gluten-Free" "Coffee House"], :phone "415-605-9554", :id "df57ff6d-1b5f-46da-b292-32321c6b1a7e"} + {:service "facebook", :facebook-photo-id "d326a510-ac21-46f3-8474-078ef511cc54", :url "http://facebook.com/photos/d326a510-ac21-46f3-8474-078ef511cc54"}] + ["Mission British Café is a underground and groovy place to have brunch weekday afternoons." + {:small "http://cloudfront.net/67d2f053-4bf1-43da-9135-0751266acd32/small.jpg", + :medium "http://cloudfront.net/67d2f053-4bf1-43da-9135-0751266acd32/med.jpg", + :large "http://cloudfront.net/67d2f053-4bf1-43da-9135-0751266acd32/large.jpg"} + {:name "Mission British Café", :categories ["British" "Café"], :phone "415-715-7004", :id "c99899e3-439c-4444-9dc4-5598632aec8d"} + {:service "foursquare", :foursquare-photo-id "1df360d1-5d63-4a28-9c37-966dc7e616f0", :mayor "joe"}] + ["SoMa Japanese Churros is a classic and popular place to watch the Warriors game on public holidays." + {:small "http://cloudfront.net/a0c29909-96a1-4292-95b7-0d3daaf25634/small.jpg", + :medium "http://cloudfront.net/a0c29909-96a1-4292-95b7-0d3daaf25634/med.jpg", + :large "http://cloudfront.net/a0c29909-96a1-4292-95b7-0d3daaf25634/large.jpg"} + {:name "SoMa Japanese Churros", :categories ["Japanese" "Churros"], :phone "415-404-1510", :id "373858b2-e634-45d0-973d-4d0fed8c438b"} + {:service "foursquare", :foursquare-photo-id "2dda53d5-9b31-4f4e-b36b-e37b45e67de7", :mayor "biggie"}] + ["Haight Soul Food Hotel & Restaurant is a amazing and acceptable place to have breakfast weekday afternoons." + {:small "http://cloudfront.net/836d04ac-32b2-4c7a-888f-155ca6e93634/small.jpg", + :medium "http://cloudfront.net/836d04ac-32b2-4c7a-888f-155ca6e93634/med.jpg", + :large "http://cloudfront.net/836d04ac-32b2-4c7a-888f-155ca6e93634/large.jpg"} + {:name "Haight Soul Food Hotel & Restaurant", :categories ["Soul Food" "Hotel & Restaurant"], :phone "415-786-9541", :id "11a72eb3-9e96-4703-9a01-e8c2a9469046"} + {:service "flare", :username "bob"}] + ["Lower Pac Heights Cage-Free Coffee House is a atmospheric and well-decorated place to have brunch weekend mornings." + {:small "http://cloudfront.net/37488ad8-7f52-40e4-9330-505b7174701d/small.jpg", + :medium "http://cloudfront.net/37488ad8-7f52-40e4-9330-505b7174701d/med.jpg", + :large "http://cloudfront.net/37488ad8-7f52-40e4-9330-505b7174701d/large.jpg"} + {:name "Lower Pac Heights Cage-Free Coffee House", :categories ["Cage-Free" "Coffee House"], :phone "415-697-9309", :id "02b1f618-41a0-406b-96dd-1a017f630b81"} + {:service "facebook", :facebook-photo-id "01dcf27f-6233-415d-8b8b-4fe7a841b457", :url "http://facebook.com/photos/01dcf27f-6233-415d-8b8b-4fe7a841b457"}] + ["Haight Gormet Pizzeria is a fantastic and modern place to pitch an investor during winter." + {:small "http://cloudfront.net/482b6b0e-88ab-4963-bc96-00add6dde0ec/small.jpg", + :medium "http://cloudfront.net/482b6b0e-88ab-4963-bc96-00add6dde0ec/med.jpg", + :large "http://cloudfront.net/482b6b0e-88ab-4963-bc96-00add6dde0ec/large.jpg"} + {:name "Haight Gormet Pizzeria", :categories ["Gormet" "Pizzeria"], :phone "415-869-2197", :id "0425bdd0-3f57-4108-80e3-78335327355a"} + {:service "yelp", :yelp-photo-id "ad5aa48a-bdda-4396-86f3-1dc63b40a0d1", :categories ["Gormet" "Pizzeria"]}] + ["Rasta's Paleo Café is a wonderful and family-friendly place to have a drink on Taco Tuesday." + {:small "http://cloudfront.net/96553ade-9bcd-464f-888e-7b4b621e44fd/small.jpg", + :medium "http://cloudfront.net/96553ade-9bcd-464f-888e-7b4b621e44fd/med.jpg", + :large "http://cloudfront.net/96553ade-9bcd-464f-888e-7b4b621e44fd/large.jpg"} + {:name "Rasta's Paleo Café", :categories ["Paleo" "Café"], :phone "415-392-6341", :id "4f9e69be-f06c-46a0-bb8b-f3ddd8218ca1"} + {:service "facebook", :facebook-photo-id "f62fb4d0-d254-4496-a5c9-01ed4f7a3651", :url "http://facebook.com/photos/f62fb4d0-d254-4496-a5c9-01ed4f7a3651"}] + ["Kyle's Low-Carb Grill is a horrible and popular place to nurse a hangover weekend mornings." + {:small "http://cloudfront.net/048415e9-f535-44c3-85ce-cb08a29bb107/small.jpg", + :medium "http://cloudfront.net/048415e9-f535-44c3-85ce-cb08a29bb107/med.jpg", + :large "http://cloudfront.net/048415e9-f535-44c3-85ce-cb08a29bb107/large.jpg"} + {:name "Kyle's Low-Carb Grill", :categories ["Low-Carb" "Grill"], :phone "415-992-8278", :id "b27f50c6-55eb-48b0-9fee-17a6ef5243bd"} + {:service "flare", :username "bob"}] + ["Pacific Heights Red White & Blue Bar & Grill is a modern and historical place to have a after-work cocktail with your pet dog." + {:small "http://cloudfront.net/7b7cbf78-f8fe-4cb1-a086-2e9cdd0fba9c/small.jpg", + :medium "http://cloudfront.net/7b7cbf78-f8fe-4cb1-a086-2e9cdd0fba9c/med.jpg", + :large "http://cloudfront.net/7b7cbf78-f8fe-4cb1-a086-2e9cdd0fba9c/large.jpg"} + {:name "Pacific Heights Red White & Blue Bar & Grill", :categories ["Red White & Blue" "Bar & Grill"], :phone "415-208-2550", :id "c7547aa1-94c1-44bd-bf5a-8655e4698ed8"} + {:service "foursquare", :foursquare-photo-id "124a768c-40b4-433c-b907-4ebcb575dd51", :mayor "biggie"}] + ["Sunset American Churros is a overrated and wonderful place to take a date in July." + {:small "http://cloudfront.net/18ddcbe9-b183-4281-8f43-5f318e8d841e/small.jpg", + :medium "http://cloudfront.net/18ddcbe9-b183-4281-8f43-5f318e8d841e/med.jpg", + :large "http://cloudfront.net/18ddcbe9-b183-4281-8f43-5f318e8d841e/large.jpg"} + {:name "Sunset American Churros", :categories ["American" "Churros"], :phone "415-191-5018", :id "2e88c921-29fb-489b-a956-d3ba1182da73"} + {:service "yelp", :yelp-photo-id "ad2c2b45-b959-48cc-aac7-c049a3902c3a", :categories ["American" "Churros"]}] + ["Chinatown Paleo Food Truck is a great and modern place to have a birthday party on Thursdays." + {:small "http://cloudfront.net/2ca5c924-bb42-4f7f-a6bd-eb17b1c9e6aa/small.jpg", + :medium "http://cloudfront.net/2ca5c924-bb42-4f7f-a6bd-eb17b1c9e6aa/med.jpg", + :large "http://cloudfront.net/2ca5c924-bb42-4f7f-a6bd-eb17b1c9e6aa/large.jpg"} + {:name "Chinatown Paleo Food Truck", :categories ["Paleo" "Food Truck"], :phone "415-583-4380", :id "aa9b5ce9-db74-470e-8573-f2faca24d546"} + {:service "yelp", :yelp-photo-id "1f41e84f-2cbb-4579-a7d4-43d4e8532353", :categories ["Paleo" "Food Truck"]}] + ["Market St. European Ice Cream Truck is a popular and historical place to pitch an investor on Thursdays." + {:small "http://cloudfront.net/2731d670-e7d3-4ade-a1da-d1381355276f/small.jpg", + :medium "http://cloudfront.net/2731d670-e7d3-4ade-a1da-d1381355276f/med.jpg", + :large "http://cloudfront.net/2731d670-e7d3-4ade-a1da-d1381355276f/large.jpg"} + {:name "Market St. European Ice Cream Truck", :categories ["European" "Ice Cream Truck"], :phone "415-555-4197", :id "4ed53fe4-4bd9-4fa3-8f61-374ea75129ca"} + {:service "yelp", :yelp-photo-id "b1d569ba-ab45-4965-af9d-366dd4bc44bb", :categories ["European" "Ice Cream Truck"]}] + ["Haight Gormet Pizzeria is a well-decorated and amazing place to take visiting friends and relatives on Taco Tuesday." + {:small "http://cloudfront.net/485712f2-ec47-43df-9a32-f872aed21d81/small.jpg", + :medium "http://cloudfront.net/485712f2-ec47-43df-9a32-f872aed21d81/med.jpg", + :large "http://cloudfront.net/485712f2-ec47-43df-9a32-f872aed21d81/large.jpg"} + {:name "Haight Gormet Pizzeria", :categories ["Gormet" "Pizzeria"], :phone "415-869-2197", :id "0425bdd0-3f57-4108-80e3-78335327355a"} + {:service "twitter", :mentions ["@haight_gormet_pizzeria"], :tags ["#gormet" "#pizzeria"], :username "rasta_toucan"}] + ["SoMa TaquerÃa Diner is a well-decorated and acceptable place to have a after-work cocktail weekend evenings." + {:small "http://cloudfront.net/c9e18865-81de-4444-93fa-dd77ff19977e/small.jpg", + :medium "http://cloudfront.net/c9e18865-81de-4444-93fa-dd77ff19977e/med.jpg", + :large "http://cloudfront.net/c9e18865-81de-4444-93fa-dd77ff19977e/large.jpg"} + {:name "SoMa TaquerÃa Diner", :categories ["TaquerÃa" "Diner"], :phone "415-947-9521", :id "f97ede4a-074f-4e24-babc-5c44f2be9c36"} + {:service "flare", :username "bob"}] + ["Cam's Soul Food Ice Cream Truck is a acceptable and fantastic place to people-watch with your pet toucan." + {:small "http://cloudfront.net/f38cd426-8b7f-4481-96b2-cb44095a0709/small.jpg", + :medium "http://cloudfront.net/f38cd426-8b7f-4481-96b2-cb44095a0709/med.jpg", + :large "http://cloudfront.net/f38cd426-8b7f-4481-96b2-cb44095a0709/large.jpg"} + {:name "Cam's Soul Food Ice Cream Truck", :categories ["Soul Food" "Ice Cream Truck"], :phone "415-270-8888", :id "f474e587-1801-43ea-93d5-4c4fd96460b8"} + {:service "flare", :username "lucky_pigeon"}] + ["Tenderloin Cage-Free Sushi is a delicious and delicious place to have breakfast with your pet toucan." + {:small "http://cloudfront.net/45dc5a3b-a0c5-4f69-afc8-711cfc5f84b5/small.jpg", + :medium "http://cloudfront.net/45dc5a3b-a0c5-4f69-afc8-711cfc5f84b5/med.jpg", + :large "http://cloudfront.net/45dc5a3b-a0c5-4f69-afc8-711cfc5f84b5/large.jpg"} + {:name "Tenderloin Cage-Free Sushi", :categories ["Cage-Free" "Sushi"], :phone "415-348-0644", :id "0b6c036f-82b0-4008-bdfe-5360dd93fb75"} + {:service "flare", :username "bob"}] + ["Polk St. Korean Taqueria is a horrible and atmospheric place to people-watch in July." + {:small "http://cloudfront.net/fc6f7000-2347-49dc-aeff-22559cfd7ce8/small.jpg", + :medium "http://cloudfront.net/fc6f7000-2347-49dc-aeff-22559cfd7ce8/med.jpg", + :large "http://cloudfront.net/fc6f7000-2347-49dc-aeff-22559cfd7ce8/large.jpg"} + {:name "Polk St. Korean Taqueria", :categories ["Korean" "Taqueria"], :phone "415-511-5531", :id "ddb09b32-6f0b-4e54-98c7-14398f16ca4e"} + {:service "foursquare", :foursquare-photo-id "e48f5b9f-a12e-4339-b6d7-992c44aa481b", :mayor "kyle"}] + ["Sunset Homestyle Grill is a world-famous and modern place to sip Champagne Friday nights." + {:small "http://cloudfront.net/d3f31d2b-f10a-44cb-91a7-37e5b9491fbc/small.jpg", + :medium "http://cloudfront.net/d3f31d2b-f10a-44cb-91a7-37e5b9491fbc/med.jpg", + :large "http://cloudfront.net/d3f31d2b-f10a-44cb-91a7-37e5b9491fbc/large.jpg"} + {:name "Sunset Homestyle Grill", :categories ["Homestyle" "Grill"], :phone "415-356-7052", :id "c57673cd-f2d0-4bbc-aed0-6c166d7cf2c3"} + {:service "flare", :username "jessica"}] + ["Chinatown American Bakery is a underground and underappreciated place to have brunch in June." + {:small "http://cloudfront.net/1707f0ac-6df5-4340-a5ef-c31609c4ead0/small.jpg", + :medium "http://cloudfront.net/1707f0ac-6df5-4340-a5ef-c31609c4ead0/med.jpg", + :large "http://cloudfront.net/1707f0ac-6df5-4340-a5ef-c31609c4ead0/large.jpg"} + {:name "Chinatown American Bakery", :categories ["American" "Bakery"], :phone "415-658-7393", :id "cf55cdbd-c614-4be1-8496-0e11b195d16f"} + {:service "twitter", :mentions ["@chinatown_american_bakery"], :tags ["#american" "#bakery"], :username "amy"}] + ["Pacific Heights Red White & Blue Bar & Grill is a swell and acceptable place to take visiting friends and relatives with your pet toucan." + {:small "http://cloudfront.net/37a5f9f6-b1bd-405f-bec2-fad8a476cf62/small.jpg", + :medium "http://cloudfront.net/37a5f9f6-b1bd-405f-bec2-fad8a476cf62/med.jpg", + :large "http://cloudfront.net/37a5f9f6-b1bd-405f-bec2-fad8a476cf62/large.jpg"} + {:name "Pacific Heights Red White & Blue Bar & Grill", :categories ["Red White & Blue" "Bar & Grill"], :phone "415-208-2550", :id "c7547aa1-94c1-44bd-bf5a-8655e4698ed8"} + {:service "yelp", :yelp-photo-id "6f0c3b95-145b-4b9f-8c63-a04ca70e3835", :categories ["Red White & Blue" "Bar & Grill"]}] + ["Sunset American Churros is a popular and delicious place to watch the Giants game on Taco Tuesday." + {:small "http://cloudfront.net/75a631a2-43b1-47a3-a7a6-687c853b857c/small.jpg", + :medium "http://cloudfront.net/75a631a2-43b1-47a3-a7a6-687c853b857c/med.jpg", + :large "http://cloudfront.net/75a631a2-43b1-47a3-a7a6-687c853b857c/large.jpg"} + {:name "Sunset American Churros", :categories ["American" "Churros"], :phone "415-191-5018", :id "2e88c921-29fb-489b-a956-d3ba1182da73"} + {:service "twitter", :mentions ["@sunset_american_churros"], :tags ["#american" "#churros"], :username "amy"}] + ["Sunset Homestyle Grill is a underappreciated and swell place to watch the Giants game on Thursdays." + {:small "http://cloudfront.net/2bd0472b-267c-4be6-992d-94e72edfe719/small.jpg", + :medium "http://cloudfront.net/2bd0472b-267c-4be6-992d-94e72edfe719/med.jpg", + :large "http://cloudfront.net/2bd0472b-267c-4be6-992d-94e72edfe719/large.jpg"} + {:name "Sunset Homestyle Grill", :categories ["Homestyle" "Grill"], :phone "415-356-7052", :id "c57673cd-f2d0-4bbc-aed0-6c166d7cf2c3"} + {:service "foursquare", :foursquare-photo-id "9ae208eb-c7c1-43b1-9a1e-f7a4d276e8e2", :mayor "cam_saul"}] + ["Rasta's British Food Truck is a classic and historical place to sip a glass of expensive wine Friday nights." + {:small "http://cloudfront.net/253137fd-7a54-431e-a991-a1a5175ff9fc/small.jpg", + :medium "http://cloudfront.net/253137fd-7a54-431e-a991-a1a5175ff9fc/med.jpg", + :large "http://cloudfront.net/253137fd-7a54-431e-a991-a1a5175ff9fc/large.jpg"} + {:name "Rasta's British Food Truck", :categories ["British" "Food Truck"], :phone "415-958-9031", :id "b6616c97-01d0-488f-a855-bcd6efe2b899"} + {:service "facebook", :facebook-photo-id "647413c2-f132-46e9-ac47-155cec8346d0", :url "http://facebook.com/photos/647413c2-f132-46e9-ac47-155cec8346d0"}] + ["Polk St. Korean Taqueria is a groovy and great place to watch the Giants game on public holidays." + {:small "http://cloudfront.net/fabc9014-285d-41b4-8b1c-a66ecae0e8b0/small.jpg", + :medium "http://cloudfront.net/fabc9014-285d-41b4-8b1c-a66ecae0e8b0/med.jpg", + :large "http://cloudfront.net/fabc9014-285d-41b4-8b1c-a66ecae0e8b0/large.jpg"} + {:name "Polk St. Korean Taqueria", :categories ["Korean" "Taqueria"], :phone "415-511-5531", :id "ddb09b32-6f0b-4e54-98c7-14398f16ca4e"} + {:service "flare", :username "jessica"}] + ["Nob Hill Gluten-Free Coffee House is a decent and acceptable place to watch the Giants game on Taco Tuesday." + {:small "http://cloudfront.net/9e1072a8-82ee-46ee-a625-578384e641f7/small.jpg", + :medium "http://cloudfront.net/9e1072a8-82ee-46ee-a625-578384e641f7/med.jpg", + :large "http://cloudfront.net/9e1072a8-82ee-46ee-a625-578384e641f7/large.jpg"} + {:name "Nob Hill Gluten-Free Coffee House", :categories ["Gluten-Free" "Coffee House"], :phone "415-605-9554", :id "df57ff6d-1b5f-46da-b292-32321c6b1a7e"} + {:service "foursquare", :foursquare-photo-id "2d74b28c-aab1-489e-a18b-1bfb3495cc19", :mayor "sameer"}] + ["Oakland American Grill is a acceptable and popular place to conduct a business meeting on Taco Tuesday." + {:small "http://cloudfront.net/beff31d4-4685-4805-bf89-dd705ada2fe2/small.jpg", + :medium "http://cloudfront.net/beff31d4-4685-4805-bf89-dd705ada2fe2/med.jpg", + :large "http://cloudfront.net/beff31d4-4685-4805-bf89-dd705ada2fe2/large.jpg"} + {:name "Oakland American Grill", :categories ["American" "Grill"], :phone "415-660-0889", :id "856f907d-b669-4b9c-8337-bf9c88883746"} + {:service "foursquare", :foursquare-photo-id "18546192-88d1-4c74-a40e-b502d32f7a5b", :mayor "mandy"}] + ["Pacific Heights Soul Food Coffee House is a decent and swell place to catch a bite to eat on Saturday night." + {:small "http://cloudfront.net/2018a275-f498-47bf-a15b-e7b1c57147f0/small.jpg", + :medium "http://cloudfront.net/2018a275-f498-47bf-a15b-e7b1c57147f0/med.jpg", + :large "http://cloudfront.net/2018a275-f498-47bf-a15b-e7b1c57147f0/large.jpg"} + {:name "Pacific Heights Soul Food Coffee House", :categories ["Soul Food" "Coffee House"], :phone "415-838-3464", :id "6aed1816-4c64-4f76-8e2a-619528e5b48d"} + {:service "twitter", :mentions ["@pacific_heights_soul_food_coffee_house"], :tags ["#soul" "#food" "#coffee" "#house"], :username "jane"}] + ["Polk St. Deep-Dish Hotel & Restaurant is a delicious and acceptable place to take a date on Taco Tuesday." + {:small "http://cloudfront.net/1bc7f807-4ee1-4c37-af6e-918e978b95fd/small.jpg", + :medium "http://cloudfront.net/1bc7f807-4ee1-4c37-af6e-918e978b95fd/med.jpg", + :large "http://cloudfront.net/1bc7f807-4ee1-4c37-af6e-918e978b95fd/large.jpg"} + {:name "Polk St. Deep-Dish Hotel & Restaurant", :categories ["Deep-Dish" "Hotel & Restaurant"], :phone "415-666-8681", :id "47f1698e-ae11-46f5-818b-85a59d0affba"} + {:service "foursquare", :foursquare-photo-id "2efbeca5-5a55-4310-abdd-936ae97bc1bc", :mayor "biggie"}] + ["Pacific Heights Soul Food Coffee House is a fantastic and popular place to watch the Giants game in July." + {:small "http://cloudfront.net/4ad94fa2-4174-4617-872b-a925006d0841/small.jpg", + :medium "http://cloudfront.net/4ad94fa2-4174-4617-872b-a925006d0841/med.jpg", + :large "http://cloudfront.net/4ad94fa2-4174-4617-872b-a925006d0841/large.jpg"} + {:name "Pacific Heights Soul Food Coffee House", :categories ["Soul Food" "Coffee House"], :phone "415-838-3464", :id "6aed1816-4c64-4f76-8e2a-619528e5b48d"} + {:service "twitter", :mentions ["@pacific_heights_soul_food_coffee_house"], :tags ["#soul" "#food" "#coffee" "#house"], :username "cam_saul"}] + ["Tenderloin Gormet Restaurant is a horrible and underground place to have brunch in July." + {:small "http://cloudfront.net/7120084f-5792-425f-8469-170b6fa01df1/small.jpg", + :medium "http://cloudfront.net/7120084f-5792-425f-8469-170b6fa01df1/med.jpg", + :large "http://cloudfront.net/7120084f-5792-425f-8469-170b6fa01df1/large.jpg"} + {:name "Tenderloin Gormet Restaurant", :categories ["Gormet" "Restaurant"], :phone "415-127-4197", :id "54a9eac8-d80d-4af8-b6d7-34651a60e59c"} + {:service "yelp", :yelp-photo-id "86617b70-de39-40aa-9ca2-9c33c5bd1fa4", :categories ["Gormet" "Restaurant"]}] + ["Pacific Heights Red White & Blue Bar & Grill is a underappreciated and modern place to meet new friends with friends." + {:small "http://cloudfront.net/a0d24e43-c01d-4b02-b236-e8dc2155aa37/small.jpg", + :medium "http://cloudfront.net/a0d24e43-c01d-4b02-b236-e8dc2155aa37/med.jpg", + :large "http://cloudfront.net/a0d24e43-c01d-4b02-b236-e8dc2155aa37/large.jpg"} + {:name "Pacific Heights Red White & Blue Bar & Grill", :categories ["Red White & Blue" "Bar & Grill"], :phone "415-208-2550", :id "c7547aa1-94c1-44bd-bf5a-8655e4698ed8"} + {:service "twitter", :mentions ["@pacific_heights_red_white_&_blue_bar_&_grill"], :tags ["#red" "#white" "#&" "#blue" "#bar" "#&" "#grill"], :username "mandy"}] + ["Nob Hill Gluten-Free Coffee House is a underappreciated and swell place to take visiting friends and relatives with your pet dog." + {:small "http://cloudfront.net/1e92e043-2910-4d1b-b72e-43608cc0e2ff/small.jpg", + :medium "http://cloudfront.net/1e92e043-2910-4d1b-b72e-43608cc0e2ff/med.jpg", + :large "http://cloudfront.net/1e92e043-2910-4d1b-b72e-43608cc0e2ff/large.jpg"} + {:name "Nob Hill Gluten-Free Coffee House", :categories ["Gluten-Free" "Coffee House"], :phone "415-605-9554", :id "df57ff6d-1b5f-46da-b292-32321c6b1a7e"} + {:service "flare", :username "jane"}] + ["Mission Japanese Coffee House is a modern and historical place to take visiting friends and relatives on Taco Tuesday." + {:small "http://cloudfront.net/7dcd5015-ddf0-40db-a38e-c40308a00720/small.jpg", + :medium "http://cloudfront.net/7dcd5015-ddf0-40db-a38e-c40308a00720/med.jpg", + :large "http://cloudfront.net/7dcd5015-ddf0-40db-a38e-c40308a00720/large.jpg"} + {:name "Mission Japanese Coffee House", :categories ["Japanese" "Coffee House"], :phone "415-561-0506", :id "60dd274e-0cbf-4521-946d-8a4e0f151150"} + {:service "facebook", :facebook-photo-id "83699a56-416c-49cc-a31a-dd6b4c5aa91b", :url "http://facebook.com/photos/83699a56-416c-49cc-a31a-dd6b4c5aa91b"}] + ["Oakland Low-Carb Bakery is a modern and fantastic place to conduct a business meeting on a Tuesday afternoon." + {:small "http://cloudfront.net/6b16ed76-4ccb-4ce2-b255-55f69271ddf1/small.jpg", + :medium "http://cloudfront.net/6b16ed76-4ccb-4ce2-b255-55f69271ddf1/med.jpg", + :large "http://cloudfront.net/6b16ed76-4ccb-4ce2-b255-55f69271ddf1/large.jpg"} + {:name "Oakland Low-Carb Bakery", :categories ["Low-Carb" "Bakery"], :phone "415-546-0101", :id "da7dd72d-60fb-495b-a2c0-1e2ae73a1a86"} + {:service "twitter", :mentions ["@oakland_low_carb_bakery"], :tags ["#low-carb" "#bakery"], :username "bob"}] + ["Cam's Old-Fashioned Coffee House is a world-famous and decent place to sip a glass of expensive wine on a Tuesday afternoon." + {:small "http://cloudfront.net/1282e126-e15b-4aa4-a3dc-869ea9d6d6c2/small.jpg", + :medium "http://cloudfront.net/1282e126-e15b-4aa4-a3dc-869ea9d6d6c2/med.jpg", + :large "http://cloudfront.net/1282e126-e15b-4aa4-a3dc-869ea9d6d6c2/large.jpg"} + {:name "Cam's Old-Fashioned Coffee House", :categories ["Old-Fashioned" "Coffee House"], :phone "415-868-2973", :id "27592c2b-e682-44bb-be28-8e9a622becca"} + {:service "yelp", :yelp-photo-id "de90a66b-d19e-4665-b0c2-c64ba899a02b", :categories ["Old-Fashioned" "Coffee House"]}] + ["Rasta's Mexican Sushi is a fantastic and atmospheric place to have a after-work cocktail weekday afternoons." + {:small "http://cloudfront.net/65dc9189-9de4-4bdc-bdfb-e2ff8247829b/small.jpg", + :medium "http://cloudfront.net/65dc9189-9de4-4bdc-bdfb-e2ff8247829b/med.jpg", + :large "http://cloudfront.net/65dc9189-9de4-4bdc-bdfb-e2ff8247829b/large.jpg"} + {:name "Rasta's Mexican Sushi", :categories ["Mexican" "Sushi"], :phone "415-387-1284", :id "e4912a22-e6ac-4806-8377-6497bf533a21"} + {:service "flare", :username "amy"}] + ["SoMa Japanese Churros is a underappreciated and swell place to take a date Friday nights." + {:small "http://cloudfront.net/b8fe2a0a-3cfc-4e39-afb7-c1fa4ff708e2/small.jpg", + :medium "http://cloudfront.net/b8fe2a0a-3cfc-4e39-afb7-c1fa4ff708e2/med.jpg", + :large "http://cloudfront.net/b8fe2a0a-3cfc-4e39-afb7-c1fa4ff708e2/large.jpg"} + {:name "SoMa Japanese Churros", :categories ["Japanese" "Churros"], :phone "415-404-1510", :id "373858b2-e634-45d0-973d-4d0fed8c438b"} + {:service "facebook", :facebook-photo-id "bdd68c35-bffc-4091-b32c-bf8bf3a8f834", :url "http://facebook.com/photos/bdd68c35-bffc-4091-b32c-bf8bf3a8f834"}] + ["Lucky's Cage-Free Liquor Store is a modern and groovy place to take a date with friends." + {:small "http://cloudfront.net/6bccb82b-1102-4f60-aa9b-372ee1164118/small.jpg", + :medium "http://cloudfront.net/6bccb82b-1102-4f60-aa9b-372ee1164118/med.jpg", + :large "http://cloudfront.net/6bccb82b-1102-4f60-aa9b-372ee1164118/large.jpg"} + {:name "Lucky's Cage-Free Liquor Store", :categories ["Cage-Free" "Liquor Store"], :phone "415-341-3219", :id "968eac8d-1fd0-443d-a7ff-b48810ab0f69"} + {:service "flare", :username "lucky_pigeon"}]]]] diff --git a/test/metabase/test/data/h2.clj b/test/metabase/test/data/h2.clj index c210f8fe09224df5472bba088301f1279cca7a97..3a6e869dc4364310e930ca5fc3f5a24b7399712b 100644 --- a/test/metabase/test/data/h2.clj +++ b/test/metabase/test/data/h2.clj @@ -63,7 +63,7 @@ (defrecord H2DatasetLoader [] generic/IGenericSQLDatasetLoader (generic/execute-sql! [_ database-definition raw-sql] - (log/info raw-sql) + (log/debug raw-sql) (k/exec-raw (korma-connection-pool database-definition) raw-sql)) (generic/korma-entity [_ database-definition table-definition] diff --git a/test/metabase/test/data/postgres.clj b/test/metabase/test/data/postgres.clj index 09180a9192ce95eeee94e844bb0507f630778d38..a77e13c5b17ba4a64427c9b96c118cdbb25941ff 100644 --- a/test/metabase/test/data/postgres.clj +++ b/test/metabase/test/data/postgres.clj @@ -47,7 +47,7 @@ (defrecord PostgresDatasetLoader [] generic/IGenericSQLDatasetLoader (generic/execute-sql! [_ database-definition raw-sql] - (log/info raw-sql) + (log/debug raw-sql) (execute! :db database-definition raw-sql)) (generic/korma-entity [_ database-definition table-definition] diff --git a/test/metabase/test/util/mql.clj b/test/metabase/test/util/mql.clj index a1d25d8fe030603860b569d0b7303c2c86a6312a..f37b175fa9b613dcf871e9b299c90db25dc5d5c2 100644 --- a/test/metabase/test/util/mql.clj +++ b/test/metabase/test/util/mql.clj @@ -5,12 +5,14 @@ [clojure.walk :refer [macroexpand-all]] [metabase.driver :as driver] [metabase.test.data :as data] - [metabase.test.data.datasets :as datasets])) + [metabase.test.data.datasets :as datasets] + [metabase.util :as u])) (defn- partition-tokens [keywords tokens] (->> (loop [all [], current-split nil, [token & more] tokens] (cond - (not token) (conj all current-split) + (and (not token) + (not (seq more))) (conj all current-split) (contains? keywords token) (recur (or (when (seq current-split) (conj all current-split)) all) @@ -22,8 +24,8 @@ (map seq) (filter identity))) -(def ^:private ^:const outer-q-tokens '#{with run return}) -(def ^:private ^:const inner-q-tokens '#{ag breakout fields filter lim order page tbl}) +(def ^:private ^:const outer-q-tokens '#{against with run return using}) +(def ^:private ^:const inner-q-tokens '#{ag aggregate breakout fields filter lim limit order page of tbl}) (defmacro Q:temp-get [& args] `(:id (data/-temp-get ~'db ~@(map name args)))) @@ -38,23 +40,31 @@ (macrolet [(~'id [& args#] `(Q:temp-get ~@args#))] ~(macroexpand-all body))))) +(defmacro Q:against [query arg] + `(Q:with-temp-db ~arg + ~query)) + +(defmacro Q:using [query arg] + `(datasets/with-dataset ~(keyword arg) + ~query)) + (defmacro Q:with [query arg & [arg2 :as more]] (case (keyword arg) - :db `(Q:with-temp-db ~arg2 - ~query) - :dataset `(datasets/with-dataset ~(keyword arg2) - ~query) + :db `(Q:against ~query ~arg2) + :dataset `(Q:using ~query ~arg2) :datasets `(do ~@(for [dataset# more] `(datasets/with-dataset ~(keyword dataset#) ~query))))) (defmacro Q:return [q & args] - `(-> ~q ~@args)) + `(->> ~q ~@args)) (defmacro Q:expand-outer [token form] - (macroexpand-all `(symbol-macrolet [~'return Q:return - ~'run driver/process-query - ~'with Q:with] + (macroexpand-all `(symbol-macrolet [~'against Q:against + ~'return Q:return + ~'run driver/process-query + ~'using Q:using + ~'with Q:with] (-> ~form ~token)))) (defmacro Q:expand-outer* [[token & tokens] form] @@ -63,8 +73,8 @@ (defmacro Q:expand-inner [& forms] {:database 'db-id - :type :query - :query `(Q:expand-clauses {} ~@forms)}) + :type :query + :query `(Q:expand-clauses {} ~@forms)}) (defmacro Q:wrap-fallback-captures [form] `(symbol-macrolet [~'db-id (data/db-id) @@ -72,23 +82,26 @@ ~(macroexpand-all form))) (defmacro Q:field [f] - (let [f (name f)] - (if-let [[_ from to] (re-matches #"^(.*)->(.*)$" f)] - ["fk->" `(Q:field ~(symbol from)) `(Q:field ~(symbol to))] - (if-let [[_ ag-field-index] (re-matches #"^ag\.(\d+)$" f)] - ["aggregation" (Integer/parseInt ag-field-index)] - (let [[_ table field] (re-matches #"^(?:([^\.]+)\.)?([^\.]+)$" f)] - `(~'id ~(if table (keyword table) - 'table) - ~(keyword field))))))) + (or (when (symbol? f) + (let [f (name f)] + (u/cond-let + [[_ from to] (re-matches #"^(.+)->(.+)$" f)] ["fk->" `(Q:field ~(symbol from)) `(Q:field ~(symbol to))] + [[_ f sub] (re-matches #"^(.+)\.\.\.(.+)$" f)] `(~@(macroexpand-1 `(Q:field ~(symbol f))) ~(keyword sub)) + [[_ ag-field-index] (re-matches #"^ag\.(\d+)$" f)] ["aggregation" (Integer/parseInt ag-field-index)] + [[_ table field] (re-matches #"^(?:([^\.]+)\.)?([^\.]+)$" f)] `(~'id ~(if table (keyword table) + 'table) + ~(keyword field))))) + f)) (defmacro Q [& tokens] (let [[outer-tokens inner-tokens] (split-with (complement (partial contains? inner-q-tokens)) tokens) outer-tokens (partition-tokens outer-q-tokens outer-tokens) inner-tokens (partition-tokens inner-q-tokens inner-tokens) - query (macroexpand-all `(Q:expand-inner ~@inner-tokens))] + query (macroexpand-all `(Q:expand-inner ~@inner-tokens)) + table (second (:source_table (:query query)))] + (assert table "No table specified. Did you include a `tbl`/`of` clause?") `(Q:wrap-fallback-captures (Q:expand-outer* ~outer-tokens - (symbol-macrolet [~'table ~(second (:source_table (:query query))) + (symbol-macrolet [~'table ~table ~'fl Q:field] ~(macroexpand-all query)))))) @@ -110,6 +123,9 @@ ['sum id] ["sum" `(~'fl ~id)] ['cum-sum id] ["cum_sum" `(~'fl ~id)]))) +(defmacro Q:aggregate [& args] + `(Q:ag ~@args)) + ;; ## breakout @@ -158,13 +174,17 @@ (defmacro Q:lim [query lim] (assoc query :limit lim)) +(defmacro Q:limit [& args] + `(Q:lim ~@args)) + ;; ## order (defmacro Q:order [query & fields] (assoc query :order_by (vec (for [field fields] `(Q:order* ~field))))) -(defmacro Q:order* [field] - (let [[_ field +-] (re-matches #"^([^\-+]+)([\-+])?$" (name field))] +(defmacro Q:order* [field-symb] + (let [[_ field +-] (re-matches #"^(.+[^\-+])([\-+])?$" (name field-symb))] + (assert field (format "Invalid field passed to order: '%s'" field-symb)) [`(~'fl ~(symbol field)) (case (keyword (or +- '+)) :+ "ascending" :- "descending")])) @@ -179,3 +199,6 @@ (defmacro Q:tbl [query table] (assoc query :source_table `(~'id ~(keyword table)))) + +(defmacro Q:of [query table] + `(Q:tbl ~query ~table))