You Can Text Yelp To Find Food Faster, Or Build The App Yourself In Ruby

March 20, 2017
Written by

Yelp3G

It’s SXSW season. The phrase “is there WiFI here?” is likely being uttered as you read this, and will be uttered every 1.3 seconds for the remainder of SXSW.

When there’s no WiFi and you’re hangry, your friend, 3G, is there for you. It’ll help you find a spot to take a breather, grab some food, and recharge. Armed with a 3G connection, and a Twilio number tied to a Yelp backend, you can browse through restaurants via text.

Using some “vanilla HTML” and the Twilio API, Ryan Kulp built a service that lets you text Yelp.

Ryan put together this Twilio app after wrestling with using Yelp in the mobile browser. He’d be prompted to download the Yelp app. He opted for viewing Yelp reviews in the browser. Then he’d pore through reviews Ryan describes as “multiple paragraphs long.” He wanted to build a quick way to get the information he needed, and do it outside of a browser or app.

When you text the Yelp 3G app, it’ll ask for a ZIP code you’d like to search in. Next, it’ll ask what you’d like to search for. It’ll then text you back a list of results including the restaurants’ phone numbers. You know, in case you want to call them and ask if they have WiFi.

If you’re texting into Ryan’s app searching for burritos and pizza all the time, Ryan will know. It’ll make that search easier for you. His HTTP request is stateless, but he needs to maintain some state to make recommendations easier, and tailored to each user. When you search for a restaurant, he logs each query so he can fire up a CSV, do a little data science and say “Oh, a ton of people in the 20815 zip code are looking for better mexican food”. (They are by the way)

If you can’t make up your mind on where to eat, here’s a little easter egg: you can text the app “random” to get — well — random results from Yelp.

Ryan was gracious enough to share a few snippets of code that power his app.

The Ruby Code Powering Ryan’s App

The Sign Up Flow

def register
  number = params['number']
  yelper = Yelper.create!(phone_number: number)

  @client.messages.create(
    from: ENV['TWILIO_PHONE_NUMBER'],
    to: yelper.phone_number,
    body: "Welcome to Yelp 3G! What's your zip code?"
  )

  redirect_to thanks_path
end

Ryan’s comments: There’s no web interface for yelp 3g. Users register by adding their cell number. Each number from the generic HTML registration form is sanitized server-side (vs in the browser) using an open source library called phony_rails.

This is how I’m able to simply pass in a (potentially mal-formatted) number, directly from the form fields, to the backend.

I’ve chosen a table name of Yelper to describe each user. following their commit to the database, Yelpers are sent a message asking for their zip code.
Onboarding
Ryan’s comments: When a user (Yelper) sends a message to a Yelp 3G dedicated (10-digit) number, the message is sent to a generic process_sms controller action.

Rails.application.routes.draw do
  root  'pages#home'
  get   'thanks', to: "pages#thanks", as: 'thanks'
  post  'register', to: "twilio#register"
  match 'twilio/process_sms' => 'twilio#process_sms', via: [:get, :post]
end

Ryan’s comments: In this process_sms action, we first identify the yelper by the params[:From] element in the Twilio JSON payload.

We also downcase and sanitize the params[:Body] value, to strip any characters that might confuse the Yelp API later (ie: whitespace, carriage returns, etc).

If a Yelper, after being identified by their phone number, does not yet have a zip code on file, Yelp 3G assumes their incoming message body is a zip code.

def process_zip_code(yelper)
  zip = params[:Body]
  yelper.update(zip_code: params[:Body])
  send_query_instructions(yelper)
end

A lot more could be done here to ensure a zipcode is properly formatted, or that the user’s input is even a zip code at all.

However, the worst case is a user’s results are not relevant. There aren’t any negative side effects from an application or exception perspective.

Once a user’s zip code is saved, yelp 3g sends an informational text about how to execute searches using yelp 3g.

Message Handling

Ryan’s comments: Simple conditionals route each incoming text message.

if @yelper.zip_code.nil?
  process_zip_code
else
  process_query
end

For example, if a Yelper has a saved zip code, future messages to the yelp 3g server are assumed to be queries.

def process_query
  if @query == 'random'
    process_random_search
  elsif @query.to_i.between?(1, 3)
    render_search_results
  else
    search_yelp
  end
end

There are a couple types of queries:

1. user is looking for a business, ie “cheap pizza”
2. user has received a few search results for “cheap pizza,” and wants more details about the 2nd one

HTTP Statelessness

Ryan’s Comments: Since it’s impossible to maintain an active connection between a texting Yelper and the yelp 3g server, we store some historical data to derive a user’s intent with a given message query.

In our database, there is also a Query database with search records owned by each Yelper.

create_table "queries", force: :cascade do |t|
  t.text    "query"
  t.integer "yelper_id"
end

add_index "queries", ["yelper_id"], name: "index_queries_on_yelper_id", using: :btree

Finding Businesses With Yelp

Ryan’s Notes: When a user wants to search yelp for a given query, we fetch these businesses through the yelp api and then format them for use in 2 locations:

1. Sending back to the user in a text message
2. Saving in the database for future retrieval of structure data

def parse_results(businesses)
  raw_results = []
  formatted_results = []

  businesses.each_with_index do |biz, idx|
    raw_results << [biz.name, biz.rating, biz.review_count]
    formatted_results << ((idx+1).to_s + '. ' +
                          biz.name + ', ' +
                          biz.rating.to_s + ' stars, ' +
                          biz.review_count.to_s + ' reviews.'
                         )
  end

  @yelper.queries.create(query: raw_results)
  send_query_results(formatted_results)
end

Drill-down On A Given Business

Ryan’s Notes: When users reply with an integer, ie between 1-3, we know they’re asking for more information about a specific search result.

def process_query
  if @query == 'random'
    process_random_search
  elsif @query.to_i.between?(1, 3)
    render_search_results
  else
    search_yelp
  end
end

Yelp 3g verifies this by checking against the user’s most recent Query in the database.

def process_query_drilldown
  target = @yelper.queries.last.query[query - 1]

  response = Yelp.client.search(@yelper.zip_code, {term: target[0]})
  business = response.businesses.first

  send_query_details(@yelper, business)
end

It is this process — saving a Yelper’s queries, and then referencing the most recent query upon each incoming text message, that yelp 3g maintains a sense of statefulness about how ‘drilled down’ a user’s search request is.

There you have it. Ryan’s not only made it easier for you to find food, he showed you how to build the app that helps you find food!