Named entity highlighting in action

Chrome Extensions with Natural Language Processing

David Tao
RateMyInvestor

--

How to: create a chrome extension that manipulates the DOM based on named entities.

Creating the Know Your VC chrome extension was more of a spur of the moment idea if anything.

Before I go in to the nuances of how we made this product, you should check out our page at knowyourvc.com, and also check out our Chrome extension here! This project is also the first open source project that I’ve released — go check it out on Github here. I’ll be talking through some of the code in the repo if you want to follow along!

Overall, the extension is a simple content-scripts extension that listens for the document.ready event and recursively parses through the DOM with a named entity parser. For every name it finds in this recursive call, we make a simple request to our Know Your VC API, which does a simple query for the name. If the name returns something other than null, our DOM parser will append divs around the name node that it found, and add a hover event for the popup.

Sounds pretty simple doesn’t it? It is!

Our extension begins with instantiating a new Hilitor. This is a very small highlighting library for javascript, which I tweaked myself for this extension. in the content.js file, we call hilitewords and we pass in the DOM node that we want to hilite. This would ideally be the root node of the DOM.

We first check if this node has child nodes, and make a recursive call to each of it’s children:

if(node.hasChildNodes()) {      
for(var i=0; i < node.childNodes.length; i++)
this.hiliteWords(node.childNodes[i]);
}

Then, we check if the node is a text node by checking the nodeType attribute of each node. if nodeType == 3 , that means we should be parsing the node.

Next, we use (probably the only) light-weight natural language processing library called compromise. It includes some named entity recognition, which we’ll be relying heavily on to parse our DOM.

We pass in the string of each DOM text node, and the NLP library returns us a very handy object full of the entities that it’s recognized, including some very information depending on the type of entity.

doc = nlp(node.nodeValue);
var data = doc.people().data();
if (data.length > 0) {
var self = this;
data.map(function(entity) {
if (entity.hasOwnProperty('firstName')) {
// do your DOM parsing here

Just a note on this library — you’d expect this to be a very costly with your standard rule-based NLP libraries. But the package was built for Javascript and web interfaces in mind, so it’s extremely lightweight and runs incredibly quickly.

We’re checking for whether the entity that the library recognized has a firstName in order to not overload the API with useless results.

Although not ideal, what we do next is we send an API request for each name that it’s found with a firstName field. But before we do that, we need to instantiate the wrappers for our new highlighted word. This actually gets a lot more complicated than it seems. Simply attaching an <em> tag before and after the word would suffice if we’re just trying to highlight all the names that appear, but we’re also adding a card that appears on hover with the investor’s information. Here’s where it starts to get a bit tricky.

var wrapper = document.createElement('div');              
var popup = document.createElement('div');
var stars = document.createElement('div');

We start manually creating DOM elements that we’ll be attaching soon. We need to parse these DOM elements along with all the CSS and attributes that it needs while get the information from our API.

Next, we make the API call! We made a very simple EC2 instance on AWS in order to support this — implementation on our API will be discussed for a later time.

But the thing with content-scripts is the additional security for non-SSL requests. This is a problem because Chrome sees this cross-origin request as something that’s not supposed to happen from one of it’s extensions. So what can we do?

This is where it starts to get a bit tricky — we extrapolate the request into our background scripts.

chrome.runtime.sendMessage({                
method: 'GET',
action: 'xhttp',
url: url,
}, function (res) {
res = JSON.parse(res);
// inject info into DOM

We’re sending a message to our background scripts, and we have a listener as well:

chrome.runtime.onMessage
.addListener(function (request, sender, callback) {
if (request.action == "xhttp") {
var xhttp = new XMLHttpRequest();
// Rest of the request

Our background scripts makes our requests for us, and it just passes the necessary payload back into the callback function we pass in.

Now we check whether the investor exists by checking the JSON object for the investorId and review:

if (!res.investorId) {                
return;
}
...
if (res.review) {
// append review text

The code beyond this has quite a bit of refactoring to do! But basically we just add onClick and hover event listeners to hide/show the review card that we’ve been creating.

And that’s basically it! While not the cleanest implementation, the extension is quite usable when looking for investors — although we do have quite a bit of mismatch between the names Compromise finds and the names in our database.

Moving forward, we should definitely consider training our own classifier. What would work for this is a sequence-to-sequence model that takes in input of the string and returns just the name of the investor found. The problem with this is building the training data from our database of investors and also the size of the model. A sequence to sequence model prediction is not a light operation — what might be best is if we made our own named entity recognizer that overfits for our own investors!

--

--