Getting Started with KitchenPC
Getting up and running with KitchenPC is hopefully an easy process. First, KitchenPC is written in C# which means it can be used with any .NET language. My examples (and future documentation) will be in C#, but feel free to use VB.Net or any other CLR compliant language. If you’re compiling from source, you’ll need to add a reference to KitchenPC.dll.
Mono should also be fine, though I haven’t done a lot of testing with this compiler and runtime. Eventually, Mono will be officially supported.
The first concept to understand is contexts.
What Is a Context?
Basically any interaction with the KitchenPC API is done through a context. A context can be thought of as a session with KitchenPC, similar to a connection with a database. At a lower level, a context provides KitchenPC with the ability to figure out who the current user is, how to save and load data, and the configuration of the environment it’s running in. In summary, a context wraps up everything KitchenPC can do into a nice package with a common interface.
At a technical level, a context is any class that implements IKPCContext. This interface has methods for things like initialization, saving data, loading data, parsing ingredient usages, etc. While you are more than welcome to implement your own IKPCContext, KitchenPC comes with two implementations out of the box! They are…
A StaticContext is, by far, the easiest way to get started with KitchenPC. This context loads all its required data from an XML file on disk. It also doesn’t save any changes to disk. Any modifications will be lost when the process ends. The most important thing to keep in mind about StaticContext is it is absolutely, positively not designed to be used in any production capacity. Do NOT use this context on your website or with any production code. It’s a playground; a way to mess around with KitchenPC to see how it works by providing a super easy solution to get up and running. This context is in no way thread safe. The performance is horrendous (pretty much any search operation will be O(n) at best!) It’s designed to hold maybe a few dozen recipes and data for a single user; any more than that, expect everything to quickly be unusable. Luckily, since the interface with any context is the same, you can start out using StaticContext and switch to another one when you’re ready.
So, how do you go about creating a StaticContext? Well, let’s jump into some code! Finally!
// Context connected to local data store var staticConfig = Configuration<StaticContext>.Build .Context(StaticContext.Configure .DataDirectory(@"C:\KitchenPC\ConsoleTest\LocalStore\") .Identity(() => new AuthIdentity(new Guid("c52a2874-bf95-4b50-9d45-a85a84309e75"), "Mike")) ) .Create(); KPCContext.Initialize(staticConfig);
Ok, so a few things going on here. First, a context is initialized by creating a configuration, which can be done fluently using Configuration<T>.Build, where T is an implementation of IKPCContext. The first method is .Context, which takes a ConfigurationBuilder<T>, which are obtainable using the static .Configure property of the context. The ConfigurationBuilder for StaticContext allows us to set a few properties relevant to this context.
The DataDirectory is a path on the local file system that contains a file called KPCData.xml. A sample of this file will be available with the source code or binaries, and will contain a few sample recipes, shopping lists, menus, etc. This will allow you to experiment with realistic data without having to create it yourself.
The .Identity method takes a Func<AuthIdentity> parameter, which can reference any function that returns an AuthIdentity (basically, a representation of a user in the KitchenPC universe). Since a context exists in static memory, it can be shared by different threads. For example, if you were building a website, you would run that above code in the Application_Start, and implement .Identity to figure out who the current user was, based on cookies or what not. In our example, we just hard code the function to return a user named Mike, which happens to exist in our sample KPCData.xml file.
At this point, staticConfig is a IConfiguration<StaticContext> object, which holds everything needed to create a context based on that configuration. Next, we just need to initialize that context, which loads necessary data into memory (in this case, from the XML file) so things like parsing and recipe modeling work. This is an expensive operation, so it should only be done once when your process starts. Eventually, you’ll be able to enable and disable individual features, which will help speed up the initialization process. For now, it takes several seconds.
We call the static method KPCContext.Initialize() and pass in our configuration. KPCContext.Initialize will create the context, initialize everything, and set the current context within your process. From this point on, KPCContext.Current will refer to our context.
You’ve probably already guessed what the other out of the box context is; DBContext. This context is able to connect KitchenPC with a SQL database, used for saving and loading data in a relational way. This context was pretty much designed around the schema of my KitchenPC website database. The DBContext class actually doesn’t do that much, because it requires an implementation of IDBAdapter to actually work. IDBAdapter tells a DBContext how to actually talk to a specific database. If you wanted to connect KitchenPC to another store, such as Mongo or memcache or your own made up thing, implementing IDBAdapter is where you’d want to start.
Luckily for you, KitchenPC also comes with an implementation of IDBAdapter as well! This implementation is called DatabaseAdapter, and is available in KitchenPC.DB.dll, which you’ll need to reference if you plan on using it. I decided to split these up into different assemblies because KitchenPC.DB.dll has a more complicated set of dependencies, and I wanted to make the core KitchenPC engine a really quick and easy download.
DatabaseAdapter is a general purpose database adapter built on NHibernate. This allows DatabaseAdapter to pretty much talk to any common SQL database, such as Microsoft SQL Server, Oracle, mySQL, or my favorite, PostgreSQL. In general, if NHibernate supports it, you can use it.
Writing a general purpose SQL adapter has its drawbacks. Since different RDMS platforms have their own features and ways of optimizing, DatabaseAdapter can only support the lowest common denominator. For example, since there’s no SQL standard implementation of full text search, recipe searching must be done with standard SQL syntax (such as using a non-indexable LIKE operator for keyword searches). Luckily, DatabaseAdapter provides various virtual methods you can override to control exactly how it interacts with your database, or you can always write your own IDBAdapter implementation which controls exactly how everything should work. I’m tempted to write a PostgreSQL specific adapter, just because it has so many cool features that I wasn’t able to port over from my website code.
Creating an instance of DBContext is slightly more complex, since we have to configure the context and the adapter. We can do the whole thing fluently as well:
// Context connected to local database var dbConfig = Configuration<DBContext>.Build .Context(DBContext.Configure .Adapter(DatabaseAdapter.Configure .DatabaseConfiguration( PostgreSQLConfiguration.PostgreSQL82 .ConnectionString(@"Server=localhost;Port=5432;User Id=Website;Password=password;Database=KPCTest") .ShowSql() ) .SearchProvider(NHSearch.Instance) ) .Identity(() => new AuthIdentity(new Guid("c52a2874-bf95-4b50-9d45-a85a84309e75"), "Mike")) ).Create();
Let’s take a look at what’s going on here. It looks similar to StaticContext, only we’re passing in a DBContext.Configure builder instead. This builder has a method called Adapter, which takes an IConfigurationBuilder object, which can be obtained through DatabaseAdapter.Configure. If you were writing your own database adapter, you’d probably also want to write your own IConfigurationBuilder.
The DatabaseAdapter configuration builder has a method called DatabaseConfiguration, which directly takes in an NHibernate IPersistenceConfigurer object. This will be familiar to anyone who has used NHiberate before. In this example, we’re creating a PostgreSQLConfiguration which will use the PostgreSQL dialect to talk with the database. This configurer also takes a connection string used to connect. The .ShowSql() method tells NHibernate to log SQL statements to the console window. You can comment that out if you don’t want to clutter your console.
The DatabaseAdapterBuilder also has a SearchProvider method. This method takes a Func<DatabaseAdapter, T> (a function that takes a DatabaseAdapter and returns a T), where T is an implementation of ISearchProvider. As I said before, recipe searching is so database dependent, I predict it will be a very common scenario to want to implement your own searching mechanism. If you wanted to use, say, Lucene.NET for searching, implementing ISearchProvider is where you’d start. However, you can use the built-in NHSearch search provider, which uses standard SQL to search for recipes. It’s not super fast, but it’ll probably do for the common website.
Once again, you’d just call KPCContext.Initialize(dbConfig); to initialize the context. This will be quite a bit slower since it needs to load in various KitchenPC data from the database and index it into memory. Again, you’d do this once every time your application starts. One nice thing about DBContext is the initialization mechanism is multi-threaded, so you can actually start to use the context right away. If you do anything that requires a fully initialized context (such as NLP or the recipe modeler), the thread will be blocked until initialization is complete.
The DatabaseAdapter obviously expects some sort of schema to exist, with default data to load. Luckily for you, DBContext is actually able to do all the database provisioning for you! However, we’ll get more into that later. In fact, this will most likely be the very next post.
So, I have a context. Now what?
Now, you can interact with KitchenPC and experiment. It doesn’t matter what context you’re using, all the code will be the same. This allows you to quickly change contexts without changing any of your application code.
For the most part, interacting with KitchenPC is done fluently using one of the five action categories.
context.Recipes allows you to interact with recipes into the database. For example, if you wanted to load the sample Weeknight Cheese Quiche recipe, as well as Berry-Good Mousse, you’d use:
var context = KPCContext.Current; var recipes = context.Recipes .Load(Recipe.FromId(new Guid("ee371336-c183-448d-889c-d6b1f767bf59"))) // Weeknight Cheese Quiche .Load(Recipe.FromId(new Guid("e6ab278f-5f4d-4249-ab7d-8f66af044265"))) // Berry-Good Mousse .WithMethod .WithUserRating .List();
Notice the Load action takes a Recipe object, which is used globally within KitchenPC to represent a recipe. Perhaps these Recipe objects came from some other piece of data, such as a user’s menu. In this case, we’re just creating them based on their raw ID in the database.
Also, notice how we can chain up multiple recipes to load as well. You can add as many calls to .Load as you want, and it will load all the necessary data in a single database call. The Fluent interface behind KitchenPC allows us to batch up complicated calls into single database operations.
There’s also a few properties. The WithMethod property indicates we want to load the recipe methods as well (which can get long), and WithUserRating JOINs in the user’s rating data to see if they’ve rated the recipe before. At the end of the Fluent expression, we call .List() which performs the database call (or, digs through the XML file) and returns an IList<> of Recipes.
It’s important to keep in mind absolutely nothing is done *until* that .List method is called, so you can programatically manipulate that query or add more conditions at runtime before materializing the data.
Now, suppose we wanted to create a menu for the current user. The Menus action lets us manipulate menus. For example, if we wanted to add our quiche recipe to a new menu called My New Menu, we’d simply do:
var result = context.Menus .Create .WithTitle("My New Menu") .AddRecipe(Recipe.FromId(new Guid("ee371336-c183-448d-889c-d6b1f767bf59"))) // Weeknight Cheese Quiche .Commit();
The Create action creates a new menu. We then set a title, and add a recipe. AddRecipe can of course be called any number of times. When we call .Commit(), the menu is created and a result is returned. This result has the ID of the newly created menu.
Oops, we forgot to add our Mousse recipe as well! Well, let’s update the menu!
context.Menus .Update(Menu.FromId(result.NewMenuId.Value)) .Add(Recipe.FromId(new Guid("e6ab278f-5f4d-4249-ab7d-8f66af044265"))) // Berry-Good Mousse .Rename("Renamed Menu") .Commit();
Here, we’re updating an existing menu and adding another recipe to it. Oh, while we’re at it, let’s rename the menu to “Renamed Menu” just for fun.
Shopping Lists are a fun one. It really ties together a lot of the raw power behind KitchenPC. We can aggregate recipes, add arbitrary ingredients in, and even throw in any ol’ random thing like paper towels. KitchenPC will automatically combine like ingredients, convert amounts and units, etc.
KitchenPC can track multiple shopping lists for a single user, but all users have a default shopping list which can be referred to using ShoppingList.Default. Let’s add a bunch of items to our default shopping list!
context.ShoppingLists .Update(ShoppingList.Default) .AddItems(a => a .AddRecipe(Recipe.FromId(new Guid("ee371336-c183-448d-889c-d6b1f767bf59"))) // Weeknight Cheese Quiche .AddIngredient(Ingredient.FromId(new Guid("948aeda5-ffff-41bd-af4e-71d1c740db76"))) // Eggs .AddItem("12 bananas") .AddItem("paper towels") ) .Commit();
In this code, we’re updating an existing list (ShoppingList.Default), and adding a bunch of items. First, we’re adding in a recipe. It’s our quiche recipe again. Next, we’re throwing in eggs, which is probably already in the quiche so those ingredients will be combined. Next, we’re adding in “12 bananas”. This string will be parsed using natural language parsing, and converted into a normalized ingredient and amount. In other words, if I added 3 more bananas, I’d have 15 bananas in my shopping list. We also add in “paper towels” which can not be parsed using NLP, so this item will just be added as text.
This method will return a fully normalized ShoppingList object, which will allow you to get the item IDs for each item in the list. The item IDs are important for when you want to update an existing item in the shopping list:
Let’s update two imaginary items in our list:
context.ShoppingLists .Update(ShoppingList.Default) .UpdateItem(ShoppingListItem.FromId("d7506487-83e3-4893-ade9-aa6e685810fe"), u => u .CrossOut ) .UpdateItem(ShoppingListItem.FromId("4e479943-0c21-4624-a3b9-1f63722cee8f"), u => u .NewAmount(new Amount(3, Units.Cup)) ) .Commit();
Here, we’re crossing out one item (this allows you to create interfaces where shopping list items can be crossed out without actually removing them) and setting the amount of another item to 3 cups. You can only change the amounts or crossed-out state using UpdateItem. If you want to actually change the text or ingredient itself, you have to remove the old one and add a new one.
Remember the queue? Well, it’s back. The queue provides the user with a way to store arbitrary recipes without having to store them in a menu.
Adding recipes to the queue is easy:
context.Queue .Enqueue .Recipe(Recipe.FromId(new Guid("ee371336-c183-448d-889c-d6b1f767bf59"))) // Weeknight Cheese Quiche .Recipe(Recipe.FromId(new Guid("e6ab278f-5f4d-4249-ab7d-8f66af044265"))) // Berry-Good Mousse .Commit();
As is removing a recipe:
context.Queue .Dequeue .Recipe(Recipe.FromId(new Guid("e6ab278f-5f4d-4249-ab7d-8f66af044265"))) // Berry-Good Mousse .Commit();
Bye-bye Mousse. Want to clear the entire queue?
context.Queue .Dequeue .All .Commit();
Ah, the modeler. Also known as What Can I Make?. I like the feature, though it never took off on my site. Still, it’s pretty cool technology that lets you figure out what recipes you can make based on what ingredients you have available.
The modeler is extremely complicated, so I hope to devote future blog posts to it. However, it runs under the context of a modeling session. A modeling session describes the goals of the user; for example, what ingredients they have, what they don’t like, what recipes not to repeat, etc. You can also use the modeler under an anonymous context, which means We don’t know anything about this user:
var results = context.Modeler .WithAnonymous .NumRecipes(5) .Generate();
This just means give me five recipes. The results will contain a list of recipe IDs and a score indicating how efficient the set is. You can set the .Scale property if you want to control if sets should be more efficient or higher rated. You can also compile the results, which means load in the full recipe briefs and create a normalized ingredient set containing all the ingredients and amount you would need to create those recipes. This is how I was able to draw real time shopping lists on my web site.
var results = context.Modeler .WithAnonymous .NumRecipes(5) .Compile();
This returns a CompiledModel object, which has recipe and aggregation data. I’ll get more into this in a separate post.
Now, instead of using WithAnonymous, you can also use WithProfile. WithProfile takes an IUserProfile implementation which tells the modeler about your user. On my website, I had an implementation that loaded their favorite tags, their blacklist, their pantry, all that stuff. You could, of course, implement this yourself based on your needs. However, you can also build your own profile fluently.
var bananas = context.ParseIngredientUsage("12 bananas"); var carrots = context.ParseIngredientUsage("carrots"); var results = context.Modeler .WithProfile(p => p .AddBlacklistedIngredient(Ingredient.FromId(new Guid("948aeda5-ffff-41bd-af4e-71d1c740db76"))) // No Eggs! .FavoriteTags(RecipeTag.Dessert | RecipeTag.GlutenFree) // I love gluten free desserts! .AddPantryItem(bananas.Usage) // I have 12 bananas .AddPantryItem(carrots.Usage) // I have carrots .NumRecipes(5) .Compile();
First, we’re using the NLP engine to parse “12 bananas” and “carrots” into normalized ingredient usages so we can use them to construct a pantry.
Next, we create a profile for this session. First, we’ll blacklist eggs. The modeler will not return any recipe that calls for eggs. We indicate we like desserts and gluten free recipes, so the modeler will score those recipes higher. We’ll also indicate we have 12 bananas and an unspecified amount of carrots. The modeler will attempt to efficiently use those ingredients, then compile them into a set of results for us.
I have no idea if my sample database will come up with anything for that query, but you get the idea.
Feel free to mess around with these examples and come up with your own scenarios. This post hasn’t even scratched the surface of what all is possible. I hope to post many more articles focusing on individual subjects, so please email me if you have any suggestions on topics. If you have any ideas, or find things that blow up spectacularly, let me know! Have fun!