<?xml version="1.0" encoding="UTF-8"?><rss version="2.0"
	xmlns:content="http://purl.org/rss/1.0/modules/content/"
	xmlns:wfw="http://wellformedweb.org/CommentAPI/"
	xmlns:dc="http://purl.org/dc/elements/1.1/"
	xmlns:atom="http://www.w3.org/2005/Atom"
	xmlns:sy="http://purl.org/rss/1.0/modules/syndication/"
	xmlns:slash="http://purl.org/rss/1.0/modules/slash/"
	>

<channel>
	<title>Directed Edge News</title>
	<atom:link href="https://blog.directededge.com/feed/" rel="self" type="application/rss+xml" />
	<link>https://blog.directededge.com</link>
	<description></description>
	<lastBuildDate>Wed, 07 Aug 2013 09:57:35 +0000</lastBuildDate>
	<language>en-US</language>
	<sy:updatePeriod>
	hourly	</sy:updatePeriod>
	<sy:updateFrequency>
	1	</sy:updateFrequency>
	<generator>https://wordpress.org/?v=6.2.6</generator>
	<item>
		<title>Support screenshots with Monosnap and S3</title>
		<link>https://blog.directededge.com/2013/08/06/support-screenshots-with-monosnap-and-s3/</link>
					<comments>https://blog.directededge.com/2013/08/06/support-screenshots-with-monosnap-and-s3/#comments</comments>
		
		<dc:creator><![CDATA[Scott Wheeler]]></dc:creator>
		<pubDate>Wed, 07 Aug 2013 01:35:55 +0000</pubDate>
				<category><![CDATA[Uncategorized]]></category>
		<guid isPermaLink="false">http://blog.directededge.com/?p=815</guid>

					<description><![CDATA[I remember the first day that I discovered Skitch. Skitch let you take screenshots of an application or region on your screen and quickly post those online with a handy link that you could show to others. You could draw arrows or other simple annotations before flipping the switch to send the image to the [&#8230;]]]></description>
										<content:encoded><![CDATA[<p>I remember the first day that I discovered <a href="https://evernote.com/skitch/">Skitch</a>. Skitch let you take screenshots of an application or region on your screen and quickly post those online with a handy link that you could show to others. You could draw arrows or other simple annotations before flipping the switch to send the image to the interwebs.</p>
<p>Since Directed Edge moved from being an API- and developer-only company into also supporting <a href="https://shopify.directededge.com/">e-commerce platforms</a>, taking screenshots to help out our customers is a regular part of our customer support process. Sometimes a screenshot is worth a thousand (well, or at least several dozen) words.</p>
<p>Skitch was eventually bought by <a href="https://evernote.com/">Evernote</a>, and as an Evernote user I appreciate the Skitch integration, but as the interface has gradually gotten clunkier for the good old taking of support screenshots, and since we&#8217;ve come to lean heavily on them, I&#8217;d been on the lookout for a more elegant solution.</p>
<p><strong>Hello, Monosnap.</strong></p>
<p>I then stumbled across <strong><a href="https://www.monosnap.com/welcome">Monosnap</a></strong>, which has free integration with <strong>Amazon&#8217;s <a href="https://aws.amazon.com/s3/">S3</a></strong>. I thought I&#8217;d give a quick run down of how we&#8217;ve moved from Skitch to using Monosnap + S3 to serve up &#8220;<tt>show.directededge.com</tt>&#8221;</p>
<p><img decoding="async" loading="lazy" class="aligncenter size-full wp-image-819" alt="Monosnap" src="https://blog.directededge.com/wp-content/uploads/2013/08/monosnap.png" width="700" height="513" />Monosnap itself is pretty straightforward. You take screenshots, potentially annotate them and it uploads them somewhere. Â It hearkens to the Skitch of old. Â However, instead of only uploading images to its own server, it also allows you to upload images to various cloud services.</p>
<p><img decoding="async" loading="lazy" class="aligncenter size-full wp-image-821" alt="Monosnap Config - Account" src="https://blog.directededge.com/wp-content/uploads/2013/08/monosnap-config-account.png" width="646" height="562" /></p>
<p>We were primarily interested in S3, since we&#8217;re using it for image storage in another recommendations product that we have coming down the pipeline.</p>
<p>So, first we created a new S3 bucket. Â We typically just name our buckets after the site that they&#8217;ll be backing:</p>
<p><img decoding="async" loading="lazy" class="aligncenter size-full wp-image-824" alt="Monosnap s3 Bucket" src="https://blog.directededge.com/wp-content/uploads/2013/08/monosnap-s3-bucket.png" width="700" height="439" /></p>
<p>The rest of the default bucket settings are fine.</p>
<p><strong>The &#8220;show&#8221;.</strong></p>
<p>Now, with s3 you can typically access your buckets at a URL like:</p>
<p><tt>https://s3.amazonaws.com/show.directededge.com</tt></p>
<p>However, you can also rearrange that URL to a DNS CNAME friendly format:</p>
<p><tt>http://show.directededge.com.s3.amazonaws.com/</tt></p>
<p>So, now we need to head over to our DNS provider (Rackspace in our case) and enter in an appriate CNAME record, to get those lovely, Directed Edge-looking, URLs.</p>
<p><img decoding="async" loading="lazy" class="aligncenter size-full wp-image-827" alt="s3 CNAME" src="https://blog.directededge.com/wp-content/uploads/2013/08/monostrap-s3-cname.png" width="700" height="439" /></p>
<p>As is usual with CNAME records, we simply had to enter a record pointing from:</p>
<p><tt>show.directededge.com</tt></p>
<p>To:</p>
<p><tt>show.directededge.com.s3.amazonaws.com.</tt></p>
<p>(Note the period on the end, standard with CNAME records.)</p>
<p>With that the DNS and storage bits are done.</p>
<p><strong>Configuring Monosnap</strong></p>
<p>From there we just needed to add a couple values to Monosnap. Â As seen in the configuration interface above, we had to enter our AWS API key and access token. Â Unfortunately, creating a special account for Amazon&#8217;s <a href="https://aws.amazon.com/iam/">Identity and Access Management</a> (IAM) didn&#8217;t seem to get along well with Monosnap, so we were forced to use our master token. Â Were we using S3 for more than image storage, this would probably be somewhat troubling.</p>
<p>However, once we entered our master credentials, we were able to select a bucket and begin uploading support screenshots to our new, pretty URL. Â For example:</p>
<p style="text-align: center;"><a href="http://show.directededge.com/Directed_Edge_for_Shopify_20130807_012624.png"><img decoding="async" loading="lazy" class="aligncenter size-full wp-image-829" alt="Directed Edge for Shopify" src="https://blog.directededge.com/wp-content/uploads/2013/08/Directed_Edge_for_Shopify_20130807_012624.png" width="700" height="439" /></a><br />
<a href="http://show.directededge.com/Directed_Edge_for_Shopify_20130807_012624.png"><tt>http://show.directededge.com/Directed_Edge_for_Shopify_20130807_012624.png</tt></a></p>
]]></content:encoded>
					
					<wfw:commentRss>https://blog.directededge.com/2013/08/06/support-screenshots-with-monosnap-and-s3/feed/</wfw:commentRss>
			<slash:comments>3</slash:comments>
		
		
			</item>
		<item>
		<title>Terror in recommendations-ville</title>
		<link>https://blog.directededge.com/2012/10/29/terror-in-recommendations-ville/</link>
					<comments>https://blog.directededge.com/2012/10/29/terror-in-recommendations-ville/#respond</comments>
		
		<dc:creator><![CDATA[Scott Wheeler]]></dc:creator>
		<pubDate>Mon, 29 Oct 2012 21:01:33 +0000</pubDate>
				<category><![CDATA[Uncategorized]]></category>
		<guid isPermaLink="false">http://blog.directededge.com/?p=785</guid>

					<description><![CDATA[UPDATE: Now, 10 months later, we&#8217;ve not had a single additional instance of data corruption, which is a spot we&#8217;re glad to be back in. Every startup seems to have one of those technical meltdown stories. Up until today, our worst had been back in 2009 when we were still in beta, and it was [&#8230;]]]></description>
										<content:encoded><![CDATA[<p><strong>UPDATE:</strong> Now, 10 months later, we&#8217;ve not had a single additional instance of data corruption, which is a spot we&#8217;re glad to be back in.</p>
<hr />
<p>Every startup seems to have one of those technical meltdown stories. Up until today, our worst had been back in 2009 when we were still in beta, and it was more embarassing than critical. (Lesson learned then: never deploy just before sleeping.)</p>
<p>Since then we&#8217;ve done pretty well. On average we have about 4-8 hours of downtime per year. So today was especially nasty.</p>
<h3 style="padding-bottom: 1em;">The first sign of trouble</h3>
<p>Around 11:01 p.m. GMT I get a mail about an unexpected reboot. One of our other guys had caught an issue and was looking into it, but I hadn&#8217;t noticed the SMS he&#8217;d sent me saying such was coming up.</p>
<p>We hop on Skype and start going through what&#8217;s going on. Our web services process was alternating between crashing and hanging every few minutes. Usually that means some corrupted indexes in one of our databases. We get that once or twice a year. It&#8217;s an annoyance (and let me tell you about the fun of trying to track down a bug that happens twice a year), but we have a script that can rebuild the indexes, depending on the size of the database, in somewhere between 10 seconds and 10 minutes.</p>
<h3 style="padding-bottom: 1em;">A Heisenbug</h3>
<p>The trick is figuring out which database is causing the problem. Aside from our enterprise customers, which are usually on their own hardware, we run a bunch of our small and medium sized databases off of the same hardware. Our web services setup is heavily multithreaded, so there are usually some 40-ish threads that are writing to the logs in parallel; figuring out which request was the one triggering the problem can sometimes be a bit tricky.</p>
<p>But we&#8217;ve been there before too. We have a switch we can use to flip on function tracing which usuall makes it clear pretty quickly &#8212; or at least has in every instance up until now.</p>
<p>This time it didn&#8217;t. We kept watching our logs, trying to catch the offending item and guessing the wrong one. We have a script to pull down databases from the production to our development machines, where we check the indexes for corruption. For about an hour we kept missing.</p>
<p><strong>We hadn&#8217;t been offline for a full hour in more than a year</strong>, so we were getting antsy at that point. We&#8217;d thought about this situation before too though &#8212; what if we couldn&#8217;t find the offending database?</p>
<h3 style="padding-bottom: 1em;">A not-so-super super-tool</h3>
<p>The last time we had a similar issue (which was sorted out in under 15 minutes), I wrote a small program to go through and check every single database on a system. It was, as we soon will learn, not so aptly called, &#8220;IntegrityChecker&#8221;. We&#8217;d never run it in production, but it had been tested on dev machines. Basically it would disable an account for a few seconds, try to get a file lock on its files, and then run the integrity checker that we usually use offline to test such things.</p>
<p>Except that for some reason it was given a file lock when the OS should have rejected it. So, now we have mmap&#8217;ed files that are being written from two process. Not good. In fact, very, very bad.</p>
<p>Unfortunately, we didn&#8217;t notice this immediately. In fact, things looked great &#8212; it identified the database with a corrupt index, we rebuilt it, waited 15 minutes to make sure everything was kosher, and headed off to snooze. It was about 4:00 a.m. local time.</p>
<h3 style="padding-bottom: 1em;">This isn&#8217;t supposed to happen</h3>
<p><strong>We have two redundant monitoring systems</strong> that live in a separate network and check all of our servers once per minute. Both of them send out a barage of SMSes if the servers stop responding to requests. But most of the API requests that were coming in were actually being handled &#8212; probably 80% or so &#8212; and our monitoring systems retry a second time if there&#8217;s a failure just so that we don&#8217;t get notified every time there&#8217;s a minor network glitch somewhere. So we weren&#8217;t notified that our servers were still sickly.</p>
<h3 style="padding-bottom: 1em;">The score</h3>
<p>Now, if you&#8217;re keeping score, here are the points so far of this perfect storm:</p>
<ul>
<li>Relatively minor issue of database index corruption</li>
<li>IntegrityChecker that&#8217;s actually corrupting databases (and not just their indexes)</li>
<li>Monitoring not catching the problems</li>
</ul>
<h3 style="padding-bottom: 1em;">Digging deeper</h3>
<p>9:30 a.m. rolls around and we&#8217;re up and trying to see if there&#8217;s anything we can piece together about the issues from the night before. Pretty soon one of the other guys that&#8217;s working with me on the problem notices that the server process actually kept crashing all night, albeit at a somewhat reduced frequency (every 10 minutes or so).</p>
<p>Since the IntegrityChecker had worked so well the night before (as we still thought), I gave it another go. It turned up this time six corrupt databases. We&#8217;d never seen that. I rebuilt them, and again, in perfect storm fashion, ran it again. Two more. Something was obviously very wrong.</p>
<p>A little while later the database process is back to restarting every minute. Panic ensues.</p>
<h3 style="padding-bottom: 1em;">Our monitoring turns on itself</h3>
<p>See, we not only have out-of-band monitoring, <strong>we also have monit running to restart things on our machines if something goes wrong</strong>.</p>
<p>We didn&#8217;t notice for about an hour that the errors in the monit log weren&#8217;t the usual ones. They were 404 &#8212; &#8220;not found&#8221; &#8212; errors. That was weird. It turned out the account that we use for status checking was corrupt. We fixed that and were back down to crashes every 10 minutes or so, but still had no idea what could possibly be causing the problems.</p>
<p>A couple of heavily caffinated hours of shooting in the dark passed. Eventually we started to suspect the IntegrityChecker. And we noticed that a few accounts (about a dozen &#8212; the ones that had the misfortune of writing while the IntegrityChecker was checking them) had been effectively nulled out. We started getting a sense of how widespread the damage was.</p>
<h3 style="padding-bottom: 1em;">The low-down</h3>
<p>There was a lot that came together to make this as bad as it was. To recap, we started off with a relatively minor issue, which was made into a major issue by a tool that was built specifically for dealing with the exact situation we were facing, and those large issues were not caught by our generally quite good monitoring, which eventually even became part of the problem itself.</p>
<h3 style="padding-bottom: 1em;">The last resort: the backups</h3>
<p>Fortunately we have backups, so we started going through accounts manually, offline this time, checking almost every database to try to find corruption. We found a few more.</p>
<p>At that point we started restoring the broken databases to their last known good state &#8212; about 17 hours before all of the mess started. We&#8217;ve done that now and things look pretty stable. It&#8217;s now been an 90 minutes since we had a crash or hang and all accounts are back online. In some cases we&#8217;re trying to merge data that came in later in the day where we can get to it, but that&#8217;s a bit spotty.</p>
<h3 style="padding-bottom: 1em;">An apology</h3>
<p><strong>This is the first time in three and a half years of being in production that we lost data and we feel terrible about it</strong>. We&#8217;ll be doing some work on our internal systems to help us monitor, diagnose and fix these issues in the future. Thanks so much for your patience.</p>
<p>Edit: One additional note: if you&#8217;re one of our customers that uses our Shopify app and your data seems to be out of date, it&#8217;ll automatically be updated the next time that we pull your shop&#8217;s data in with our importer. There was no data loss for Shopify customers.</p>
]]></content:encoded>
					
					<wfw:commentRss>https://blog.directededge.com/2012/10/29/terror-in-recommendations-ville/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
		<item>
		<title>Bindings update that removes extra server round-trips</title>
		<link>https://blog.directededge.com/2012/09/27/bindings-update-that-removes-extra-server-round-trips/</link>
					<comments>https://blog.directededge.com/2012/09/27/bindings-update-that-removes-extra-server-round-trips/#respond</comments>
		
		<dc:creator><![CDATA[Scott Wheeler]]></dc:creator>
		<pubDate>Fri, 28 Sep 2012 06:21:55 +0000</pubDate>
				<category><![CDATA[Uncategorized]]></category>
		<guid isPermaLink="false">http://blog.directededge.com/?p=780</guid>

					<description><![CDATA[So, we noticed recently that our Java and Python bindings were doing an extra round-trip to our servers for every request. We&#8217;ve just done updates to each of them that removes this. Per the HTTP spec, when a request uses HTTP BASIC authentication, as our API does, the client is supposed to first send a [&#8230;]]]></description>
										<content:encoded><![CDATA[<p>So, we noticed recently that our Java and Python bindings were doing an extra round-trip to our servers for every request.  We&#8217;ve just done <a href="https://github.com/directededge/directed-edge-bindings">updates</a> to each of them that removes this.</p>
<p>Per the HTTP spec, when a request uses HTTP BASIC authentication, as our API does, the client is supposed to first send a request without credentials.  When it receives a 401 error, then it should retry with the credentials supplied and repeat that process for each and every request.</p>
<p>While that makes sense for interactive use from a browser, for use in a web services API, it makes much more sense to avoid the extra round-trip and send the credentials already in the first request, thus significantly speeding up request times.</p>
<p>You can <a href="https://github.com/directededge/directed-edge-bindings">get the updated versions of our bindings on Github</a>.</p>
]]></content:encoded>
					
					<wfw:commentRss>https://blog.directededge.com/2012/09/27/bindings-update-that-removes-extra-server-round-trips/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
		<item>
		<title>Shopify app updates, new Ruby bindings beta, new web services features coming down the pipe</title>
		<link>https://blog.directededge.com/2012/09/18/shopify-app-updates-new-ruby-bindings-beta-new-web-services-features-coming-down-the-pipe/</link>
					<comments>https://blog.directededge.com/2012/09/18/shopify-app-updates-new-ruby-bindings-beta-new-web-services-features-coming-down-the-pipe/#respond</comments>
		
		<dc:creator><![CDATA[Scott Wheeler]]></dc:creator>
		<pubDate>Wed, 19 Sep 2012 03:12:11 +0000</pubDate>
				<category><![CDATA[Uncategorized]]></category>
		<guid isPermaLink="false">http://blog.directededge.com/?p=763</guid>

					<description><![CDATA[Shopify app updates So, it&#8217;s been a gazillion years since we posted updates here, but there have been a number of things shaking out of the woodwork. First, we just did the biggest update to our Shopify app since we first launched it a couple years back. Â It features a new Bootstrap-ified configuration interface and [&#8230;]]]></description>
										<content:encoded><![CDATA[<h4 style="padding-bottom: 1em">Shopify app updates</h4>
<p>So, it&#8217;s been a gazillion years since we posted updates here, but there have been a number of things shaking out of the woodwork.</p>
<p>First, we just did the biggest update to <a href="http://apps.shopify.com/directed-edge-product-recommender">our Shopify app</a> since we first launched it a couple years back. Â It features a new <a href="http://twitter.github.com/bootstrap/">Bootstrap</a>-ified configuration interface and a whole bunch of new recommendations types.</p>
<p><img decoding="async" loading="lazy" class="aligncenter size-full wp-image-764" title="Product Settings" src="https://blog.directededge.com/wp-content/uploads/2012/09/product-settings-blog.png" alt="" width="700" height="590" /></p>
<p><img decoding="async" loading="lazy" class="aligncenter size-full wp-image-765" title="Recommendations Types" src="https://blog.directededge.com/wp-content/uploads/2012/09/recommendations-blog.png" alt="" width="700" height="603" /></p>
<h4 style="padding-bottom: 1em">New Ruby Bindings in Beta</strong></h4>
<p>Whilst working on our Shopify app, which itself is a Rails app, we got frustrated with the current state of our Ruby bindings.</p>
<p>You see, our Ruby bindings were written back in the dark days before we actually used Ruby at all inside of the company. Â Truth be told, the original version was written by yours truly a couple hours after I started learning Ruby.</p>
<p>These days, along with Java and C++ (which our lower-level, performance critical bits are written in), we write quite a bit of Ruby as as such, our tastes have become more refined.</p>
<p>Here are a couple of quick examples:</p>
<p><strong>Old:</strong></p>
<pre lang="ruby">require 'rubygems'
require 'directed_edge'

DE_USER = ENV['DIRECTEDEDGE_TEST_DB']
DE_PASS = ENV['DIRECTEDEDGE_TEST_PASS']

# Import some data

DE_FILE = 'temp.xml'

exporter = DirectedEdge::Exporter.new(DE_FILE)

(1..10).each do |i|
  item = DirectedEdge::Item.new(exporter.database, "item_#{i}")
  item.add_tag('item')
  item.link_to("item_#{rand(10) + 1}")
  exporter.export(item)
end

exporter.finish

database = DirectedEdge::Database.new(DE_USER, DE_PASS)
database.import(DE_FILE)

# Get recommendations

item = DirectedEdge::Item.new(database, 'item_1')
item.related([ 'item' ]).each { |i| puts i }

# Update item

item.add_tag('foo')
item.save</pre>
<p><strong>New:</strong></p>
<pre lang="ruby">require 'rubygems'
require 'directededge'

DE_USER = ENV['DIRECTEDEDGE_TEST_DB']
DE_PASS = ENV['DIRECTEDEDGE_TEST_PASS']

database = DirectedEdge::Database.new(DE_USER, DE_PASS)

DirectedEdge::UpdateJob.run(database, :replace) do |update|
  (1..10).each do |i|
    update.item("item_#{i}") do |item|
      item.tags.add('item')
      item.links.add("item_#{rand(10) + 1}")
    end
  end
end

# Get recommendations

item = DirectedEdge::Item.new(database, 'item_1')
item.related(:tags => :item).each { |i| puts i }

# Update item

item.tags.add('foo')
item.save</pre>
<p>The changes are most obvious in the importer above, but there are a lot of little subtle improvements in the API&#8217;s semantics.</p>
<p>If you&#8217;re up for beta testing the new bindings, <a href="mailto:info@directededge.com">drop us a line</a>. Â The documentation is sorely lacking at present (ahem, meaning non-existent), but that&#8217;s the main thing that we know of that&#8217;s missing at the moment: Â the new bindings are already in use by our Shopify app and the <a href="https://github.com/directededge/directed-edge-bindings/tree/v1/Ruby">code is up on Github</a>.</p>
<h4 style="padding-bottom: 1em">Web services updates on the way</h4>
<p>In a similar vein, we&#8217;ve got some stuff that we added to the web services for the Shopify app that still have some rough corners that we want to smooth out before pushing them out to the unwashed masses, but we&#8217;ll soon have explicit API support for tracking product view histories and showing recommendations based on them. Â It&#8217;s been possible to do such in a hackey way in the past, but the new stuff adds some special math-fu for time-clustered recommendations and storing high-traffic queues of updates. Â More on that soon.</p>
]]></content:encoded>
					
					<wfw:commentRss>https://blog.directededge.com/2012/09/18/shopify-app-updates-new-ruby-bindings-beta-new-web-services-features-coming-down-the-pipe/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
		<item>
		<title>The easiest way to add recommendations to your Rails app, announcing acts_as_edgy</title>
		<link>https://blog.directededge.com/2011/03/15/the-easiest-way-to-add-recommendations-to-your-rails-app-announcing-acts_as_edgy/</link>
					<comments>https://blog.directededge.com/2011/03/15/the-easiest-way-to-add-recommendations-to-your-rails-app-announcing-acts_as_edgy/#comments</comments>
		
		<dc:creator><![CDATA[Scott Wheeler]]></dc:creator>
		<pubDate>Tue, 15 Mar 2011 18:17:44 +0000</pubDate>
				<category><![CDATA[Uncategorized]]></category>
		<guid isPermaLink="false">http://blog.directededge.com/?p=702</guid>

					<description><![CDATA[So, there are two things I want to talk about. The first is how our new beta hotness, acts_as_edgy, just made it super easy to add Directed Edge recommendations to Rails 2 apps. How easy? One line easy. Recommendations the Easy Way Let&#8217;s suppose for example that you&#8217;ve got a model called User and another [&#8230;]]]></description>
										<content:encoded><![CDATA[<p><img decoding="async" loading="lazy" src="https://blog.directededge.com/wp-content/uploads/2011/03/rails-plus-directededge.png" alt="" title="Rails plus Directed Edge" width="250" height="111"/></p>
<p>So, there are two things I want to talk about.  The first is how our new beta hotness, <tt>acts_as_edgy</tt>, just made it super easy to add Directed Edge recommendations to Rails 2 apps.  How easy?  One line easy.</p>
<h3 style="margin-bottom: 1em">Recommendations the Easy Way</h3>
<p>Let&#8217;s suppose for example that you&#8217;ve got a model called User and another called Product.  And let&#8217;s also suppose that you support &#8220;likes&#8221; in your app, so you&#8217;ve got a model called Like too that just maps from a user ID to a product ID.  Now, you want to show people personalized recommendations when they log in and related products when they&#8217;re viewing product pages.  Nothing too exotic there.</p>
<p>So, here&#8217;s what you add to your app in that case:</p>
<pre lang="ruby">
class User < ActiveRecord::Base
  acts_as_edgy(:like, Like, Product)
  ...
end
</pre>
<p>The <tt>acts_as_edgy</tt> line is the whole shebang.  That's it.  After that's in there you've got rake tasks that let you do <tt>rake edgy:export</tt> and it'll push your data over to our servers.  You can also have it automatically update your data with our web service on saves to your model.  Did I mention it's easy?</p>
<p>Okay, okay.  So, yeah, I skipped the part about where you actually do something with the recommendations.  But that's not rocket surgery either.  Now that you've got that in your model you can do:</p>
<pre lang="ruby">
User.first.edgy_recommended # returns a list of recommended products based on likes
Product.first.edgy_related  # returns a list of related products based on likes
</pre>
<p>You can do much more complicated stuff too.  Our Black Magic figures out the route between models that you provide a list of.  One of the apps that we've been testing with is Spree.  Let's say that we want to do product recommendations based on sales.  In Spree that means that we have to map a User to a Product, but there are a bunch of intermediaries:  Orders belong to a User, LineItems belong to an Order, LineItems also belong to a Variant and Variants belong to a Product.  Whew.  What's that look like in <tt>acts_as_edgy</tt> nomenclature?</p>
<pre lang="ruby">
class User < ActiveRecord::Base
  acts_as_edgy(:purchase, Order, LineItem, Variant, Product)
  # ...
end
</pre>
<p>You just list the models that it has to pass through on its way to its target and it figures out (or, well, tries to, this assumes you've used nice consistent column names, which you of course did, didn't you?) how to get there.</p>
<p>And then, once again, we can just query for related and recommended stuff like it's nobody's business.</p>
<p>You also have access to the regular stuff that our API supports.  So if you had both likes and purchases in the same application, that's where that handle sitting up there right at the front of the <tt>acts_as_edgy</tt> line comes in handy.  You can chose how you want to weight those, e.g.:</p>
<pre lang="ruby">
Product.first.edgy_related(:like_weight => 0.1, :purchase_weight => 0.9)
</pre>
<p>And there you've got recommendations based on some mix and match weights that seem appropriate to you.</p>
<p>I wanted to get the cool stuff out first, but there are naturally a couple of set up steps that have to happen beforehand:</p>
<p><strong>THIS IS THE PART YOU ACTUALLY HAVE TO READ</strong></p>
<ol>
<li><strong><a href="http://www.directededge.com/signup.html">Sign up for a Directed Edge account</a></strong></li>
<li>Install the <tt><a href="http://rubygems.org/gems/directed-edge">directed-edge</a></tt> and <tt><a href="http://rubygems.org/gems/will_paginate">will_paginate</a></tt> gems</li>
<li>Install the <a href="https://github.com/directededge/acts_as_edgy">plugin</a></li>
<li>Run <tt>rake edgy:configure</tt> to enter your login info</li>
</ol>
<p>There's more on the nuts and bolts of that over on the <a href="https://github.com/directededge/acts_as_edgy">Github page</a>, and just let us know if you get stuck.  But here's a real world example.  Wait.  I've just realized I lied to you.  There are <em>three</em> things I want to tell you about.</p>
<p><img decoding="async" loading="lazy" src="https://blog.directededge.com/wp-content/uploads/2011/03/spree.png" alt="" title="Spree Logo" width="176" height="82" class="alignright size-full wp-image-719" /></p>
<h3 style="margin-bottom: 1em">We have a Spree plugin.</h3>
<p>Yes, yes, we do.  Hell, we even have a full <em><a href="http://developer.directededge.com/article/Getting_started_with_Spree">tutorial</a></em> on working with Directed Edge and Spree!  It only works with Spree 0.11 at present since we haven't ported this baby to Rails 3 yet.  Mostly we needed something real to test this thing on, and Spree seemed like a nice way to test with data models that weren't tied to our own assumptions.</p>
<p>You can get at <a href="https://github.com/directededge/acts_as_edgy_spree">the full plugin on Github</a>.</p>
<p>And here are the guts -- the example I mentioned above:</p>
<ol>
<li>An <a href="https://github.com/directededge/acts_as_edgy_spree/blob/master/config/initializers/edgy_spree.rb">initializer</a> to add the line above to the User model</li>
<li>A <a href="https://github.com/directededge/acts_as_edgy_spree/blob/master/app/helpers/edgy_helper.rb">helper</a> to figure out if we're doing personalized recommendations, related products or basket recommendations</li>
<li>A <a href="https://github.com/directededge/acts_as_edgy_spree/blob/master/app/views/shared/_edgy_related_table.html.erb">partial template</a> to handle showing the results</li>
</ol>
<p>That's pretty lean for all of the glue and display code for adding recommendations (and instant updating) to a full-blown e-commerce thingereedoo.</p>
<h3 style="margin-bottom: 1em">The nerdy bits.</h3>
<p>There are a few neat technical things that are happening behind the scenes to make all of this stuff easy from a user's perspective.</p>
<p><strong>SQL Generator</strong></p>
<p>One of them is a fancy custom SQL generator that builds optimal queries for all of this stuff.  Entire Rails models get exported with one query.  The generated SQL can get hella ugly, but it offloads most of the dirty work to the database rather than having to do it all in Ruby code.</p>
<p>The above Spree example (the User to Product mapping) generates this SQL monstrosity:</p>
<pre lang="sql">
select users.id as from_id, 'user' as from_type, variants.product_id as to_id,
case when variants.product_id is not null then 'product' end as to_type,
case when variants.product_id is not null then 'purchase' end as link_type from users
left outer join orders on users.id = orders.user_id
left outer join line_items on orders.id = line_items.order_id
left outer join variants on line_items.variant_id = variants.id
where users.id is not null order by from_id;
</pre>
<p>We build up a thing we call a "connection" which is a number of "bridges".  A bridge is just a route between two models, including the foreign keys and so a connection is a full path from one model to another one via a chain of foreign keys.  In the simple case this is detected based on the model's name and built in ActiveRecord methods for reporting foreign keys, but you can also specify a bridge manually for foreign keys that are created that don't match the typical nomenclature.  That isn't documented at the moment, but shout if that's something that you need.</p>
<p><strong>Triggers</strong></p>
<p>Another neat thing is the automatic installation of model triggers.  So when it's building that connection mentioned before, our system knows which models trigger updates which need to be sent over to Directed Edge to keep your data in sync.</p>
<p>So if <tt>acts_as_edgy</tt> is set up to automagically send updates (a config parameter that can be called in the config blog that gets written when you call rake edgy:configure) then as soon as a model changes anywhere along that path, we get the goods.  And this triggers a different code path that leads to our SQL generator just pulling out the stuff that needs to be updated in a single query.</p>
<p><b>Future:  Asynchronous Web-service Calls</b></p>
<p>And since those updates are hitting a remote web service, it's ideal if they're not blocking execution.  We make liberal use of a class we call Future (conceptually borrowed from <a href="http://doc.qt.nokia.com/4.7-snapshot/qfuture.html">QFuture</a> from Qt) which executes a block of code in a background thread and only forces synchronization when its data is accessed.  Here's what it looks like:</p>
<pre lang="ruby">
class Future
  def initialize(postprocessor = nil, &finalize)
    if ENV['EDGY_SYNCHRONOUS']
      @data = postprocessor ? postprocessor.call(finalize.call) : finalize.call
      return
    end
    @postprocessor = postprocessor
    @future = Thread.new do
      begin
        finalize.call
      rescue => ex
        warn "Exception in background thread: #{ex}"
      end
    end
  end

  def method_missing(method, *args, &block)
    data.send(method, *args, &block)
  end

  def to_s
    data.to_s
  end

  private

  def data
    @data ||= @postprocessor ? @postprocessor.call(@future.value) : @future.value
end
</pre>
<p>Since we implement <tt>method_missing</tt> on the Future class to first block and then forward the call on, you can use it just like a normal value, e.g.:</p>
<pre lang="ruby">
foo = Future.new { sleep 1; 42 }
puts foo # prints 42
</pre>
<p>Altogether, while weighing in at a slim 382 lines of code the plugin is fairly light, but it's pretty dense code with quite a bit of interesting stuff going on.</p>
<p>It's still quite beta, so we expect there will be a handful of kinks to iron out and there are features we'd still like to add, but hopefully we'll be doing that iteratively as you wonderful folks start building things with it and telling us what you need.</p>
]]></content:encoded>
					
					<wfw:commentRss>https://blog.directededge.com/2011/03/15/the-easiest-way-to-add-recommendations-to-your-rails-app-announcing-acts_as_edgy/feed/</wfw:commentRss>
			<slash:comments>9</slash:comments>
		
		
			</item>
		<item>
		<title>Google Spam Heresy: The AdSense Paradox</title>
		<link>https://blog.directededge.com/2011/01/06/google-spam-heresy-the-adsense-paradox/</link>
					<comments>https://blog.directededge.com/2011/01/06/google-spam-heresy-the-adsense-paradox/#comments</comments>
		
		<dc:creator><![CDATA[Scott Wheeler]]></dc:creator>
		<pubDate>Thu, 06 Jan 2011 08:38:32 +0000</pubDate>
				<category><![CDATA[Uncategorized]]></category>
		<guid isPermaLink="false">http://blog.directededge.com/?p=696</guid>

					<description><![CDATA[There&#8217;s been much ado about the problem of spam and Google of late. Being something of a search weenie, as my eyelids were feeling heavy today I found myself mulling over the problem, &#8220;How would one detect Google spamming?&#8221; The answer turns out to be surprisingly easy. Who has an incentive to spam Google? People [&#8230;]]]></description>
										<content:encoded><![CDATA[<p>There&#8217;s been <a href="http://www.marco.org/2617546197">much</a> <a href="http://www.codinghorror.com/blog/2011/01/trouble-in-the-house-of-google.html">ado</a> about the problem of spam and Google of late. Being something of a search weenie, as my eyelids were feeling heavy today I found myself mulling over the problem, &#8220;How would one detect Google spamming?&#8221;</p>
<p>The answer turns out to be surprisingly easy. Who has an incentive to spam Google? People living from advertising. Who owns the largest online display ad network? Google.</p>
<p>Unlike with email, where there are heuristics at work to guess the intentions of the sender based on the content, Google has that data right in front of them.</p>
<p><strong>So, here&#8217;s the heresy: the spamminess of a web site is inversely proportional to its ad click-through.</strong></p>
<p>Think about it &#8212; in a typical internet search, a navigation path terminating at that page is the best result. If they click on an ad, it probably means you missed serving up the right page in the first place. As a corollary, the pages best optimized to pull you in via a search term and send you back out via a related ad are among the worst results.</p>
<p>So if you&#8217;re Google which value do you optimize for? More ad clicks or better search results? I&#8217;m a big enough Google fan that I believe that they&#8217;d mostly want to optimize for good search results since that&#8217;s what made them the company that they are. But what do you do if there&#8217;s an inverse correlation between the two? Bite the hand that feeds you?</p>
<p>Thinking about things this way in my opinion makes the issue even more interesting, because it seems to hint at something systemic &#8212; i.e. that there there might be something deeper problematic in financing search through display advertising.</p>
<p>Obviously this is a massive oversimplification of the problem of spam, but the paradox intrigued me.</p>
]]></content:encoded>
					
					<wfw:commentRss>https://blog.directededge.com/2011/01/06/google-spam-heresy-the-adsense-paradox/feed/</wfw:commentRss>
			<slash:comments>6</slash:comments>
		
		
			</item>
		<item>
		<title>What&#8217;s happening?</title>
		<link>https://blog.directededge.com/2010/11/29/whats-happening/</link>
					<comments>https://blog.directededge.com/2010/11/29/whats-happening/#respond</comments>
		
		<dc:creator><![CDATA[Scott Wheeler]]></dc:creator>
		<pubDate>Tue, 30 Nov 2010 05:55:55 +0000</pubDate>
				<category><![CDATA[Uncategorized]]></category>
		<guid isPermaLink="false">http://blog.directededge.com/?p=687</guid>

					<description><![CDATA[So, the blog has been a bit silent of late, mostly because there&#8217;s been a lot of incremental stuff that&#8217;s been happening. We shout that stuff out over at Twitter and Facebook, but we&#8217;ll pull some of the last couple-o-months together here. But while I still have you attention: We need Rails beta testers. If [&#8230;]]]></description>
										<content:encoded><![CDATA[<p><img decoding="async" loading="lazy" class="aligncenter size-full wp-image-688" title="What's happening?" src="https://blog.directededge.com/wp-content/uploads/2010/11/whats-happening.jpg" alt="" width="400" height="273" /></p>
<p>So, the blog has been a bit silent of late, mostly because there&#8217;s been a lot of incremental stuff that&#8217;s been happening. We shout that stuff out over at <a href="http://twitter.com/directededge">Twitter</a> and <a href="http://www.facebook.com/directededge">Facebook</a>, but we&#8217;ll pull some of the last couple-o-months together here. But while I still have you attention:</p>
<p><strong>We need Rails beta testers. If you&#8217;ve been thinking about adding recommendations to your Rails app, <a href="mailto:info@directededge.com">drop us a line now</a>.</strong></p>
<p>So, with that out of the way, a few of the things that have gone down in Directed Edge-land of late:</p>
<ul>
<li>We&#8217;ve been ramping up the features in our Shopify app based on things that we&#8217;ve been hearing from customers. The Shopify folks covered our new automagic bundling feature <a href="http://blog.shopify.com/2010/11/29/app-roundup">here</a>.</li>
<li>New version of our Ruby gem, 0.2.1 <a href="http://rubygems.org/gems/directed-edge">out</a>, and a couple of features added to our <a href="https://github.com/directededge/directed-edge-bindings/tree/master/Java/">Java bindings</a> so that they approachÂ parityÂ with the other bindings.</li>
<li>Wener Vogels, CTO of Amazon, has been spreading the good news about Directed Edge and we caught up with him when he was presenting about AWS in Berlin.</li>
<li>Yours truly will be doing a big tour out through New York, Austin and San Francisco / Silicon Valley in December and January.Â HollerÂ if you want to meet and talk about recommendations.</li>
</ul>
<div id="attachment_691" style="width: 610px" class="wp-caption aligncenter"><img aria-describedby="caption-attachment-691" decoding="async" loading="lazy" class="size-full wp-image-691" title="Werner Vogels - Directed Edge" src="https://blog.directededge.com/wp-content/uploads/2010/11/werner-vogels-directed-edge.jpeg" alt="" width="600" height="337" /><p id="caption-attachment-691" class="wp-caption-text">Werner Vogels presenting Directed Edge as &quot;one of the building blocks of Cloud Computing&quot;</p></div>
<p>While a lot of the stuff in the last few months has been focused on internal tools and run-of-the-mill growing pains stuff, we&#8217;re getting pretty close to a series of announcements about new stuff coming down the pipe. (For some reason those always seem to come in batches.) We&#8217;re hoping the first of those will be ready in the next week or so.</p>
]]></content:encoded>
					
					<wfw:commentRss>https://blog.directededge.com/2010/11/29/whats-happening/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
		<item>
		<title>Directed Edge delivering 33% of revenue for The Market Quarter</title>
		<link>https://blog.directededge.com/2010/06/16/directed-edge-delivering-33-of-revenue-for-the-market-quarte/</link>
					<comments>https://blog.directededge.com/2010/06/16/directed-edge-delivering-33-of-revenue-for-the-market-quarte/#respond</comments>
		
		<dc:creator><![CDATA[Scott Wheeler]]></dc:creator>
		<pubDate>Wed, 16 Jun 2010 20:44:10 +0000</pubDate>
				<category><![CDATA[Uncategorized]]></category>
		<guid isPermaLink="false">http://blog.directededge.com/?p=681</guid>

					<description><![CDATA[Jonathan Briggs, who runs The Market Quarter wrote a few months back on getting up and going with our Shopify app for recommendations. He reported back today after analyzing the first few months of data: What astounded me was how many of my customers (not just visitors) clicked on recommendations. Indeed the ExpressRex referrer is [&#8230;]]]></description>
										<content:encoded><![CDATA[<p><img decoding="async" loading="lazy" class="aligncenter size-full wp-image-682" title="marketquarter" src="https://blog.directededge.com/wp-content/uploads/2010/06/marketquarter.png" alt="" width="710" height="438" srcset="https://blog.directededge.com/wp-content/uploads/2010/06/marketquarter.png 710w, https://blog.directededge.com/wp-content/uploads/2010/06/marketquarter-700x431.png 700w" sizes="(max-width: 710px) 100vw, 710px" /><a href="http://www.jonathanbriggs.com/">Jonathan Briggs</a>, who runs <a href="http://www.marketquarter.com/">The Market Quarter</a> wrote a few months back on <a href="http://www.jonathanbriggs.com/experimenting-with-shopify-product-recommendation-engine-app,803,AR.html">getting up and going with our Shopify app</a> for recommendations. He reported back today after analyzing the first few months of data:</p>
<blockquote><p>What astounded me was how many of my customers (not just visitors) clicked on recommendations. Indeed the ExpressRex referrer is responsible for a full third (33%) of my revenue in the last quarter and has a 36% conversion rate.</p></blockquote>
<p>You can read the <a href="http://www.jonathanbriggs.com/ecommerce/33-of-revenue-expressrex-recommendation-engine-recommended,814,AR.html">full post</a> in his blog.</p>
]]></content:encoded>
					
					<wfw:commentRss>https://blog.directededge.com/2010/06/16/directed-edge-delivering-33-of-revenue-for-the-market-quarte/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
		<item>
		<title>Shopify: Help Your Customers Discover Products and Grow Your Average Order Size</title>
		<link>https://blog.directededge.com/2010/06/07/shopify-help-your-customers-discover-products-and-grow-your-average-order-size/</link>
					<comments>https://blog.directededge.com/2010/06/07/shopify-help-your-customers-discover-products-and-grow-your-average-order-size/#respond</comments>
		
		<dc:creator><![CDATA[Scott Wheeler]]></dc:creator>
		<pubDate>Mon, 07 Jun 2010 22:24:29 +0000</pubDate>
				<category><![CDATA[Uncategorized]]></category>
		<guid isPermaLink="false">http://blog.directededge.com/?p=678</guid>

					<description><![CDATA[From over at the Shopify blog: Eric Houtkooper of www.PupLife.com helps customers find and discover other products they may want. He has increased his customersâ€™ average order size by recommending products as they browse the store and go through checkout. He suggests you do the same&#8230; More there.]]></description>
										<content:encoded><![CDATA[<p>From over at the Shopify blog:</p>
<blockquote><p>Eric Houtkooper of <a href="http://www.puplife.com/">www.PupLife.com</a> helps customers find and discover other products they may want. He has increased his customersâ€<img src="https://s.w.org/images/core/emoji/14.0.0/72x72/2122.png" alt="™" class="wp-smiley" style="height: 1em; max-height: 1em;" /> average order size by recommending products as they browse the store and go through checkout. He suggests you do the same&#8230;</p></blockquote>
<p>More <a href="http://blog.shopify.com/2010/6/7/help-your-customers-discover-products-and-grow-your-average-order-size">there</a>.</p>
]]></content:encoded>
					
					<wfw:commentRss>https://blog.directededge.com/2010/06/07/shopify-help-your-customers-discover-products-and-grow-your-average-order-size/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
		<item>
		<title>Using taps without running a taps server</title>
		<link>https://blog.directededge.com/2010/06/05/using-taps-without-running-a-taps-server/</link>
					<comments>https://blog.directededge.com/2010/06/05/using-taps-without-running-a-taps-server/#respond</comments>
		
		<dc:creator><![CDATA[Scott Wheeler]]></dc:creator>
		<pubDate>Sun, 06 Jun 2010 04:50:18 +0000</pubDate>
				<category><![CDATA[Uncategorized]]></category>
		<guid isPermaLink="false">http://blog.directededge.com/?p=674</guid>

					<description><![CDATA[So, the Ruby world has a nifty thinger for syncing up databases over the interwebs. It&#8217;s called Taps, from the superheroes over at Heroku. It&#8217;s great &#8212; you just run a little Sinatra-based server and then give the database URLs and it handles all of the plumbing. But, you see, I was none-too-keen on having [&#8230;]]]></description>
										<content:encoded><![CDATA[<p>So, the Ruby world has a nifty thinger for syncing up databases over the interwebs. It&#8217;s called <a href="http://adam.heroku.com/past/2009/2/11/taps_for_easy_database_transfers/">Taps</a>, from the superheroes over at <a href="http://heroku.com/">Heroku</a>. It&#8217;s great &#8212; you just run a little Sinatra-based server and then give the database URLs and it handles all of the plumbing.</p>
<p>But, you see, I was none-too-keen on having another long-running Ruby process, not to mention an open port with production database data lumbering around on it, so I thought I&#8217;d let you guys in on a little hack we produced internally to let you get all of the fun of taps, but without the taps server.</p>
<p>Basically it starts up the taps server on the remote server, tunnels the transfer over SSH, then sends a ctrl-c to the server to kill it&#8217;s done. It&#8217;s pull-only very intentionally &#8212; I want to push from a development database to a production database about like I want a hole in my head.</p>
<pre lang="ruby">
#!/usr/bin/env ruby

require 'rubygems'
require 'active_support/secure_random'
require 'net/ssh'

SSH_USER = '[sshuser]'
SSH_HOST = '[dbhost]'

LOCAL_DB = 'mysql://[dbuser]:[dbpass]@localhost/[dbname]'
REMOTE_DB = 'mysql://[dbuser]:[dbpass]@localhost/[dbname]'

TAPS_USER = ActiveSupport::SecureRandom.hex(16)
TAPS_PASS = ActiveSupport::SecureRandom.hex(16)

URL = "http://#{TAPS_USER}:#{TAPS_PASS}@localhost:5000"

Net::SSH.start(SSH_HOST, SSH_USER, :compression => true) do |ssh|
  ssh.forward.local(5000, 'localhost', 5000)

  ready = false

  channel = ssh.open_channel do |c|
    c.request_pty
    c.on_data { |c, data| ready = true if data =~ /port=5000/ }
    c.exec("taps server #{REMOTE_DB} #{TAPS_USER} #{TAPS_PASS}")
  end

  finished = false

  Thread.new do
    sleep 0.1 until ready
    system "taps pull #{LOCAL_DB} #{URL}"
    finished = true
  end

  ssh.loop(0.1) do
    channel.send_data(Net::SSH::Connection::Term::VINTR) if finished
    !finished
  end
end
</pre>
<p>Substitute in the right values in the constants up at the top and you&#8217;ve got a nifty way to securely use taps without leaving a server running.</p>
]]></content:encoded>
					
					<wfw:commentRss>https://blog.directededge.com/2010/06/05/using-taps-without-running-a-taps-server/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
	</channel>
</rss>

<!--
Performance optimized by W3 Total Cache. Learn more: https://www.boldgrid.com/w3-total-cache/

Page Caching using disk: enhanced (SSL caching disabled) 
Minified using disk

Served from: blog.directededge.com @ 2026-04-04 10:31:06 by W3 Total Cache
-->