2

I have user IDs like a6347324, b7432408, f54789922 (non-numeric), and numeric item IDs. I want to able to use the REST API in Myrrix to insert such user-item pairs. But /pref/a6347324/1234 returns an error. It is expected, considering the parseLong in line 74 of https://github.com/myrrix/myrrix-recommender/blob/master/web-common/src/net/myrrix/web/servlets/PreferenceServlet.java

I am using version 0.11. I don't want to use the "tag" feature since it does not return the string tags like equivalent numeric IDs, and I don't think it will work with the because API (since tags are not returned!). Will there be a fix for this soon? Is there an immediate workaround that I can use or implement quickly?

Nilesh
  • 1,222
  • 1
  • 11
  • 23

2 Answers2

2

The REST API itself works in terms of numeric IDs, by design. It's the client that potentially does hashing of some other ID -- numeric or not -- to make a numeric ID. You want to use TranslatingClientRecommender instead, which can help manage the translation back to original IDs.

The added benefit is potentially some security; you need not actually send real user and item IDs to the service, which may be important if it is to be hosted on a third party cloud like Amazon EC2.

Sean Owen
  • 66,182
  • 23
  • 141
  • 173
  • Suppose I have both my client and the server on the same machine. My application is in PHP, so using the REST-API is easy. But using TranslatingClientRecommender would mean that I will have to use the java myrrix-client, right? Shouldn't calling that for every addition of preference or any operation whatsoever in real time, incur a non-negligible overhead, considering it has to fire up the JVM every time? I guess my only option is to build a custom wrapper around the existing REST-API, isn't it? – Nilesh Apr 14 '13 at 19:52
  • Yes I would not do that. Michael Salib has created an unofficial PHP client: https://github.com/michelsalib/bcc-myrrix but I don't think it does hashing. You can perform whatever hashing you like in PHP; the only difficulty is maintaining the reverse mapping somehow. Then you just translate to/from as needed. If you wanted to be consistent with Java, use the lower 64 bits of the MD5 hash of the stream. – Sean Owen Apr 14 '13 at 19:58
  • Yes, I already use bcc-myrrix, but it doesn't do translating/hashing. I think I'll use an MD5 hash as you mentioned and probably use the MySQL 5.6 InnoDB/memcached key-value store to store the map. Thanks. – Nilesh Apr 14 '13 at 20:51
  • If you already have users / items in a DB, consider adding a simple unique row ID too. Or storing some hash as a column automatically. There are many ways to do this. – Sean Owen Apr 14 '13 at 21:12
  • I don't already have users/items in a DB. But if I did, what purpose would the unique row ID (autonumber or automatically added hash) serve though? – Nilesh Apr 14 '13 at 21:29
  • 1
    It gives you a numeric ID for these entities, which is all you're doing with hashing anyway. And it would implicitly be a mapping you can access both ways easily. But if you don't have it already it's a moot point. – Sean Owen Apr 14 '13 at 22:54
1

I decided to add this answer with some details on how I actually ended up solving the problem. Please note that I've made significant modifications later and made the code more object oriented. This is just a simple explanation for ease of understanding.

Here's a WebClientRecommender class that's basically a small wrapper around a TranslatingClientRecommender.

package suggester.client.recommender;

import java.io.File;
import java.io.Reader;
import java.net.URI;
import java.util.Arrays;
import java.util.List;
import net.myrrix.client.MyrrixClientConfiguration;
import net.myrrix.client.translating.TranslatedRecommendedItem;
import org.apache.mahout.cf.taste.common.TasteException;

/**
 *
 * @author Nilesh Chakraborty
 */
public class WebClientRecommender extends AbstractClientRecommender {

    public WebClientRecommender(MyrrixClientConfiguration myrrixClientConfiguration) {
        super(myrrixClientConfiguration);
    }

    @Override
    public List<TranslatedRecommendedItem> recommend(String idListFile, String recommendTo, String recommendType, int howMany) throws TasteException {
        clientRecommender.addItemIDs(new File(idListFile));
        List<TranslatedRecommendedItem> recommendations = clientRecommender.recommend(recommendTo, howMany, false, new String[]{recommendType});
        return recommendations;
    }

    @Override
    public List<TranslatedRecommendedItem> recommendAnonymous(String idListFile, String recommendType, int howMany, String[] list) throws TasteException {
        clientRecommender.addItemIDs(new File(idListFile));
        float[] values = new float[list.length];
        Arrays.fill(values, 30);
        List<TranslatedRecommendedItem> recommendations = clientRecommender.recommendToAnonymous(list, values, howMany, new String[]{recommendType}, "testID");
        return recommendations;
    }

    public List<TranslatedRecommendedItem> recommendAnonymous(URI idListFile, String recommendType, int howMany, String[] list) throws TasteException {
        clientRecommender.addItemIDs(new File(idListFile));
        float[] values = new float[list.length];
        Arrays.fill(values, 30);
        List<TranslatedRecommendedItem> recommendations = clientRecommender.recommendToAnonymous(list, values, howMany, new String[]{recommendType}, "testID");
        return recommendations;
    }

    @Override
    public void ingest(Reader csvReader) throws TasteException {
        clientRecommender.ingest(csvReader);
    }
}

Here is AbstractClientRecommender:

package suggester.client.recommender;

import java.io.IOException;
import java.io.Reader;
import java.util.List;
import java.util.logging.Level;
import java.util.logging.Logger;
import net.myrrix.client.ClientRecommender;
import net.myrrix.client.MyrrixClientConfiguration;
import net.myrrix.client.translating.TranslatedRecommendedItem;
import net.myrrix.client.translating.TranslatingClientRecommender;
import org.apache.mahout.cf.taste.common.TasteException;

/**
 *
 * @author nilesh
 */
abstract class AbstractClientRecommender {

    protected TranslatingClientRecommender clientRecommender;

    public AbstractClientRecommender(MyrrixClientConfiguration myrrixClientConfiguration) {
        try {
            clientRecommender = new TranslatingClientRecommender(new ClientRecommender(myrrixClientConfiguration));
        } catch (IOException ex) {
            Logger.getLogger(CLIClientRecommender.class.getName()).log(Level.SEVERE, null, ex);
        }
    }

    public abstract List<TranslatedRecommendedItem> recommend(String idListFile, String recommendTo, String recommendType, int howMany) throws TasteException;

    public abstract List<TranslatedRecommendedItem> recommendAnonymous(String idListFile, String recommendType, int howMany, String[] list) throws TasteException;

    public void ingest(String csvFile) throws TasteException {
    }

    public void ingest(Reader csvReader) throws TasteException {
    }
}

Now, I'm using the WebClientRecommender class in a custom Servlet. Here's SuggesterServlet:

package suggester.client.servlets;

import com.google.common.base.Splitter;
import suggester.client.recommender.WebClientRecommender;
import java.io.IOException;
import java.net.URL;
import javax.naming.Context;
import javax.naming.InitialContext;
import javax.naming.NamingException;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import net.myrrix.client.MyrrixClientConfiguration;
import net.myrrix.web.servlets.AbstractMyrrixServlet;

/**
 *
 * @author nilesh
 */
public abstract class AbstractSuggesterServlet extends AbstractMyrrixServlet {

    static final Splitter SLASH = Splitter.on('/').omitEmptyStrings();
    private WebClientRecommender recommender;

    @Override
    protected void doGet(HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException {
        MyrrixClientConfiguration config = new MyrrixClientConfiguration();
        config.setHost(request.getServerName());
        config.setPort(request.getServerPort());
        recommender = new WebClientRecommender(config);
    }

    protected final WebClientRecommender getClientRecommender() {
        return recommender;
    }
}

Now, here we go with a SuggesterServlet:

package suggester.client.servlets;

import com.google.common.collect.Iterables;
import java.io.IOException;
import java.io.PrintWriter;
import java.net.URISyntaxException;
import java.util.List;
import javax.naming.NamingException;
import javax.servlet.ServletException;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import net.myrrix.client.translating.TranslatedRecommendedItem;
import org.apache.mahout.cf.taste.common.TasteException;

/**
 *
 * @author nilesh
 */
public class SuggesterServlet extends AbstractSuggesterServlet {

    @Override
    protected void doGet(HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException {
        super.doGet(request, response);

        String pathInfo = request.getPathInfo();
        String[] pathComponents = Iterables.toArray(SLASH.split(pathInfo), String.class);

        if (pathComponents.length == 0) {
            response.sendError(HttpServletResponse.SC_BAD_REQUEST);
            return;
        }

        try {
            List<TranslatedRecommendedItem> recommended = getClientRecommender().recommendAnonymous(getPropFilePath("proplist").toURI(), request.getParameter("type"), getHowMany(request), pathComponents);
            output(request, response, recommended);
        } catch (URISyntaxException use) {
            response.sendError(HttpServletResponse.SC_BAD_REQUEST, use.toString());
        } catch (NamingException ne) {
            response.sendError(HttpServletResponse.SC_BAD_REQUEST, ne.toString());
        } catch (TasteException te) {
            response.sendError(HttpServletResponse.SC_BAD_REQUEST, te.toString());
        }
    }

    protected final void output(HttpServletRequest request,
            ServletResponse response,
            List<TranslatedRecommendedItem> items) throws IOException {

        PrintWriter writer = response.getWriter();
        // Always print JSON
        writer.write('[');
        boolean first = true;
        for (TranslatedRecommendedItem item : items) {
            if (first) {
                first = false;
            } else {
                writer.write(',');
            }
            writer.write("[\"");
            writer.write(item.getItemID());
            writer.write("\",");
            writer.write(Float.toString(item.getValue()));
            writer.write(']');
        }
        writer.write(']');
    }
}

The reading of the prop-file and adding the string IDs to the TranslatingClientRecommender (which connects to the Myrrix REST Servlets), using that TranslatingClientRecommender inside a custom Servlet (HAS-A relationship) - this is basically how I did it. I just needed to modify Myrrix's web.xml to add my custom servlet to the list, and everything can be run inside a single application on Tomcat.

Nilesh
  • 1,222
  • 1
  • 11
  • 23