Sample Application: Ruby

Prerequsites

Before you begin to use these examples, make sure that:

All of the code we’re going to see is available on Github in the Primal developer onboarding repository.

Talking to Primal

The first, most obvious example of what to do with the data service is to interact with it in the first place. The following class will be our access point to Primal’s data service. It will identify our app, authenticate our user, and provide us with simple interaction methods.

Accessing Primal

#!/usr/bin/env ruby

# We need to use some ruby gems
require 'rubygems'

# We require these gems
#
# To install them:
#   gem install httparty
#   gem install json
#
require 'httparty'
require 'json'

#
# The PrimalAccess class abstracts the access to Primal such
# that you can call simple methods on it to get what you need.
#
class PrimalAccess
  include HTTParty
  base_uri 'https://data.primal.com'
  # Uncomment this next line to see what HTTParty is doing
  # debug_output $stderr
  # Set this to false in order to turn off debugging of this class
  @@debugMe = true

  #
  # Constructor for the PrimalAccess class
  #
  # Pass in the username and password of the user you're going
  # to access in order to construct and object that will work
  # with that user
  #
  def initialize(appId, appKey, username, password)
    @headers = {
      :headers => {
        'Primal-App-ID' => appId,
        'Primal-App-Key' => appKey
      },
      :basic_auth => {
        :username => username,
        :password => password
      }
    }
  end

  #
  # Sometimes we're going to get topics that are complex (i.e. they contain a
  # scheme and host) and we want to simplify those.  Because we're not making
  # calls with bare URLs but have told HTTParty what the base_uri is, we need to
  # pull that base uri off of the topic, should it be there.
  #
  def extractJustTopic(topic)
    topic.sub(%r{https://.*?/}, '/')
  end

  #
  # POSTs a new topic to Primal in order to seed that topic.
  #
  # The 'topic' parameter will be used to construct a POST URL
  # that looks like "/topic"
  #
  # Returns two values: the response code and the body.
  # Anything but a response code of 201 is to be considered
  # an error.
  #
  def postNewTopic(topic, opts = {})
    topic = extractJustTopic(topic)
    count = 0
    code = 400
    body = ''
    options = @headers.merge(opts)
    while (count < 5)
      if @@debugMe
        $stderr.puts "POSTing to #{topic}"
      end
      response = self.class.post("#{topic}", options)
      code = response.code
      body = response.body
      #
      # 400 - bad request
      # 401 - application not authorized to access the user's account
      # 403 - application not authorized to use Primal
      #
      if code >= 400 && code <= 403
        if @@debugMe
          $stderr.puts "POST received a #{code}"
        end
        break
      #
      # 429 - application has reached its request limit for the moment
      #
      elsif code == 429
        # Sleep for 10 seconds
        if @@debugMe
          $stderr.puts "Got a 429.  Waiting (#{count})."
        end
        sleep 10
        count += 1
      #
      # 201 - success
      #
      elsif code == 201
        if @@debugMe
          $stderr.puts "POST successful"
        end
        break
      else
        abort "Received unexpected response code (#{code}) for POST #{uri}"
      end
    end
    return code, body
  end

  #
  # Uses the pre-existing topic to filter the default source
  # of content through the interest network defined by the topic.
  #
  # The given parameter will be used to construct a GET URL
  # that looks like "/topic"
  #
  # You can pass a dictionary of optional arguments that will
  # be merged in to the query parameters, if you wish.
  # e.g. 
  #   { :"primal:contentScore:min" => 0.7 }
  #   { :"primal:contentCount:max" => 5 }
  #   { :contentSource => MyDataSource } ... or ...
  #   { :contentSource => PrimalSource }
  #
  # Returns two values: the response code, and the body.
  # If successful (i.e. a response code of 200) then the body
  # will be the JSON payload of the filtered content.
  #
  def filterContent(topic, opts = {})
    topic = extractJustTopic(topic)
    count = 0
    code = 400
    body = ''
    options = @headers.merge({ :query => {
        :timeOut => 'max'
      }.merge(opts)
    })
    while (count < 10)
      if @@debugMe
        $stderr.puts "GETting #{topic}"
      end
      response = self.class.get("#{topic}", options)
      code = response.code
      body = response.body
      #
      # 400 - bad request
      # 401 - application not authorized to access the user's account
      # 403 - application not authorized to use Primal
      # 404 - object not found
      #
      if code >= 400 && code <= 404
        if @@debugMe
          $stderr.puts "GET received a #{code}"
        end
        break
      #
      # 429 - application has reached its request limit for the moment
      #
      elsif code == 429
        if @@debugMe
          $stderr.puts "Got a 429.  Waiting (#{count})."
        end
        # Sleep for 10 seconds
        sleep 10
        # We don't allow as many retries when we might be throttled
        count += 2
      #
      # 200 - success
      #
      elsif code == 200
        if @@debugMe
          $stderr.puts "Results are complete"
        end
        break
      #
      # We don't know what happened but it can't be good
      #
      else
        abort "Received unexpected response code (#{code}) for GET #{topic}"
      end
    end
    return code, body
  end

  #
  # This is a convenience method that will POST the topic to
  # Primal and then filter the default source of content through the
  # resulting interest network.
  #
  # The response from this method is a bit less clear than using
  # a POST and filter explicitly, since you may not know which
  # one of the two operations has failed (assuming a failure).
  #
  # Returns two values: the response code and the body.  The only
  # successful response code from this method is 200.  If
  # successful then the body contains the JSON payload of the
  # filtered content.
  #
  def postThenFilter(topic, opts = {})
    code, body = postNewTopic(topic)
    if code == 201
      code, body = filterContent(topic, opts)
    end
    return code, body
  end
end

You don’t actually need to understand this code in order to use it. So, if you just want to move forward right now, please do. If you want to learn more about what the script is doing, then consult the comments.

Retrieving Data from Primal

We can now use the PrimalAccess.rb script to retrieve some data from the data service. The following script will interact with Primal to retrieve content from the source you care about, personalized to a specific set of interests.

#!/usr/bin/env ruby
 
# Load in the PrimalAccess class
require './PrimalAccess.rb'
require 'rubygems'
 
# We require this particular gem
#
# To install it:
#   gem install json
#
require 'json'
 
# Prints things out nicely
require 'pp'
 
# Constructs the PrimalAccess object so we can talk to Primal
primal = PrimalAccess.new("<your appId>", "<your appKey>",
                          "<your username>", "<your password>")
 
#
# We've made this a separate function because it's going to be what we're going
# to be modifying most often.  The data service will return to us a JSON payload
# as a regular order of business.  What's going to change is how we manipulate
# that result.  This example does very little.
#
def processJSON(json)
  # "Pretty Print" the JSON response
  pp json
end
 
#
# Call the convenience method that POSTs our topic to Primal and then filters
# the default content against the resulting interest network.
#
code, body = primal.postThenFilter("/travel/adventure")
 
# If successful
if code == 200
  # Convert the payload to JSON
  json = JSON.parse(body)
  # Process the result
  processJSON(json)
else
  puts "Something went wrong #{code} -- #{body}"
end

The PrimalAccess.rb script successfully hides all of the repetitive work of interacting with the data service so that we can focus on the business at hand. In this case, that business is merely printing out the JSON response from Primal, but going forward, all we have to do is change the implementation ofprocessJSON() to change the application.

Content Titles and Links

The JSON response from the previous call is incredibly useful, but to present it to a person, you need to pick out the bits of data that are of important and present them in a reasonable manner. In this example, we’re going to modify the processJSON() function in order to extract the title and resource link of matched content and present them in a more usable format.

#!/usr/bin/env ruby
 
# Load in the PrimalAccess class
require './PrimalAccess.rb'
require 'rubygems'
 
# We require this particular gem
#
# To install it:
#   gem install json
#
require 'json'
 
# Prints things out nicely
require 'pp'
 
# Constructs the PrimalAccess object so we can talk to Primal
primal = PrimalAccess.new("<your appId>", "<your appKey>",
                          "<your username>", "<your password>")
 
#
# Call the convenience method that POSTs our topic to Primal and then filters
# the content against the resulting interest network.
#
code, body = primal.postThenFilter("/travel/adventure")
 
#
# Our changes are here.  All we need to do is grab the dc:collection block from
# the JSON response and transform it to a collection of strings that contain the
# information we care about.
#
def processJSON(json)
  # Grab the array from dc:collection
  collection = json['dc:collection']
  # Convert that array to an array of strings
  data = collection.collect { |dict|
    "title: #{dict['dc:title']}\n" +
    "link: #{dict['dc:relation']}\n\n"
  }
  puts data
end
 
# If successful
if code == 200
  # Convert the payload to JSON
  json = JSON.parse(body)
  # Process the result
  processJSON(json)
else
  puts "Something went wrong #{code} -- #{body}"
end

The script includes everything we had before, for the sake of completeness, but the function on which you want to focus is processJSON().

This simplifies all of the hard work that Primal has done for us and extracts the information from the response that we care about.

Extracting Subject Tags

The dc:collection block is extremely useful — it contains all of the content from your important sources that intersects with your particular set of interests. There is another block of information in the response that contains our interests (represented in SKOS format), and we can use that block of information to enhance our understanding of the content. In this example, we’re going to do exactly that.

Each entry in the dc:collection has an array of identifiers called dc:subject. You use these identifiers to look up elements from the SKOS block to extract more detailed information about what Primal has found.

This example extracts the subject tag names, as well as the title and link as before. It also extracts the content score that Primal has assigned to each piece of content. It transforms the dc:collection entry as we’ve always been doing, extracted more information from it in the process, and also intersected bits of it with the information contained in the SKOS block.

#!/usr/bin/env ruby
 
# Load in the PrimalAccess class
require './PrimalAccess.rb'
require 'rubygems'
 
# We require this particular gem
#
# To install it:
#   gem install json
#
require 'json'
 
# Prints things out nicely
require 'pp'
 
# Constructs the PrimalAccess object so we can talk to Primal
primal = PrimalAccess.new("<your appId>", "<your appKey>",
                          "<your username>", "<your password>")
 
#
# Call the convenience method that POSTs our topic to Primal and then filters
# the content against the resulting interest network.
#
code, body = primal.postThenFilter("/travel/adventure")
 
# 
# Returns an unordered list of the matched topics and their URL identifiers back
# in to Primal.
# 
# dcCollectionEntry - The JSON object pulled from the dc:collection.
# topicsJson - The JSON object represented by
#   skos:ConceptScheme/skos:Collection.
#
# Returns the unordered list of matched topics or the empty string if no topics
# can be found.
# 
def getSubjectTags(dcCollectionEntry, topicsJson)
  # Get the subjects from the dcCollectionEntry
  subjects = dcCollectionEntry['dc:subject']
 
  # If they're defined
  if subjects
    # Convert the subject links to subject labels
    strings = subjects.collect { |subj|
      # Look up the object in the skos block and extract the label
      topicsJson[subj]['skos:prefLabel']
    }
    # Make it look nice
    strings.join(", ")
  else
    ""
  end
end
 
#
# Again, this is where to find the bulk of our changes.  We need to grab the
# dc:collection block as well as the skos:Collection block from the
# skos:ConceptScheme.  These give us enough data to extract the needed bits and
# pieces of each entry in the dc:collection.
#
def processJSON(json)
  # Extract the dc:collection array
  dcCollection = json['dc:collection']
 
  # Extract the skos:Collection dictionary
  skosCollection = json['skos:ConceptScheme']['skos:Collection']
 
  data = dcCollection.collect { |dict|
    # Extract our needed information from the dictionary
    score = dict['primal:contentScore']
    title = dict['dc:title']
    link = dict['dc:relation']
 
    # Generated a list of tag names that matched the content
    tagNames = getSubjectTags(dict, skosCollection)
 
    # Grab the identifier links as well so that we can ask the
    # data service for more information about these subjects
    tagLinks = dict['dc:subject'].collect { |link|
      "        #{link}"
    }.join("\n")
 
    # Create the string to be returned to 'data'
    "#{title}\n" +
    "    Relevancy score: #{score}\n" +
    "    Link to source: #{link}\n" +
    "    Matched topics: #{tagNames}\n" +
    "    Matched topic links:\n#{tagLinks}\n\n"
  }
 
  puts data
end
 
# If successful
if code == 200
  # Convert the payload to JSON
  json = JSON.parse(body)
  # Process the result
  processJSON(json)
else
  puts "Something went wrong #{code} -- #{body}"
end

Above we see what the code is doing. The identifiers in the dc:subject are being mapped directly to entries in the skos:collection, from which we can pull the skos:prefLabel element for display purposes.

Here’s what you’ve just gotten Primal to do for you, though the data service:

You’ve managed to have Primal filter the content you care about through your topics of interest and produce a graph of information represented as the JSON you’ve been processing.

Creating a Network with Many Topics

We’ve created interest graphss around single topics to this point. Primal expands our topics and creates more robust interest graphss for us automatically but what if we want to get an even richer network using more input? We can do this using a number of POST calls and have the data service expand our network for us.

#!/usr/bin/env ruby
 
# Load in the PrimalAccess class
require './PrimalAccess.rb'
require 'rubygems'
 
# We require this particular gem
#
# To install it:
#   gem install json
#
require 'json'
 
# Prints things out nicely
require 'pp'

# Constructs the PrimalAccess object so we can talk to Primal
primal = PrimalAccess.new("<your appId>", "<your appKey>",
                          "<your username>", "<your password>")
 
#
# We're not going to do anything special here.  You know how to manipulate the
# results to get some bits and pieces of information that are important to you,
# so let's just fly past this right now.
#
def processJSON(json)
  # "Pretty Print" the JSON response
  pp json
end
 
#
# Here we're going to be creating a large graph of interests that descend from
# 'travel'.  The graph of interests that Primal is going to create will be
# extreme indeed!
#
interests = [
    '/travel/canada/north/adventure/skiing;hiking;climbing;sledding',
    '/travel/norway/kayak;iceberg;paddle',
    '/travel/adventure/alaska/hunting;camping;survival',
    '/travel/adventure/arctic/igloo;ice+fishing;survival;snow+mobile',
    '/travel/extreme/winter/mountain+climbing;death'
]

# 
# Now we're going to use the head topic of our interest network for filtering
# purposes.  The graph Primal has created is going to used to forumlate the
# terms we use for filtering.
#
interestForFiltering = "/travel"
 
#
# Create interests around all of our topics, each in turn
#
interests.each { |topic|
    puts "Creating interests around #{topic}..."
    code, body = primal.postNewTopic(topic)
    if code != 201
        abort "Unable to create interests around #{topic}.\n" +
              "Error #{code}, message: \"#{body}\""
    end
}

#
# Now that the interests have been created, lets use them to grab
# some content that intersects with them
#
puts "Filtering content..."
code, body = primal.filterContent(interestForFiltering)

# If successful
if code == 200
  # Convert the payload to JSON
  json = JSON.parse(body)
  # Process the result
  processJSON(json)
else
  puts "Something went wrong #{code} -- #{body}"
end

Filtering Results by Content Type

Each entry in the dc:collection contains an element called dc:type that specifies the type of content as defined by schema.org (not all types are being used). We can manipulate the JSON response to select only those elements whose category matches what we want. In the case below, we’ll accept only those elements whose dc:type is WebPage.

#!/usr/bin/env ruby
 
# Load in the PrimalAccess class
require './PrimalAccess.rb'
require 'rubygems'
 
# We require this particular gem
#
# To install it:
#   gem install json
#
require 'json'
 
# Prints things out nicely
require 'pp'
 
# Constructs the PrimalAccess object so we can talk to Primal
primal = PrimalAccess.new("<your appId>", "<your appKey>",
                          "<your username>", "<your password>")
 
#
# Here's the important bit.  We're going to parse the JSON response and accept
# only those entries in the dc:collection whose dc:type value is "WebPage"
#
def processJSON(json)
  collection = json['dc:collection']
  # Select only those entries for which the dc:type is "WebPage" and then transform
  # them into strings that look pretty
  results = collection.select { |dict|
    dict['dc:type'] == "WebPage"
  }.collect { |dict|
    "title: #{dict['dc:title']}\n" +
    "link: #{dict['dc:relation']}\n" +
    "source: #{dict['dc:type']}\n\n"
  }.each { |result|
    puts result
  }
end
 
#
# Call the convenience method that POSTs our topic to Primal and then filters
# the content against the resulting interest network.
#
code, body = primal.postThenFilter("/travel/adventure")
 
# If successful
if code == 200
  # Convert the payload to JSON
  json = JSON.parse(body)
  # Process the result
  processJSON(json)
else
  puts "Something went wrong #{code} -- #{body}"
end

Filtering Content by a Scoring Threshold

Primal calculates a score for each topic that tells you how relevant it is to the request that you made. You can use this score to filter content so that the data service only returns items with a minimum score threshold. If we add primal:contentScore:min=n (where n is a floating point value between 0 and 1), the data service will not return any content that has a score below n.

#!/usr/bin/env ruby
 
# Load in the PrimalAccess class
require './PrimalAccess.rb'
require 'rubygems'
 
# We require this particular gem
#
# To install it:
#   gem install json
#
require 'json'
 
# Prints things out nicely
require 'pp'
 
# Constructs the PrimalAccess object so we can talk to Primal
primal = PrimalAccess.new("<your appId>", "<your appKey>",
                          "<your username>", "<your password>")
 
#
# We're going to use the special last parameter in the filterContent call that
# allows us to specify parameters that will go into the query of the GET call.
# By adding 'primal:contentScore:min=0.7' in the query parameter of the URL, we direct the data
# service to return content to us that scores 0.7 or better.  The resulting URL
# will look something like this:
#
#    https://data.primal.com/travel/adventure?primal:contentScore:min=0.7
#
code, body = primal.filterContent("/travel/adventure", {
                                    :"primal:contentScore:min" => 0.7
                                  })
 
#
# We use the processJSON() function as we have in other examples to process the
# results from the GET.  This just extracts what we need and displays it without
# all of the extra noise from the JSON response.
#
def processJSON(json)
  # Let's just print out titles, links and scores
  json['dc:collection'].collect { |dict|
    "title: #{dict['dc:title']}\n" +
    "link: #{dict['dc:relation']}\n" +
    "score: #{dict['primal:contentScore']}\n\n"
  }.each { |result|
    puts result
  }
end
 
# If successful
if code == 200
  # Convert the payload to JSON
  json = JSON.parse(body)
  # Process the result
  processJSON(json)
else
  puts "Something went wrong #{code} -- #{body}"
end

Limiting the Number of Content Results

Along with filtering by a scoring threshold, you can also specify the maximum number of content items you want the data service to return. When you add primal:contentCount:max=n (where n is an integer greater than 0) the data service will ensure that the dc:collection does not contain more than n items (ordered by decreasing value of primal:contentScore).

#!/usr/bin/env ruby
 
# Load in the PrimalAccess class
require './PrimalAccess.rb'
require 'rubygems'
 
# We require this particular gem
#
# To install it:
#   gem install json
#
require 'json'
 
# Prints things out nicely
require 'pp'
 
# Constructs the PrimalAccess object so we can talk to Primal
primal = PrimalAccess.new("<your appId>", "<your appKey>",
                          "<your username>", "<your password>")
 
#
# We're going to use the special last parameter in the filterContent call that
# allows us to specify parameters that will go into the query of the GET call.
# By adding 'primal:contentCount:max=20' in the query parameter of the URL, we direct
# the data service to return no more than 20 items to us
#
#   https://data.primal.com/travel/adventure?primal:contentCount:max=20
#
code, body = primal.filterContent("/travel/adventure", {
                                    :"primal:contentCount:max" => 20
                                  })
 
#
# We've made this a separate function because it's going to be
# what we're going to be modifying most often.  The data service
# will return to us, a JSON payload as a regular order of 
# business.  What's going to change is how we manipulate that
# result.  For now, we're starting small
#
def processJSON(json)
  count = 0
  # Let's just print out titles, links, scores and index number
  json['dc:collection'].collect { |dict|
    count += 1
    "index: #{count}\n" +
    "title: #{dict['dc:title']}\n" +
    "link: #{dict['dc:relation']}\n" +
    "score: #{dict['primal:contentScore']}\n\n"
  }.each { |result|
    puts result
  }
end
 
# If successful
if code == 200
  # Convert the payload to JSON
  json = JSON.parse(body)
  # Process the result
  processJSON(json)
else
  puts "Something went wrong #{code} -- #{body}"
end

An Interactive Application

The response from the data service provides a lot of connected information, and many of those connections lead right back to Primal. In fact, the whole idea of the data service is that it’s an infinite, open collection of data that’s linked up dynamically and changes over time. It’s really simple to see how this would make Primal a perfect service to be used interactively, and we’re going to explore that in this example. How your app works with Primal’s data service may not be this explicit, but the repeated feedback and interaction with Primal should be part of how your app works.

#!/usr/bin/env ruby
 
# Load in the PrimalAccess class
require './PrimalAccess.rb'
require 'rubygems'
 
# We require this particular gem
#
# To install it:
#   gem install json
#
require 'json'
 
# Constructs the PrimalAccess object so we can talk to Primal
$primal = PrimalAccess.new("<your appId>", "<your appKey>",
                          "<your username>", "<your password>")
 
# 
# Returns an unordered list of the matched topics and their URL identifiers back
# in to Primal.
# 
# dcCollectionEntry - The JSON object pulled from the dc:collection.
# skosCollection - The JSON object represented by
#   skos:ConceptScheme/skos:Collection.
# Returns a list dictionaries with :subject and :prefLabel as entries.
# 
def getSubjectTags(dcCollectionEntry, skosCollection)
  # Get the subjects from the dcCollectionEntry
  subjects = dcCollectionEntry['dc:subject']
 
  # If they're defined
  if subjects
    # Convert the subject links to subject labels
    strings = subjects.collect { |subj|
      # Look up the object in the skos block and extract the label
      {
        :subject => subj,
        :prefLabel => skosCollection[subj]['skos:prefLabel']
      }
    }
  else
    []
  end
end
 
#
# We're looking to process the JSON response into a new data structure that can
# be used to present information to the user.
#
def processJSON(json)
  # Grab the array from dc:collection
  dcCollection = json['dc:collection']
  # Grab the skos block
  skosCollection = json['skos:ConceptScheme']['skos:Collection']
  # Convert the JSON into an array of dictionaries that we can present to
  # the user
  dcCollection.collect { |dict|
    {
      :score => dict['primal:contentScore'],
      :title => dict['dc:title'],
      :link => dict['dc:relation'],
      :subjects => getSubjectTags(dict, skosCollection)
    }
  }.reverse # orders it by reverse score so that the best stuff is at the
            # bottom. It'll look better for the user so that they don't have to
            # scroll up
end
 
#
# We first create an interest network around a given topic and then filter the
# the given source of content through that resulting network in this one
# function.  The returned data is exactly what was returned from processJSON()
#
def postAndFilter(topic)
  print "Creating interests around #{topic}..."
  STDOUT.flush
  code, body = $primal.postNewTopic(topic)
  # If successful
  if code == 201
    puts " success."
    print "Filtering content against #{topic}..."
    STDOUT.flush
    code, body = $primal.filterContent(topic)
    if code == 200
      puts " success."
      # Convert the payload to JSON
      json = JSON.parse(body)
      # Process the result
      processJSON(json)
    else
      abort "Filtering request failed (#{code}). Message: #{body}"
    end
  else
    abort "Creation request failed (#{code}). Message: #{body}"
  end
end

#
# Display the subjects within an entry of the dc:collection block to the user
#
def printSubjects(subjects)
  subjects.each_index { |j|
    puts "    #{j}) #{subjects[j][:prefLabel]}"
  }
end

#
# Displays the JSON results to the user in a way that they can then select from
# them later
#
def printResults(data)
  puts "\n\n"
  puts "======================================="
  puts "\n"
  data.each_index { |i|
    puts "#{i})"
    puts "  score: #{data[i][:score]}"
    puts "  title: #{data[i][:title]}"
    puts "  link:  #{data[i][:link]}"
    puts "  subjects:"
    printSubjects(data[i][:subjects])
  }
end

#
# Asks the user to enter a number, checks to ensure that they've done so
# properly and returns that result to the caller
#
def getUserIndex(message, max)
  while true
    puts "\n#{message} ('q' quits)"
    idx = gets().chomp()
    if idx =~ %r{[qQ]}
      puts "See ya!"
      exit(0)
    elsif idx =~ %r{\s*\d+\s*} && idx.to_i >= 0 && idx.to_i < max
      return idx.to_i
    else
      puts "\nI don't know what '#{idx}' is, but it's not a valid index\n\n"
    end
  end
end

# ====================
# Main
# ====================

# Start the ball rolling with an initial set of topics
puts "Give me a topic (in Primal hierarchical form - e.g. /adventure/hiking;france):"
topic = gets().chomp()

# Start the loop
while true
  puts "Alright, let's do it..."

  # Get the filtered content from Primal
  data = postAndFilter(topic)

  # Show it to the user
  printResults(data)

  # Ask them which piece of content to look at
  idx = getUserIndex("Which content number do you want to look into?", data.length)

  # Get the subject to feed back into primal
  subjects = data[idx][:subjects]
  printSubjects(subjects)
  idx = getUserIndex("Which subject do you want to look into?", subjects.length)

  # Get the topic and the source and get ready to do it again!
  topic = subjects[idx][:subject].gsub(%r{^https://.*?/}, '/')
end

Before we start tearing it apart, let’s see some interaction with it:

Give me a topic (in Primal hierarchical form - e.g. /adventure/hiking;france):
/technology/mobile;smartphone;web+browsing
 
Alright, let's do it...
Creating interests around /technology/mobile;smartphone;web+browsing... success.
Filtering content against /technology/mobile;smartphone;web+browsing... success.
 
=======================================
 
0)
  score: 0.543
  title: The Smartphone Market 2010-2015 - Market Research
  link:  https://data.primal.co ... tphone-Market-2010-2015%2FRPT643712
  subjects:
    0) technology company
    1) mobile application
    2) mobile network
    3) smartphone
 
### ... Lots more results ... ###
 
80)
  score: 0.998
  title: Wireless News, Mobile Devices and Wireless Facts | AT&T
  link:  https://data.primal.co ... com%2Fgen%2Fpress-room%3Fpid%3D1841
  subjects:
    0) mobile computers
    1) html
    2) mobile device
    3) phone
    4) video
    5) web browsing
    6) smartphone
    7) mobile
    8) technology
 
 
Which content number do you want to look into? ('q' quits)
80   
    0) mobile computers
    1) html
    2) mobile device
    3) phone
    4) video
    5) web browsing
    6) smartphone
    7) mobile
    8) technology
 
Which subject do you want to look into? ('q' quits)
0
 
Alright, let's do it...
Creating interests around /technology/mobile/mobile+computers... success.
Filtering content against /technology/mobile/mobile+computers... success.
 
### ... and so on ... ###

Basically you enter some topics and Primal does the rest. Once the results come back from the data service, they get formatted in a manner that’s decent for our simple text-based user interface and the user does their thing.

What’s important here is what happens at the end of this interaction example. The user is presented with a question:

Which content number do you want to look into? ('q' quits)

This lets them choose one of the pieces of content that the data service has returned. There’s more to the content that Primal’s given us than just a URL; this piece of content is linked to other topics that we can further investigate with Primal. Once the user has selected a piece of content, they are presented with the subjects that intersected with that content:

Which subject do you want to look into? ('q' quits)

So, they now choose the subject that they’re interested in here, and we feed that back into the data service just like we fed in the initial topic at the beginning of the application. The user could do this forever and Primal would never tire!

We saw part of this before when looking into Extracting Subject Tags but didn’t go this deep. In that example all we wanted to do extract them and show them, but now we’re using them to close the loop. From an engineering perspective, this app isn’t all that exciting since it’s just finishing off what we started in a much earlier example, but really… that’s the point. Most of the code here is all about interaction with the user — how we show them stuff, how they talk to us, and so forth — the work to interact with the data service is extremely simple.