Building Bot. Part Four: Putting everything together

This is the fourth post from bot series. Now I have POJOs, codecs and bot skeleton. We need to figure out how to find recipes by ingredients in a flexible way. I came up with idea that on each request, the bot will return only one recipe. A user could click on the ingredient which he wants to exclude (doesn’t have or just doesn’t want) and get a new recipe which matches updated criteria. After some tests with different approaches, I found the convenient solution: use full-text search on ingredients. MongoDB has search syntax which is similar to Google. You enter terms which you’re looking for and exclude terms with “-“. Perfect! Let’s create a text index for ingredients.

db.recipes.ensureIndex({"ingredients.name":"text"},{ default_language:"russian"});

We want to get the most relevant recipe, that’s why we need to sort it by relevance. It is possible with adding textScore from metadata to result using projections and sort on this field. Our query should look like this:

db.recipes.find({ $text: { $search: "beef potato -carrot"}}, {score: {$meta: "textScore"}}).sort({score: {$meta: "textScore"}}).limit(1).pretty();

In Java in will look like this:

public Recipe findRecipes(String ingredients) {
FindIterable<Recipe> recipes = this.recipes.find(new Document(“$text”, new Document(“$search”, ingredients)));
Recipe answer = recipes.projection(new Document(“score”, new Document(“$meta”, “textScore”)))
.sort(new Document(“score”, new Document(“$meta”, “textScore”))).first();return answer;

return answer;
}

The rest of queries are trivial. You could find it on GitHub

Now I can search recipes by ingredients. But in UI part the bot should handle clicks somehow. Telegram API provides inline keyboard capability. Here is how I’ll exclude ingredients. Don’t mind the Russian language, it’s just how it looks.

screenshot

Here is the code:

private InlineKeyboardMarkup createIngredientsKeyboard(List<Ingredient> ingredients){
  InlineKeyboardMarkup keyboardMarkup = new InlineKeyboardMarkup();
  List<List<InlineKeyboardButton>> buttons = new ArrayList<>();
  for (int i = 0; i < ingredients.size(); i++) {
    Ingredient ing = ingredients.get(i);
    InlineKeyboardButton btn = new InlineKeyboardButton();
    btn.setText("\u274C "+ing.getName());
    btn.setCallbackData(String.valueOf(i));
    buttons.add(Arrays.asList(btn));
  }
  keyboardMarkup.setKeyboard(buttons);
  return keyboardMarkup;
}
private void sendKeyboardResponse(String r, String chatID, InlineKeyboardMarkup keyboard){
  SendMessage sendMessageRequest = new SendMessage();
  sendMessageRequest.setChatId(chatID);
  sendMessageRequest.setText(r);
  sendMessageRequest.setReplyMarkup(keyboard);
  sendMessageRequest.enableHtml(true);
  try{
    sendMessage(sendMessageRequest);
  } catch (TelegramApiException e) {
    mongo.getFeedbackStorage().saveFeedback(e.toString());
  }
}

Note the btn.setCallbackData method. When the button is pressed, you get the new update and could get it with update.hasCallbackQuery() and update.getCallbackQuery() in your onUpdateReceived method (see the previous post). The bad news is that callback query could be 64 bytes maximum. That is why I save the index of an ingredient in it and was forced to use sessions to save current query and indexes of ingredients for each user.

So, this is pretty much everything. You could find the complete source code on GitHub and try the bot on StoreBot