Finding Interesting People
Online dating is one of those things, like auctions and classified listings, that's really only valuable once the userbase reaches a certain size. It could be argued that the threshold is even higher for dating sites, since one will presumably only use the service until they've found someone. Unlike auction and classified sites, successful use of a dating site means abandoning the service. I've never seriously used an online personals site, but I've seen what's out there and have been struck by tasteless advertising, exploitation of users to lure in others, poor design, and overly complicated questionnaires. It would seem a waste of time to even fill out the forms.
I've investigated different ways of providing a valuable meeting service without having to build up a dedicated network first. I approached this idea as location-aware software, dabbled with Facebook, and eventually built on top of Twitter. I started out with an iPhone app that generated a "compatibility score" of people around you from data obtained from a fairly extensive set of questions. It would ping the server with one's current location, and then compare the GPS coordinates and each user's answers. Wasn't right. Questionnaires are weird when the people that are filling them out are around. I felt awkward asking anyone to fill it out, and wasn't too compelled to do so myself, which is clearly a bad sign. Maybe we only like filling out the same form a few times, before it gets tirelessly old. Some have done them for dating sites, or on MySpace, Facebook, and others. It's one thing when you're expressing information about yourself for the hell of it, and another when looking for something in return.Now since a lot of people already have profiles up somewhere online, I thought about doing a Facebook or OpenSocial app. Again I was able to come up with a technical solution that didn't work socially. I just didn't want what I'd written. If making something you (and by extension, others) want is "scratching an itch," this might be "scratching a scab." For whatever reason, I for one don't want to walk around broadcasting my Facebook profile with my current location. Around this time I first tried out Loopt. When they rolled out the Mix feature, they nailed the core functionality of what I had in mind, and true enough, it wasn't that compelling (in Maine anyway).I think exploratory product development is graph traversal: you proceed down one path, discover it's flawed, go down another, see someone else has already realized a big part of it, reduce down what you considered important, and move in another direction. My work on this project was essentially depth-first, with an aim to "do matchmaking right."Most approaches to matchmaking-style dating sites seem to be a kind of "compatibility test." This might be the optimal strategy for, say, finding an organ donar or maybe an employee, but doesn't seem well-suited to starting romantic relationships, or even friendships. Around this time I watched Stephen Pinker discuss the non-rational basis of love, and it's evolutionary function:.
I'd been going about it all wrong. I was trying to do pattern matching when what I really wanted were interesting diffs. What I was looking for was luck, surprise, and randomness; those were the requirements to optimize for. When I was able to mentally articulate this, it gave me a new perspective on the project, and the design became more clear. I wrote a (mobile-optimized) web app with a reduced set of questions and an algorithm based on complementary answers, timing, and a heavy helping of stochasm. The basic idea is that one fills in some simple fields and gets occasional messages linking to matched profiles. To implement this, I first had to choose between different communication protocols. Should contact be by location, phone, email, IM, Facebook, or Twitter?Twitter seemed to fit best. The pure communication systems lacked much context. Just being recommended an address isn't that helpful; what you want is a way of checking someone out first, and then being able to communicate with them comes second. Plus, I realized that the best meeting site probably won't even have to be about "dating" per se. So I generalized it out to just finding interesting people on Twitter:
http://tweetmatcher.appjet.net
In gearing up for this application, I started writing a wrapper over the Twitter API for AppJet. While it wasn't used extensively in the app, it's somethat that I see being useful (to myself and others) in the future:
http://source.lib-status.appjet.net
When it came to the interface, I decided to make this very much an exercise in getting a form to work with smart, reusable text fields. Forms are a pain: validation is fairly arbitrary, and it's a real pain to write the logic (on the client and server!) each time you want to collect some information. So I wrote a number of jQuery extensions, which would tap into the Value.js library I wrote largely in parallel.
The first field in the application is "Username." When text is entered, the given user name is queried against Twitter to see if it's a real user. Right now there's no protection against spoofing someone else's name, in the future a password field should be included, or Twitter OAuth (which was opened after primary development) could be used. The implementation is a big hack: it just looks up a Twitter page and checks the source to see if the user exists. This could be done more elegantly by using the REST API.Let's look more deeply the age extension to jQuery.
/**
* Change background color on age.
*
* @params {object} [o] Options. Can pass on/off properties for activated/deactivated colors and min/max for youngest/oldest valid age.
*/
jQuery.fn.age = function(o) { o = o || {} o.on = o.on || "#90ee90"
o.off = o.off || "#ff5d5d"
o.min = o.min || 18
o.max = o.max || 115 // http://en.wikipedia.org/wiki/Gertrude_Baines var agelength = 0 $(this).keypress(function(e) { var key = $.which(e) if (!$.is.num(e)) return(false) key != 8 && agelength+1 <= 3 && agelength++
key == 8 || agelength != 3 && agelength-- if (agelength <= 3) { var age = $("input[name=age]").val(),
id = Number(key != 8 ? Number(age+$.str(e)) :
age.substring(0, age.length-1)) if (id && (id < o.min || id > o.max))
$("input[name=age]").bg($.start(o.off, "#"))
else if (id >= o.min && id <= o.max)
$("input[name=age]").bg($.start(o.on, "#"))
else
$("input[name=age]").bg("#fff") } })}
The function optionally takes in an object specifying activated/deactivated color and minimum/maximum age, with defaults provided otherwise. [1] The keypress method gets called on each press of a key. It only takes numbers, and then displays whether the given age is invalid (show deactivated background color) or not (activated background). Here on and off basically mean valid and invalid.
bg jQuery extension method. Defined earlier in the source, this is a convenience allowing one to say
$("a").bg("fff")
instead of
$("a").css("background", "#fff")
The "Gender" and "Seeking" fields are implemented similarly to the age extension.
The "Location" field uses an API designed by Aza Raskin, using the MaxMind geolocation API. It's not perfect, sometimes it'll get caught on "Loading..." and can give inaccurate results (it thinks my iPhone is in New York). But it usually works for me, giving United States and Maine for Country and District respectively. When a result is determined, it's cached. So if "Country" is clicked, then "District," and then "Country" again, the country name is put in from saved state on the client, without having to pull the data again.
The "Interests" field basically collects keywords for comparison. Right now given terms are just put up against others' submissions, but these could be used to find people on Twitter by using the Search API. Right now hitting tab jumps down to the submit button. It could alternatively add a new field below the current one, which for now has to be done by clicking the "+" with the mouse. Ideally the interface should work completely with just the keyboard.
Let's turn to the server-side. While client-side validation is very mature, the server basically just spits back anything obviously wrong. The key parts of the server-side are the matching algorithm and caching mechanism. Matching is done by a function called get_match; (the get_ is just to denote that it gets executed on HTTP GET requests to the /match subdirectory; this is a built-in AppJet feature and is somewhat reminiscent of Sinatra). The match function computes the differences between other (suitable) users, and comes up with a score. This is kept internal, and depends on the prefs object, which gives the "points" to award for similarities (i.e. living in the same location awards 10 points right now; each shared interest 2 points). While this is still a "compatibility score," the idea is that it could be determined implicitly (scraping profile information, at the user's command, perhaps), and from this light the form is just a way of testing it out.
I didn't realize before working on this software that caching is the key to performance. I implemented three caching mechanisms throughout the program, for usernames, geolocation, and on the client-side. Retrieved usernames from Twitter are stored in the database, so typing "max" should be faster than doing, I don't know, "askjki." This also decreases requests needed to be made to Twitter, and since the data is fairly small, shouldn't be much of a burden to me, at least for now. The geolocation determined from IP is handled in a similar manner. And the client-side caching, mentioned earlier, just saves location results in an object on the client.
I'm probably most happy with the jQuery extensions and AppJet Twitter API wrapper I wrote. I feel that the interface components are valuable, my experience working on caching worthwhile, and the result achieves what I set out to do. I plan to continue working on this through the components, and see what more can come of it.
The code is available at http://source.tweetmatcher.appjet.net; feel free to clone it!
