<?xml version="1.0" encoding="utf-8"?><feed xmlns="http://www.w3.org/2005/Atom" ><generator uri="https://jekyllrb.com/" version="4.4.1">Jekyll</generator><link href="https://mehmetyksl.com/feed.xml" rel="self" type="application/atom+xml" /><link href="https://mehmetyksl.com/" rel="alternate" type="text/html" /><updated>2026-08-29T16:21:08+03:00</updated><id>https://mehmetyksl.com/feed.xml</id><title type="html">Cross Pollinations</title><subtitle>Notes on programming and math.</subtitle><entry><title type="html">Program Synthesis</title><link href="https://mehmetyksl.com/2025/03/16/program-synthesis.html" rel="alternate" type="text/html" title="Program Synthesis" /><published>2025-03-16T00:00:00+03:00</published><updated>2025-03-16T00:00:00+03:00</updated><id>https://mehmetyksl.com/2025/03/16/program-synthesis</id><content type="html" xml:base="https://mehmetyksl.com/2025/03/16/program-synthesis.html"><![CDATA[<h2 id="introduction">Introduction</h2>

<p>The gist of it is generating programs symbolically instead of using a neural network. It is a completely different approach to program generation and comes with different trade-offs.</p>

<p>There is no hallucination, no syntax errors and guaranteed to be type correct if you want. You can give it a library for it to use, give any kind of hint or constraint. This could be assertions like <code class="language-plaintext highlighter-rouge">assert f(2) = 4</code> or <code class="language-plaintext highlighter-rouge">assert f(x) = IsPrime(x)</code> for all inputs <code class="language-plaintext highlighter-rouge">x</code>. You can say “only use 100 steps to calculate the result” or “only use 100 bytes of memory.”</p>

<h2 id="a-simple-implementation">A Simple Implementation</h2>

<p>We can enumerate every program until you find the “correct” one. You can define correctness however you want.</p>

<p>One way is to encode a specification. Here’s an example:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>method Max(a: int, b: int): int
  ensures Max(a, b) &gt;= a &amp;&amp; Max(a, b) &gt;= b
  ensures Max(a, b) == a || Max(a, b) == b
{
  TODO()
}
</code></pre></div></div>

<p><code class="language-plaintext highlighter-rouge">ensures Max(a, b) &gt;= a &amp;&amp; Max(a, b) &gt;= b</code> tells us that the return value must be greater than or equal to both <code class="language-plaintext highlighter-rouge">a</code> and <code class="language-plaintext highlighter-rouge">b</code>. <code class="language-plaintext highlighter-rouge">ensures Max(a, b) == a || Max(a, b) == b</code> specifies that the result must either be <code class="language-plaintext highlighter-rouge">a</code> or <code class="language-plaintext highlighter-rouge">b</code>. If we didn’t have the first condition, just returning <code class="language-plaintext highlighter-rouge">a</code> would be correct; and <code class="language-plaintext highlighter-rouge">a + b</code> would be correct if we didn’t have the second condition.</p>

<p>Another option is to provide examples:</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">assert</span> <span class="nc">Max</span><span class="p">(</span><span class="mi">1</span><span class="p">,</span> <span class="mi">2</span><span class="p">)</span> <span class="o">==</span> <span class="mi">2</span>
<span class="k">assert</span> <span class="nc">Max</span><span class="p">(</span><span class="o">-</span><span class="mi">1</span><span class="p">,</span> <span class="mi">0</span><span class="p">)</span> <span class="o">==</span> <span class="mi">0</span>
</code></pre></div></div>

<p>I like this one better. It’s simpler than doing formal reasoning. It feels more interactive.  <a href="https://youtu.be/PhJUH136SIY?si=rClgua340pnAxTQ-">This video</a> is a fantastic demo of what I mean. In it, the presenter tries to synthesize a program to extract users’ first and last names from emails.</p>

<p>It is not either or. We can run the type checker and run it against the examples. Victor Taelin’s <a href="https://x.com/VictorTaelin/status/1881392823246729640">SupGen</a> uses both approaches. There’s also Smyth, which has interactive examples on their <a href="https://uchicago-pl.github.io/smyth/">website</a> you can play with. I think they’re planning on integrating it into the <a href="https://hazel.org/">Hazel</a> programming language. I highly recommend checking it out as well.</p>

<p>Although their mechanism doesn’t look like our brains at all, they generalize well from small number of examples. AIs that generalize well and interpretable might look like these instead of deep neural networks.</p>

<p>How do we enumerate programs?</p>

<h2 id="enumerating-programs">Enumerating Programs</h2>

<p>Let’s simplify the problem as much as possible but not too much, as we want to capture the essential complexity. The following language seems like a perfect candidate:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>expr =
	| 0
	| S(expr)
	| expr * expr
</code></pre></div></div>

<p>It has natural numbers. Represented as in <a href="https://en.wikipedia.org/wiki/Peano_axioms">Peano arithmetic</a>. <code class="language-plaintext highlighter-rouge">S(0)</code> represents 1, <code class="language-plaintext highlighter-rouge">S(S(0))</code> represents 2, etc. We also have multiplication expressions because otherwise, we could just enumerate like <code class="language-plaintext highlighter-rouge">0, S(0), S(S(0)), ...</code> and that’s not interesting at all. With multiplication however, simply enumerating like that won’t work because we’d never generate expressions like <code class="language-plaintext highlighter-rouge">0 * S(0)</code>. In a sense, we have to be fair to each expression type, sometimes generating one type and sometimes another.</p>

<p>So we have a search problem where:</p>

<ol>
  <li>We have to make a decision at each step e.g. after <code class="language-plaintext highlighter-rouge">S(0) * 0</code> what’s the next expression?</li>
  <li>There is a hierarchy between things we search for. Every <code class="language-plaintext highlighter-rouge">S(expr * expr)</code> is a <code class="language-plaintext highlighter-rouge">S(expr)</code> and that in turn is an <code class="language-plaintext highlighter-rouge">expr</code> .</li>
</ol>

<p>A tree structure is ideal for visualizing this problem. Now, you might be wondering, how should we connect the nodes? And what should be the root? The grammar already tells us. <code class="language-plaintext highlighter-rouge">expr</code> is the root. <code class="language-plaintext highlighter-rouge">0</code>, <code class="language-plaintext highlighter-rouge">S(expr)</code>, <code class="language-plaintext highlighter-rouge">expr * expr</code> are the children. <code class="language-plaintext highlighter-rouge">S(0)</code>, <code class="language-plaintext highlighter-rouge">S(expr)</code>, <code class="language-plaintext highlighter-rouge">S(expr * expr)</code> are the children of <code class="language-plaintext highlighter-rouge">S(expr)</code> and so on.</p>

<p><code class="language-plaintext highlighter-rouge">expr</code> is like <code class="language-plaintext highlighter-rouge">TODO</code> expressions. It’s a placeholder for any expression. Smyth calls these holes and represent them with <code class="language-plaintext highlighter-rouge">??</code>. I’ll use <code class="language-plaintext highlighter-rouge">?</code> instead.</p>

<p>Here’s a visualization:</p>

<p><img src="/images/d2.webp" alt="" /></p>

<p>It mirrors the grammar. Everywhere we see a <code class="language-plaintext highlighter-rouge">?</code>, we replace it with <code class="language-plaintext highlighter-rouge">0</code>, <code class="language-plaintext highlighter-rouge">S(?)</code>, and <code class="language-plaintext highlighter-rouge">? * ?</code>. It also looks like we’re pattern matching against the program and becoming more specific at each level:</p>

<div class="language-rust highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">match</span> <span class="n">x</span> <span class="p">{</span>
	<span class="mi">0</span> <span class="k">=&gt;</span> <span class="o">..</span>
	<span class="nf">S</span><span class="p">(</span><span class="n">y</span><span class="p">)</span> <span class="k">=&gt;</span> <span class="k">match</span> <span class="n">y</span> <span class="p">{</span>
		<span class="nf">S</span><span class="p">(</span><span class="mi">0</span><span class="p">)</span> <span class="k">=&gt;</span> <span class="o">..</span>
		<span class="nf">S</span><span class="p">(</span><span class="nf">S</span><span class="p">(</span><span class="n">z</span><span class="p">))</span> <span class="k">=&gt;</span> <span class="o">..</span>
		<span class="nf">S</span><span class="p">(</span><span class="nf">Mul</span><span class="p">(</span><span class="n">z</span><span class="p">,</span> <span class="n">k</span><span class="p">))</span> <span class="k">=&gt;</span> <span class="o">..</span>
	<span class="p">}</span>
	<span class="nf">Mul</span><span class="p">(</span><span class="n">y</span><span class="p">,</span> <span class="n">z</span><span class="p">)</span> <span class="k">=&gt;</span> <span class="k">match</span> <span class="p">(</span><span class="n">y</span><span class="p">,</span> <span class="n">z</span><span class="p">)</span> <span class="p">{</span>
		<span class="p">(</span><span class="mi">0</span><span class="p">,</span> <span class="mi">0</span><span class="p">)</span> <span class="k">=&gt;</span> <span class="o">..</span>
		<span class="p">(</span><span class="mi">0</span><span class="p">,</span> <span class="nf">S</span><span class="p">(</span><span class="n">k</span><span class="p">))</span> <span class="k">=&gt;</span> <span class="o">..</span>
		<span class="o">..</span>
	<span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>

<p>As you can see, the depth of this tree is infinite, so we can’t really use DFS. BFS is perfect for this use case because it explores the tree level by level. Since the programs grow from short to long, if there is a simpler correct program, we’ll find it first.</p>

<h3 id="implementing-it-in-code">Implementing It in Code</h3>

<p>Let’s first just scaffold the BFS algorithm:</p>

<div class="language-js highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">function</span> <span class="nf">enumerate</span><span class="p">(</span><span class="nx">program</span><span class="p">)</span> <span class="p">{</span>
  <span class="kd">const</span> <span class="nx">queue</span> <span class="o">=</span> <span class="p">[</span><span class="nx">program</span><span class="p">];</span>
  <span class="k">while </span><span class="p">(</span><span class="nx">queue</span><span class="p">.</span><span class="nx">length</span> <span class="o">!==</span> <span class="mi">0</span><span class="p">)</span> <span class="p">{</span>
    <span class="kd">const</span> <span class="nx">expr</span> <span class="o">=</span> <span class="nx">queue</span><span class="p">.</span><span class="nf">shift</span><span class="p">();</span>
    <span class="nx">console</span><span class="p">.</span><span class="nf">log</span><span class="p">(</span><span class="nf">showExpression</span><span class="p">(</span><span class="nx">expr</span><span class="p">));</span>

    <span class="c1">//TODO: find the children and add them to the queue</span>
  <span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>

<p>There’s nothing new here; we start our queue with the root and print the nodes in the queue until it’s empty. The core of the algorithm is finding the children of a node. Let’s define an <code class="language-plaintext highlighter-rouge">expand</code> function that, given a node, replaces each hole with possible expression types as we discussed earlier.</p>

<div class="language-js highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// Define helpers to construct the expressions so that it's not crowded.</span>
<span class="kd">const</span> <span class="nx">hole</span> <span class="o">=</span> <span class="p">()</span> <span class="o">=&gt;</span> <span class="p">({</span> <span class="na">type</span><span class="p">:</span> <span class="dl">"</span><span class="s2">hole</span><span class="dl">"</span> <span class="p">});</span>
<span class="kd">const</span> <span class="nx">O</span> <span class="o">=</span> <span class="p">()</span> <span class="o">=&gt;</span> <span class="p">({</span> <span class="na">type</span><span class="p">:</span> <span class="dl">"</span><span class="s2">zero</span><span class="dl">"</span> <span class="p">});</span>
<span class="kd">const</span> <span class="nx">S</span> <span class="o">=</span> <span class="p">(</span><span class="nx">prev</span><span class="p">)</span> <span class="o">=&gt;</span> <span class="p">({</span> <span class="na">type</span><span class="p">:</span> <span class="dl">"</span><span class="s2">succ</span><span class="dl">"</span><span class="p">,</span> <span class="nx">prev</span> <span class="p">});</span>
<span class="kd">const</span> <span class="nx">mul</span> <span class="o">=</span> <span class="p">(</span><span class="nx">left</span><span class="p">,</span> <span class="nx">right</span><span class="p">)</span> <span class="o">=&gt;</span> <span class="p">({</span>
  <span class="na">type</span><span class="p">:</span> <span class="dl">"</span><span class="s2">mul</span><span class="dl">"</span><span class="p">,</span>
  <span class="nx">left</span><span class="p">,</span>
  <span class="nx">right</span><span class="p">,</span>
<span class="p">});</span>

<span class="kd">function</span> <span class="nf">expand</span><span class="p">(</span><span class="nx">expr</span><span class="p">)</span> <span class="p">{</span>
  <span class="k">switch </span><span class="p">(</span><span class="nx">expr</span><span class="p">.</span><span class="nx">type</span><span class="p">)</span> <span class="p">{</span>
    <span class="k">case</span> <span class="dl">"</span><span class="s2">zero</span><span class="dl">"</span><span class="p">:</span>
      <span class="k">return</span> <span class="p">[];</span>
    <span class="k">case</span> <span class="dl">"</span><span class="s2">hole</span><span class="dl">"</span><span class="p">:</span>
      <span class="c1">// [0, S(?), ? * ?]</span>
      <span class="k">return</span> <span class="p">[</span><span class="nc">O</span><span class="p">(),</span> <span class="nc">S</span><span class="p">(</span><span class="nf">hole</span><span class="p">()),</span> <span class="nf">mul</span><span class="p">(</span><span class="nf">hole</span><span class="p">(),</span> <span class="nf">hole</span><span class="p">())];</span>
  <span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>

<p>Those first two cases were clear-cut. But now we get to the other two: <code class="language-plaintext highlighter-rouge">succ</code> and <code class="language-plaintext highlighter-rouge">mul</code>. Things start to get a little trickier here because these expressions are unbounded. They can get as as complicated as you want. We could have something like <code class="language-plaintext highlighter-rouge">S(S(S(S(?))))</code> or <code class="language-plaintext highlighter-rouge">(? * 0) * (0 * S(?))</code>. Now, one way to handle this would be to just walk through the expression, the AST, and replace any <code class="language-plaintext highlighter-rouge">?</code> we find. But that sounds difficult because we need to copy the tree for every replacement.</p>

<p>Since we’re already dealing with a tree structure, a more natural approach is to use recursion. We can simply call our <code class="language-plaintext highlighter-rouge">expand</code> function on each part of the expression. This way, we’ll eventually hit those base cases we already coded up. Let’s see how this works with <code class="language-plaintext highlighter-rouge">S(S(?))</code> as an example:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>expand(S(S(?)) 
     |
expand(S(?))
     |
expand(?) == [0, S(?), ? * ?]
</code></pre></div></div>

<p>Now we do the reverse: we take these basic expressions and construct our original expression. In this case, we wrap each of those expressions with <code class="language-plaintext highlighter-rouge">S</code>:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>[0, S(?), ? * ?]
       |
[S(0), S(S(?)), S(? * ?)]
       |
[S(S(0)), S(S(S(?))), S(S(? * ?))]
</code></pre></div></div>

<p>Translating this into code:</p>

<div class="language-diff highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">function expand(expr) {
</span>  switch (expr.type) {
    case "zero":
      return [];
    case "hole":
	    // [0, S(?), ? * ?]
      return [O(), S(hole()), mul(hole(), hole())];
<span class="gi">+   case "succ": {
+	    const result = [];
+	    for(const prev of expand(expr.prev)) {
+		    result.push(S(prev))
+	    }
+	    return result;
</span>    }
  }
}
</code></pre></div></div>

<p>This is just a map operation so we could do it more succinctly like this:</p>

<div class="language-diff highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">function expand(expr) {
</span>  switch (expr.type) {
    case "zero":
      return [];
    case "hole":
	    // [0, S(?), ? * ?]
      return [O(), S(hole()), mul(hole(), hole())];
<span class="gi">+   case "succ": {
+	    return expand(expr.prev).map(S);
+   }
</span>  }
}
</code></pre></div></div>

<p><code class="language-plaintext highlighter-rouge">? * ?</code> expression is the same but since we have two sub-expressions, we have nested for loops:</p>

<div class="language-diff highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">function expand(expr) {
</span>  switch (expr.type) {
    case "zero":
      return [];
    case "hole":
      // [0, S(?), ? * ?]
      return [O(), S(hole()), mul(hole(), hole())];
    case "succ": {
      return expand(expr.prev).map(S);
    }
<span class="gi">+   case "mul": {
+     const result = [];
+     for (const left of expand(expr.left)) {
+       for (const right of expand(expr.right)) {
+         result.push(mul(left, right));
+       }
+     }
+     return result;
+   }
</span>  }
}
<span class="err">
</span></code></pre></div></div>

<p>Now we can use this to find the children in our <code class="language-plaintext highlighter-rouge">enumerate</code> function:</p>

<div class="language-js highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">function</span> <span class="nf">enumerate</span><span class="p">(</span><span class="nx">program</span><span class="p">)</span> <span class="p">{</span>
  <span class="kd">const</span> <span class="nx">queue</span> <span class="o">=</span> <span class="p">[</span><span class="nx">program</span><span class="p">];</span>
  <span class="k">while </span><span class="p">(</span><span class="nx">queue</span><span class="p">.</span><span class="nx">length</span> <span class="o">!==</span> <span class="mi">0</span><span class="p">)</span> <span class="p">{</span>
    <span class="kd">const</span> <span class="nx">expr</span> <span class="o">=</span> <span class="nx">queue</span><span class="p">.</span><span class="nf">shift</span><span class="p">();</span>
    <span class="nx">console</span><span class="p">.</span><span class="nf">log</span><span class="p">(</span><span class="nf">showExpression</span><span class="p">(</span><span class="nx">expr</span><span class="p">));</span>

    <span class="k">for </span><span class="p">(</span><span class="kd">const</span> <span class="nx">child</span> <span class="k">of</span> <span class="nf">expand</span><span class="p">(</span><span class="nx">expr</span><span class="p">))</span> <span class="p">{</span>
      <span class="nx">queue</span><span class="p">.</span><span class="nf">push</span><span class="p">(</span><span class="nx">child</span><span class="p">);</span>
    <span class="p">}</span>
  <span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>

<p>and lastly the <code class="language-plaintext highlighter-rouge">showExpression</code> function:</p>

<div class="language-js highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">function</span> <span class="nf">showExpression</span><span class="p">(</span><span class="nx">expr</span><span class="p">:</span> <span class="nx">Expression</span><span class="p">):</span> <span class="nx">string</span> <span class="p">{</span>
  <span class="k">switch </span><span class="p">(</span><span class="nx">expr</span><span class="p">.</span><span class="nx">type</span><span class="p">)</span> <span class="p">{</span>
    <span class="k">case</span> <span class="dl">"</span><span class="s2">hole</span><span class="dl">"</span><span class="p">:</span>
      <span class="k">return</span> <span class="dl">"</span><span class="s2">?</span><span class="dl">"</span><span class="p">;</span>
    <span class="k">case</span> <span class="dl">"</span><span class="s2">zero</span><span class="dl">"</span><span class="p">:</span>
      <span class="k">return</span> <span class="dl">"</span><span class="s2">0</span><span class="dl">"</span><span class="p">;</span>
    <span class="k">case</span> <span class="dl">"</span><span class="s2">succ</span><span class="dl">"</span><span class="p">:</span>
      <span class="k">return</span> <span class="s2">`S(</span><span class="p">${</span><span class="nf">showExpression</span><span class="p">(</span><span class="nx">expr</span><span class="p">.</span><span class="nx">prev</span><span class="p">)}</span><span class="s2">)`</span><span class="p">;</span>
    <span class="k">case</span> <span class="dl">"</span><span class="s2">mul</span><span class="dl">"</span><span class="p">:</span>
      <span class="k">return</span> <span class="s2">`(</span><span class="p">${</span><span class="nf">showExpression</span><span class="p">(</span><span class="nx">expr</span><span class="p">.</span><span class="nx">left</span><span class="p">)}</span><span class="s2"> * </span><span class="p">${</span><span class="nf">showExpression</span><span class="p">(</span><span class="nx">expr</span><span class="p">.</span><span class="nx">right</span><span class="p">)}</span><span class="s2">)`</span><span class="p">;</span>
  <span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>

<p>If you run <code class="language-plaintext highlighter-rouge">enumerate</code> function, it’ll output as follows:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>?
0
S(?)
(? * ?)
S(0)
S(S(?))
S((? * ?))
(0 * 0)
(0 * S(?))
(0 * (? * ?))
... and so on
</code></pre></div></div>

<p>If you look at the tree again, you’ll notice that only the complete nodes are actual programs. To print only those, we can track that information in the queue:</p>

<div class="language-diff highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">function enumerate(program: Expression) {
</span><span class="gd">- const queue = [program];
</span><span class="gi">+ const queue = [["incomplete", program]];
</span>  while (queue.length !== 0) {
<span class="gd">-   const expr = queue.shift()!;
-   console.log(showExpression(expr));
</span><span class="gi">+   const [type, expr] = queue.shift()!;
+   if (type === "complete") {
+     console.log(showExpression(expr));
+   }
</span>    for (const [i, child] of expand(expr).entries()) {
      // Mark it as complete if it's the first child.
<span class="gi">+     const type = i === 0 ? "complete" : "incomplete";
</span>      queue.push([type, child]);
    }
  }
}
<span class="err">
</span></code></pre></div></div>

<p>Now we only get complete programs:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>0
S(0)
(0 * 0)
S(S(0))
S((0 * 0))
(S(0) * S(0))
(S(0) * (0 * 0))
((0 * 0) * S(0))
((0 * 0) * (0 * 0))
S(S(S(0)))
...and so on

</code></pre></div></div>

<p>Although it’s a nice and short solution, its memory consumption grows exponentially. I don’t know a way to get around that. Another solution would be storing the generated programs in an array and building the next program using their combinations. For example:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>0
S(0), 0 * 0
S(S(0)), S(0 * 0), 0 * S(0), 0 * (0 * 0), S(0) * 0, ...
...and so on
</code></pre></div></div>

<p>We don’t generate incomplete programs, but the growth is still exponential. However, with type information and examples, we’ll be able to prune most of the branches because the number of incorrect implementations is much larger than the number of correct ones.</p>

<h2 id="synthesizing-a-specific-program">Synthesizing a Specific Program</h2>

<p>Now that we can enumerate programs, we can search for the correct one. Since the language is simple, there is not much anything we can do. Let’s just say, we stop when we find the first program that evaluates to <code class="language-plaintext highlighter-rouge">15</code>.</p>

<p>We need an <code class="language-plaintext highlighter-rouge">evaluate</code> function:</p>
<div class="language-ts highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">function</span> <span class="nf">evaluate</span><span class="p">(</span><span class="nx">expr</span><span class="p">:</span> <span class="nx">Expression</span><span class="p">):</span> <span class="kr">number</span> <span class="o">|</span> <span class="kc">null</span> <span class="p">{</span>
  <span class="k">switch </span><span class="p">(</span><span class="nx">expr</span><span class="p">.</span><span class="kd">type</span><span class="p">)</span> <span class="p">{</span>
    <span class="k">case</span> <span class="dl">"</span><span class="s2">hole</span><span class="dl">"</span><span class="p">:</span>
      <span class="k">return</span> <span class="kc">null</span><span class="p">;</span>
    <span class="k">case</span> <span class="dl">"</span><span class="s2">zero</span><span class="dl">"</span><span class="p">:</span>
      <span class="k">return</span> <span class="mi">0</span><span class="p">;</span>
    <span class="k">case</span> <span class="dl">"</span><span class="s2">succ</span><span class="dl">"</span><span class="p">:</span>
      <span class="k">return</span> <span class="mi">1</span> <span class="o">+</span> <span class="nf">evaluate</span><span class="p">(</span><span class="nx">expr</span><span class="p">.</span><span class="nx">prev</span><span class="p">)</span>
    <span class="k">case</span> <span class="dl">"</span><span class="s2">mul</span><span class="dl">"</span><span class="p">:</span> <span class="p">{</span>
	  <span class="kd">const</span> <span class="nx">left</span> <span class="o">=</span> <span class="nf">evaluate</span><span class="p">(</span><span class="nx">expr</span><span class="p">.</span><span class="nx">left</span><span class="p">);</span>
	  <span class="kd">const</span> <span class="nx">right</span> <span class="o">=</span> <span class="nf">evaluate</span><span class="p">(</span><span class="nx">expr</span><span class="p">.</span><span class="nx">right</span><span class="p">);</span>
	  <span class="k">if </span><span class="p">(</span><span class="nx">left</span> <span class="o">===</span> <span class="kc">null</span> <span class="o">||</span> <span class="nx">right</span> <span class="o">===</span> <span class="kc">null</span><span class="p">)</span> <span class="k">return</span> <span class="kc">null</span><span class="p">;</span>
	  <span class="k">return</span> <span class="nx">left</span> <span class="o">+</span> <span class="nx">right</span><span class="p">;</span>
    <span class="p">}</span>
  <span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>

<p>We could try to define partial evaluation for expressions that contains holes but let’s stick with this simple solution instead. Every time we see <code class="language-plaintext highlighter-rouge">null</code> we can ignore the program.</p>

<p>Now the synthesizer just takes a number that correct program must be evaluated to and return the first one. We can take the <code class="language-plaintext highlighter-rouge">enumerate</code> and modify it a little:</p>
<div class="language-ts highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">function</span> <span class="nf">enumerate</span><span class="p">(</span><span class="nx">result</span><span class="p">:</span> <span class="kr">number</span><span class="p">)</span> <span class="p">{</span>
  <span class="kd">const</span> <span class="nx">queue</span> <span class="o">=</span> <span class="p">[{</span> <span class="na">type</span><span class="p">:</span> <span class="dl">"</span><span class="s2">Hole</span><span class="dl">"</span> <span class="p">}];</span>
  <span class="k">while </span><span class="p">(</span><span class="nx">queue</span><span class="p">.</span><span class="nx">length</span> <span class="o">!==</span> <span class="mi">0</span><span class="p">)</span> <span class="p">{</span>
    <span class="kd">const</span> <span class="nx">expr</span> <span class="o">=</span> <span class="nx">queue</span><span class="p">.</span><span class="nf">shift</span><span class="p">()</span><span class="o">!</span><span class="p">;</span>
    <span class="k">if </span><span class="p">(</span><span class="nf">evaluate</span><span class="p">(</span><span class="nx">expr</span><span class="p">)</span> <span class="o">===</span> <span class="nx">result</span><span class="p">)</span> <span class="p">{</span>
      <span class="k">return</span> <span class="nx">expr</span><span class="p">;</span>
    <span class="p">}</span>
    <span class="k">for </span><span class="p">(</span><span class="kd">const</span> <span class="nx">child</span> <span class="k">of</span> <span class="nf">expand</span><span class="p">(</span><span class="nx">expr</span><span class="p">))</span> <span class="p">{</span>
      <span class="nx">queue</span><span class="p">.</span><span class="nf">push</span><span class="p">(</span><span class="nx">child</span><span class="p">);</span>
    <span class="p">}</span>
  <span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>

<p>Let’s say the correct program doesn’t include multiplication:</p>
<div class="language-ts highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">function</span> <span class="nf">enumerate</span><span class="p">(</span><span class="nx">result</span><span class="p">:</span> <span class="kr">number</span><span class="p">)</span> <span class="p">{</span>
  <span class="kd">const</span> <span class="nx">queue</span> <span class="o">=</span> <span class="p">[{</span> <span class="na">type</span><span class="p">:</span> <span class="dl">"</span><span class="s2">Hole</span><span class="dl">"</span> <span class="p">}];</span>
  <span class="k">while </span><span class="p">(</span><span class="nx">queue</span><span class="p">.</span><span class="nx">length</span> <span class="o">!==</span> <span class="mi">0</span><span class="p">)</span> <span class="p">{</span>
    <span class="kd">const</span> <span class="nx">expr</span> <span class="o">=</span> <span class="nx">queue</span><span class="p">.</span><span class="nf">shift</span><span class="p">()</span><span class="o">!</span><span class="p">;</span>
    <span class="k">if </span><span class="p">(</span><span class="nx">expr</span><span class="p">.</span><span class="kd">type</span> <span class="o">!==</span> <span class="dl">"</span><span class="s2">mul</span><span class="dl">"</span> <span class="o">&amp;&amp;</span> <span class="nf">evaluate</span><span class="p">(</span><span class="nx">expr</span><span class="p">)</span> <span class="o">===</span> <span class="nx">result</span><span class="p">)</span> <span class="p">{</span>
      <span class="k">return</span> <span class="nx">expr</span><span class="p">;</span>
    <span class="p">}</span>
    <span class="k">for </span><span class="p">(</span><span class="kd">const</span> <span class="nx">child</span> <span class="k">of</span> <span class="nf">expand</span><span class="p">(</span><span class="nx">expr</span><span class="p">))</span> <span class="p">{</span>
      <span class="nx">queue</span><span class="p">.</span><span class="nf">push</span><span class="p">(</span><span class="nx">child</span><span class="p">);</span>
    <span class="p">}</span>
  <span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>

<p>And it’s size is no more than 10:</p>
<div class="language-ts highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">function</span> <span class="nf">size</span><span class="p">(</span><span class="nx">expr</span><span class="p">:</span> <span class="nx">Expression</span><span class="p">):</span> <span class="kr">number</span> <span class="p">{</span>
  <span class="k">switch </span><span class="p">(</span><span class="nx">expr</span><span class="p">.</span><span class="kd">type</span><span class="p">)</span> <span class="p">{</span>
    <span class="k">case</span> <span class="dl">"</span><span class="s2">hole</span><span class="dl">"</span><span class="p">:</span>
      <span class="k">return</span> <span class="mi">0</span><span class="p">;</span>
    <span class="k">case</span> <span class="dl">"</span><span class="s2">zero</span><span class="dl">"</span><span class="p">:</span>
      <span class="k">return</span> <span class="mi">0</span><span class="p">;</span>
    <span class="k">case</span> <span class="dl">"</span><span class="s2">succ</span><span class="dl">"</span><span class="p">:</span>
      <span class="k">return</span> <span class="mi">1</span> <span class="o">+</span> <span class="nf">size</span><span class="p">(</span><span class="nx">expr</span><span class="p">.</span><span class="nx">prev</span><span class="p">)</span>
    <span class="k">case</span> <span class="dl">"</span><span class="s2">mul</span><span class="dl">"</span><span class="p">:</span> <span class="p">{</span>
	  <span class="k">return</span> <span class="mi">1</span> <span class="o">+</span> <span class="nf">size</span><span class="p">(</span><span class="nx">expr</span><span class="p">.</span><span class="nx">left</span><span class="p">)</span> <span class="o">+</span> <span class="nf">size</span><span class="p">(</span><span class="nx">expr</span><span class="p">.</span><span class="nx">right</span><span class="p">);</span>
    <span class="p">}</span>
  <span class="p">}</span>
<span class="p">}</span>

<span class="kd">function</span> <span class="nf">enumerate</span><span class="p">(</span><span class="nx">result</span><span class="p">:</span> <span class="kr">number</span><span class="p">)</span> <span class="p">{</span>
  <span class="kd">const</span> <span class="nx">queue</span> <span class="o">=</span> <span class="p">[{</span> <span class="na">type</span><span class="p">:</span> <span class="dl">"</span><span class="s2">Hole</span><span class="dl">"</span> <span class="p">}];</span>
  <span class="k">while </span><span class="p">(</span><span class="nx">queue</span><span class="p">.</span><span class="nx">length</span> <span class="o">!==</span> <span class="mi">0</span><span class="p">)</span> <span class="p">{</span>
    <span class="kd">const</span> <span class="nx">expr</span> <span class="o">=</span> <span class="nx">queue</span><span class="p">.</span><span class="nf">shift</span><span class="p">()</span><span class="o">!</span><span class="p">;</span>
    <span class="k">if </span><span class="p">(</span><span class="nf">size</span><span class="p">(</span><span class="nx">expr</span><span class="p">)</span> <span class="o">&lt;</span> <span class="mi">10</span> <span class="o">&amp;&amp;</span> <span class="nx">expr</span><span class="p">.</span><span class="kd">type</span> <span class="o">!==</span> <span class="dl">"</span><span class="s2">mul</span><span class="dl">"</span> <span class="o">&amp;&amp;</span> <span class="nf">evaluate</span><span class="p">(</span><span class="nx">expr</span><span class="p">)</span> <span class="o">===</span> <span class="nx">result</span><span class="p">)</span> <span class="p">{</span>
      <span class="k">return</span> <span class="nx">expr</span><span class="p">;</span>
    <span class="p">}</span>
    <span class="k">for </span><span class="p">(</span><span class="kd">const</span> <span class="nx">child</span> <span class="k">of</span> <span class="nf">expand</span><span class="p">(</span><span class="nx">expr</span><span class="p">))</span> <span class="p">{</span>
      <span class="nx">queue</span><span class="p">.</span><span class="nf">push</span><span class="p">(</span><span class="nx">child</span><span class="p">);</span>
    <span class="p">}</span>
  <span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>

<p>I can add all kinds of constraints and I am guaranteed that the resulting program will not violate them. There is no small probability, no temperature variable that would cause that.</p>

<h2 id="conclusion">Conclusion</h2>

<p>Beyond this, there are all sorts of extensions you can make. You can make the language more expressive; have variables, functions, compound data types, add types so that you can eliminate entire branches early on, store the useful programs you learned earlier and give it to the synthesizer. This way it wouldn’t need to find the same things over and over again and synthesize higher level programs.</p>]]></content><author><name></name></author><summary type="html"><![CDATA[Introduction]]></summary></entry><entry><title type="html">Let’s Implement Multiple Dispatch</title><link href="https://mehmetyksl.com/2024/08/20/lets-implement-multiple-dispatch.html" rel="alternate" type="text/html" title="Let’s Implement Multiple Dispatch" /><published>2024-08-20T00:00:00+03:00</published><updated>2024-08-20T00:00:00+03:00</updated><id>https://mehmetyksl.com/2024/08/20/lets-implement-multiple-dispatch</id><content type="html" xml:base="https://mehmetyksl.com/2024/08/20/lets-implement-multiple-dispatch.html"><![CDATA[<p><a href="https://julialang.org/">Julia</a>’s multiple dispatch feature sounds interesting. I’m going to implement it in this post.</p>

<h2 id="describing-the-problem">Describing the Problem</h2>

<p>I’m going to assume you know a bit about subtyping and function overloading. But here’s a simple explanation of it: It’s a way to relate types. For example, if type B is a subtype of type A, that means B is a kind of A. So, you can use B anywhere you need an A.</p>

<p>And function overloading is a way to define multiple functions with the same name but different parameters. The function that is called is the one that matches these parameters the “best”.</p>

<p>Let’s say we have a subtyping hierarchy like this:</p>

<div class="language-julia highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">abstract type</span><span class="nc"> Number</span>
<span class="k">struct</span><span class="nc"> Complex</span> <span class="o">&lt;:</span> <span class="kt">Number</span>
<span class="k">struct</span><span class="nc"> Real</span> <span class="o">&lt;:</span> <span class="kt">Complex</span>
</code></pre></div></div>

<p>And function like this:</p>

<div class="language-julia highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">function</span><span class="nf"> foo</span><span class="x">(</span><span class="n">x</span><span class="o">::</span><span class="kt">Number</span><span class="x">,</span> <span class="n">y</span><span class="o">::</span><span class="kt">Number</span><span class="x">)</span>
    <span class="n">println</span><span class="x">(</span><span class="s">"Number, Number"</span><span class="x">)</span>
<span class="k">end</span>

<span class="k">function</span><span class="nf"> foo</span><span class="x">(</span><span class="n">x</span><span class="o">::</span><span class="kt">Complex</span><span class="x">,</span> <span class="n">y</span><span class="o">::</span><span class="kt">Complex</span><span class="x">)</span>
    <span class="n">println</span><span class="x">(</span><span class="s">"Complex, Complex"</span><span class="x">)</span>
<span class="k">end</span>

<span class="k">function</span><span class="nf"> foo</span><span class="x">(</span><span class="n">x</span><span class="o">::</span><span class="kt">Real</span><span class="x">,</span> <span class="n">y</span><span class="o">::</span><span class="kt">Real</span><span class="x">)</span>
    <span class="n">println</span><span class="x">(</span><span class="s">"Real, Real"</span><span class="x">)</span>
<span class="k">end</span>
</code></pre></div></div>

<p>We want to select the most specific function for the given arguments. For example, if we call it with <code class="language-plaintext highlighter-rouge">foo(Real(), Real())</code> we call the last definition; if we call it with <code class="language-plaintext highlighter-rouge">foo(Complex(), Number())</code> we call the first definition because we can’t pass a <code class="language-plaintext highlighter-rouge">Number</code> where we need a <code class="language-plaintext highlighter-rouge">Complex</code>. The thing is, when we called <code class="language-plaintext highlighter-rouge">foo(Real(), Real())</code> it would be totally fine to call the second definition; the code would work just fine. So we not only want to find a method that conforms to the arguments but also a way to rank them.</p>

<h2 id="modeling-subtyping">Modeling Subtyping</h2>

<p>Let’s start by modeling subtyping first. I want something like this:</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">any</span> <span class="o">=</span> <span class="no">Type</span><span class="p">.</span><span class="nf">new</span><span class="p">(</span><span class="s2">"Any"</span><span class="p">)</span>
<span class="n">number</span> <span class="o">=</span> <span class="no">Type</span><span class="p">.</span><span class="nf">new</span><span class="p">(</span><span class="s2">"Number"</span><span class="p">,</span> <span class="n">any</span><span class="p">)</span>
<span class="n">complex</span> <span class="o">=</span> <span class="no">Type</span><span class="p">.</span><span class="nf">new</span><span class="p">(</span><span class="s2">"Complex"</span><span class="p">,</span> <span class="n">number</span><span class="p">)</span>
<span class="n">real</span> <span class="o">=</span> <span class="no">Type</span><span class="p">.</span><span class="nf">new</span><span class="p">(</span><span class="s2">"Real"</span><span class="p">,</span> <span class="n">complex</span><span class="p">)</span>
<span class="n">string</span> <span class="o">=</span> <span class="no">Type</span><span class="p">.</span><span class="nf">new</span><span class="p">(</span><span class="s2">"String"</span><span class="p">,</span> <span class="n">any</span><span class="p">)</span>

<span class="nb">puts</span> <span class="n">real</span><span class="p">.</span><span class="nf">is?</span><span class="p">(</span><span class="n">number</span><span class="p">)</span> <span class="c1"># true</span>
<span class="nb">puts</span> <span class="n">real</span><span class="p">.</span><span class="nf">is?</span><span class="p">(</span><span class="n">complex</span><span class="p">)</span> <span class="c1"># true</span>
<span class="nb">puts</span> <span class="n">real</span><span class="p">.</span><span class="nf">is?</span><span class="p">(</span><span class="n">real</span><span class="p">)</span> <span class="c1"># true</span>
<span class="nb">puts</span> <span class="n">real</span><span class="p">.</span><span class="nf">is?</span><span class="p">(</span><span class="n">string</span><span class="p">)</span> <span class="c1"># false</span>
<span class="nb">puts</span> <span class="n">real</span><span class="p">.</span><span class="nf">is?</span><span class="p">(</span><span class="n">any</span><span class="p">)</span> <span class="c1"># true</span>
<span class="nb">puts</span> <span class="n">string</span><span class="p">.</span><span class="nf">is?</span><span class="p">(</span><span class="n">any</span><span class="p">)</span> <span class="c1"># true</span>
</code></pre></div></div>

<p>All types are subtype of <code class="language-plaintext highlighter-rouge">Any</code> which has no supertype. You’ll notice that a type is a subtype of itself. Why? I can use the type <code class="language-plaintext highlighter-rouge">Real</code> anywhere where I need a <code class="language-plaintext highlighter-rouge">Real</code>. And <code class="language-plaintext highlighter-rouge">Any</code> is also a supertype of all types, which includes itself, which I think is quite nice.</p>

<p>We could implement this API like this:</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">class</span> <span class="nc">Type</span>
  <span class="nb">attr_reader</span> <span class="ss">:name</span><span class="p">,</span> <span class="ss">:supertype</span>

  <span class="k">def</span> <span class="nf">initialize</span><span class="p">(</span><span class="nb">name</span><span class="p">,</span> <span class="n">supertype</span> <span class="o">=</span> <span class="kp">nil</span><span class="p">)</span>
    <span class="vi">@name</span> <span class="o">=</span> <span class="nb">name</span>
    <span class="vi">@supertype</span> <span class="o">=</span> <span class="n">supertype</span>
  <span class="k">end</span>

  <span class="k">def</span> <span class="nf">is?</span><span class="p">(</span><span class="n">type</span><span class="p">)</span>
    <span class="c1"># Every type is a subtype of itself.</span>
    <span class="k">return</span> <span class="kp">true</span> <span class="k">if</span> <span class="n">type</span> <span class="o">==</span> <span class="nb">self</span>
    <span class="c1"># Any has no supertype.</span>
    <span class="k">return</span> <span class="kp">false</span> <span class="k">if</span> <span class="vi">@supertype</span><span class="p">.</span><span class="nf">nil?</span>
    <span class="c1"># Is `type` my grandparent?</span>
    <span class="vi">@supertype</span><span class="p">.</span><span class="nf">is?</span><span class="p">(</span><span class="n">type</span><span class="p">)</span>
  <span class="k">end</span>

  <span class="k">def</span> <span class="nf">==</span><span class="p">(</span><span class="n">type</span><span class="p">)</span>
    <span class="vi">@name</span> <span class="o">==</span> <span class="n">type</span><span class="p">.</span><span class="nf">name</span>
  <span class="k">end</span>
<span class="k">end</span>
</code></pre></div></div>

<h2 id="modeling-function-signatures">Modeling Function Signatures</h2>

<p>A signature is just a list of types.</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">class</span> <span class="nc">Signature</span>
  <span class="nb">attr_reader</span> <span class="ss">:types</span>

  <span class="k">def</span> <span class="nf">initialize</span><span class="p">(</span><span class="n">types</span><span class="p">)</span>
    <span class="vi">@types</span> <span class="o">=</span> <span class="n">types</span>
  <span class="k">end</span>

  <span class="k">def</span> <span class="nf">==</span><span class="p">(</span><span class="n">signature</span><span class="p">)</span>
    <span class="vi">@types</span> <span class="o">==</span> <span class="n">signature</span><span class="p">.</span><span class="nf">types</span>
  <span class="k">end</span>
<span class="k">end</span>
</code></pre></div></div>

<p>We need a way to know if it is legal to call a signature with a given list of argument types. Let’s look at a simple case:</p>

<div class="language-julia highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">function</span><span class="nf"> f</span><span class="x">(</span><span class="n">x</span><span class="o">::</span><span class="kt">Real</span><span class="x">)</span> <span class="k">end</span>
</code></pre></div></div>

<p>We obviously shouldn’t be able to call this function with <code class="language-plaintext highlighter-rouge">f(Complex())</code> because <code class="language-plaintext highlighter-rouge">Complex</code> is not a subtype of <code class="language-plaintext highlighter-rouge">Real</code>.
This generalizes to multiple arguments as well. We should be able to call <code class="language-plaintext highlighter-rouge">f(Real(), Real())</code> but not <code class="language-plaintext highlighter-rouge">f(Complex(), Real())</code>.</p>

<p>The first argument isn’t special. We should be able to call a function with a list of types <code class="language-plaintext highlighter-rouge">(t1, t2, ..., tn)</code> where the signature of the function is <code class="language-plaintext highlighter-rouge">(s1, s2, ..., sn)</code> if every <code class="language-plaintext highlighter-rouge">t</code> is a subtype of the corresponding type <code class="language-plaintext highlighter-rouge">s</code>.</p>

<p>Here’s the implementation:</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">class</span> <span class="nc">Signature</span>
  <span class="c1"># Other stuff...</span>

  <span class="k">def</span> <span class="nf">conforms?</span><span class="p">(</span><span class="n">signature</span><span class="p">)</span>
    <span class="c1"># for a signature to conform to this one:</span>
    <span class="c1"># 1. it must have the same number of types</span>
    <span class="k">return</span> <span class="kp">false</span> <span class="k">if</span> <span class="n">signature</span><span class="p">.</span><span class="nf">types</span><span class="p">.</span><span class="nf">length</span> <span class="o">!=</span> <span class="vi">@types</span><span class="p">.</span><span class="nf">length</span>
    <span class="c1"># 2. each type must be a subtype of the corresponding type in this signature</span>
    <span class="vi">@types</span><span class="p">.</span><span class="nf">zip</span><span class="p">(</span><span class="n">signature</span><span class="p">.</span><span class="nf">types</span><span class="p">).</span><span class="nf">all?</span> <span class="p">{</span> <span class="o">|</span><span class="n">a</span><span class="p">,</span> <span class="n">b</span><span class="o">|</span> <span class="n">a</span><span class="p">.</span><span class="nf">is?</span><span class="p">(</span><span class="n">b</span><span class="p">)</span> <span class="p">}</span>
  <span class="k">end</span>
<span class="k">end</span>
</code></pre></div></div>

<h2 id="ranking-conforming-signatures">Ranking Conforming Signatures</h2>

<p>Now we come to the meat of the problem. How do we select the most specific function for a given list of argument types? We need a way to rank them. Let’s look at the simplest case again.</p>

<div class="language-julia highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">function</span><span class="nf"> f</span><span class="x">(</span><span class="n">x</span><span class="o">::</span><span class="kt">Number</span><span class="x">)</span> <span class="k">end</span>
<span class="k">function</span><span class="nf"> f</span><span class="x">(</span><span class="n">x</span><span class="o">::</span><span class="kt">Complex</span><span class="x">)</span> <span class="k">end</span>
<span class="n">f</span><span class="x">(</span><span class="kt">Real</span><span class="x">())</span>
</code></pre></div></div>

<p>If I asked you which function should be called, you would say the second one, right? Why? Well, when we consider the subtype hierarchy <code class="language-plaintext highlighter-rouge">Real &lt;: Complex &lt;: Number &lt;: Any</code>, the <strong>closest</strong> to type <code class="language-plaintext highlighter-rouge">Real</code> is <code class="language-plaintext highlighter-rouge">Complex</code> so you choose that. This is the main idea behind the ranking algorithm.</p>

<p>We need a way to get the distance from the given type:</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">class</span> <span class="nc">Type</span>
  <span class="c1"># Other stuff...</span>

  <span class="k">def</span> <span class="nf">distance</span><span class="p">(</span><span class="n">type</span><span class="p">)</span>
    <span class="c1"># A String can't be a subtype of a Number or vice versa.</span>
    <span class="k">raise</span> <span class="s2">"Not a subtype"</span> <span class="k">unless</span> <span class="n">is?</span><span class="p">(</span><span class="n">type</span><span class="p">)</span>
    <span class="c1"># You are 0 distance away from yourself.</span>
    <span class="k">return</span> <span class="mi">0</span> <span class="k">if</span> <span class="nb">self</span> <span class="o">==</span> <span class="n">type</span>
    <span class="c1"># We are whatever distance `type` away from our supertype + 1.</span>
    <span class="c1"># Example:</span>
    <span class="c1">#   real.distance(any) = 1 + complex.distance(any)</span>
    <span class="c1">#                      = 1 + 1 + number.distance(any)</span>
    <span class="c1">#                      = 1 + 1 + 1 + any.distance(any)</span>
    <span class="c1">#                      = 1 + 1 + 1 + 0</span>
    <span class="c1">#                      = 3</span>
    <span class="mi">1</span> <span class="o">+</span> <span class="vi">@supertype</span><span class="p">.</span><span class="nf">distance</span><span class="p">(</span><span class="n">type</span><span class="p">)</span>
  <span class="k">end</span>
<span class="k">end</span>
</code></pre></div></div>

<p>This can rank single argument functions. Supporting multiple arguments is straightforward. It’s like Euclidean distance: $\sqrt{(2 - 3)^2 + (4 - 5)^2}$ but easier. Find the element-wise distance between each argument and sum them up like $\lvert 2 - 3 \rvert + \lvert 4 - 5 \rvert$</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">class</span> <span class="nc">Signature</span>
  <span class="c1"># Other stuff...</span>

  <span class="k">def</span> <span class="nf">distance</span><span class="p">(</span><span class="n">signature</span><span class="p">)</span>
    <span class="vi">@types</span><span class="p">.</span><span class="nf">zip</span><span class="p">(</span><span class="n">signature</span><span class="p">.</span><span class="nf">types</span><span class="p">).</span><span class="nf">sum</span> <span class="p">{</span> <span class="o">|</span><span class="n">a</span><span class="p">,</span> <span class="n">b</span><span class="o">|</span> <span class="n">a</span><span class="p">.</span><span class="nf">distance</span><span class="p">(</span><span class="n">b</span><span class="p">)</span> <span class="p">}</span>
  <span class="k">end</span>
<span class="k">end</span>
</code></pre></div></div>
<p>This is called <a href="https://en.wikipedia.org/wiki/Taxicab_geometry">Manhattan distance</a>.</p>

<p>If we put this to work:</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">any</span> <span class="o">=</span> <span class="no">Type</span><span class="p">.</span><span class="nf">new</span><span class="p">(</span><span class="s2">"Any"</span><span class="p">)</span>
<span class="n">number</span> <span class="o">=</span> <span class="no">Type</span><span class="p">.</span><span class="nf">new</span><span class="p">(</span><span class="s2">"Number"</span><span class="p">,</span> <span class="n">any</span><span class="p">)</span>
<span class="n">complex</span> <span class="o">=</span> <span class="no">Type</span><span class="p">.</span><span class="nf">new</span><span class="p">(</span><span class="s2">"Complex"</span><span class="p">,</span> <span class="n">number</span><span class="p">)</span>
<span class="n">real</span> <span class="o">=</span> <span class="no">Type</span><span class="p">.</span><span class="nf">new</span><span class="p">(</span><span class="s2">"Real"</span><span class="p">,</span> <span class="n">complex</span><span class="p">)</span>

<span class="n">f1</span> <span class="o">=</span> <span class="no">Signature</span><span class="p">.</span><span class="nf">new</span><span class="p">([</span><span class="n">number</span><span class="p">,</span> <span class="n">number</span><span class="p">])</span>
<span class="n">f2</span> <span class="o">=</span> <span class="no">Signature</span><span class="p">.</span><span class="nf">new</span><span class="p">([</span><span class="n">complex</span><span class="p">,</span> <span class="n">complex</span><span class="p">])</span>
<span class="n">f3</span> <span class="o">=</span> <span class="no">Signature</span><span class="p">.</span><span class="nf">new</span><span class="p">([</span><span class="n">real</span><span class="p">,</span> <span class="n">real</span><span class="p">])</span>

<span class="n">call_signature</span> <span class="o">=</span> <span class="no">Signature</span><span class="p">.</span><span class="nf">new</span><span class="p">([</span><span class="n">real</span><span class="p">,</span> <span class="n">real</span><span class="p">])</span>
<span class="nb">puts</span> <span class="p">[</span><span class="n">f1</span><span class="p">,</span> <span class="n">f2</span><span class="p">,</span> <span class="n">f3</span><span class="p">].</span><span class="nf">min_by</span> <span class="p">{</span> <span class="o">|</span><span class="n">f</span><span class="o">|</span> <span class="n">call_signature</span><span class="p">.</span><span class="nf">distance</span><span class="p">(</span><span class="n">f</span><span class="p">)</span> <span class="p">}</span> <span class="o">==</span> <span class="n">f3</span> <span class="c1"># true</span>
</code></pre></div></div>

<p>A function isn’t just a signature though, it also has a name.</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">class</span> <span class="nc">Function</span>
  <span class="nb">attr_accessor</span> <span class="ss">:name</span><span class="p">,</span> <span class="ss">:signature</span>

  <span class="k">def</span> <span class="nf">initialize</span><span class="p">(</span><span class="nb">name</span><span class="p">,</span> <span class="n">signature</span><span class="p">)</span>
    <span class="vi">@name</span> <span class="o">=</span> <span class="nb">name</span>
    <span class="vi">@signature</span> <span class="o">=</span> <span class="n">signature</span>
  <span class="k">end</span>

  <span class="k">def</span> <span class="nf">to_s</span>
    <span class="s2">"</span><span class="si">#{</span><span class="vi">@name</span><span class="si">}#{</span><span class="vi">@signature</span><span class="si">}</span><span class="s2">"</span>
  <span class="k">end</span>

  <span class="k">def</span> <span class="nf">==</span><span class="p">(</span><span class="n">other</span><span class="p">)</span>
    <span class="vi">@name</span> <span class="o">==</span> <span class="n">other</span><span class="p">.</span><span class="nf">name</span> <span class="o">&amp;&amp;</span> <span class="vi">@signature</span> <span class="o">==</span> <span class="n">other</span><span class="p">.</span><span class="nf">signature</span>
  <span class="k">end</span>
<span class="k">end</span>
</code></pre></div></div>

<p>And we’ll have a table of functions that contains all the definitions and gives the most specific one for a given call.</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">class</span> <span class="nc">FunctionTable</span>
  <span class="nb">attr_accessor</span> <span class="ss">:functions</span>

  <span class="k">def</span> <span class="nf">initialize</span><span class="p">()</span>
    <span class="vi">@functions</span> <span class="o">=</span> <span class="p">[]</span>
  <span class="k">end</span>

  <span class="k">def</span> <span class="nf">add</span><span class="p">(</span><span class="n">function</span><span class="p">)</span>
    <span class="k">raise</span> <span class="s2">"Function already exists"</span> <span class="k">if</span> <span class="vi">@functions</span><span class="p">.</span><span class="nf">include?</span><span class="p">(</span><span class="n">function</span><span class="p">)</span>
    <span class="vi">@functions</span> <span class="o">&lt;&lt;</span> <span class="n">function</span>
  <span class="k">end</span>

  <span class="k">def</span> <span class="nf">find</span><span class="p">(</span><span class="n">function</span><span class="p">)</span>
    <span class="c1"># find all the signatures that conform to the given signature.</span>
    <span class="n">candidates</span> <span class="o">=</span> <span class="vi">@functions</span><span class="p">.</span><span class="nf">select</span> <span class="p">{</span> <span class="o">|</span><span class="n">m</span><span class="o">|</span> <span class="n">function</span><span class="p">.</span><span class="nf">signature</span><span class="p">.</span><span class="nf">conforms?</span><span class="p">(</span><span class="n">m</span><span class="p">.</span><span class="nf">signature</span><span class="p">)</span> <span class="o">&amp;&amp;</span> <span class="n">m</span><span class="p">.</span><span class="nf">name</span> <span class="o">==</span> <span class="n">function</span><span class="p">.</span><span class="nf">name</span> <span class="p">}</span>
    <span class="c1"># sort them by distance from closest to furthest.</span>
    <span class="n">sorted_by_distance</span> <span class="o">=</span> <span class="n">candidates</span><span class="p">.</span><span class="nf">sort_by</span> <span class="p">{</span> <span class="o">|</span><span class="n">m</span><span class="o">|</span> <span class="n">function</span><span class="p">.</span><span class="nf">signature</span><span class="p">.</span><span class="nf">distance</span><span class="p">(</span><span class="n">m</span><span class="p">.</span><span class="nf">signature</span><span class="p">)</span> <span class="p">}</span>

    <span class="c1"># find the closest one.</span>
    <span class="c1"># There may be more than one with the same distance, so we find all of them.</span>
    <span class="n">distances</span> <span class="o">=</span> <span class="n">sorted_by_distance</span><span class="p">.</span><span class="nf">map</span> <span class="p">{</span> <span class="o">|</span><span class="n">m</span><span class="o">|</span> <span class="n">function</span><span class="p">.</span><span class="nf">signature</span><span class="p">.</span><span class="nf">distance</span><span class="p">(</span><span class="n">m</span><span class="p">.</span><span class="nf">signature</span><span class="p">)</span> <span class="p">}</span>
    <span class="n">min_distance</span> <span class="o">=</span> <span class="n">distances</span><span class="p">.</span><span class="nf">min</span>
    <span class="n">closest_functions</span> <span class="o">=</span> <span class="n">sorted_by_distance</span><span class="p">.</span><span class="nf">select</span> <span class="p">{</span> <span class="o">|</span><span class="n">m</span><span class="o">|</span> <span class="n">function</span><span class="p">.</span><span class="nf">signature</span><span class="p">.</span><span class="nf">distance</span><span class="p">(</span><span class="n">m</span><span class="p">.</span><span class="nf">signature</span><span class="p">)</span> <span class="o">==</span> <span class="n">min_distance</span> <span class="p">}</span>
    <span class="k">raise</span> <span class="s2">"Ambiguous function call between </span><span class="si">#{</span><span class="n">closest_functions</span><span class="si">}</span><span class="s2">"</span> <span class="k">if</span> <span class="n">closest_functions</span><span class="p">.</span><span class="nf">length</span> <span class="o">&gt;</span> <span class="mi">1</span>
    <span class="n">closest_functions</span><span class="p">.</span><span class="nf">first</span>
  <span class="k">end</span>
<span class="k">end</span>
</code></pre></div></div>

<p>The most interesting method is <code class="language-plaintext highlighter-rouge">find</code>.</p>

<ol>
  <li>It finds all the functions with the same name and conforming signature.</li>
  <li>Sorts them by distance.</li>
  <li>Finds the closest one.</li>
  <li>If there are more than one with the same distance, it raises an error. You could also return the second closest method instead of raising an error. (I wonder if there is a metric where two different point can’t have the same distance from a different third point).</li>
</ol>

<p>If we put it all together:</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">any</span> <span class="o">=</span> <span class="no">Type</span><span class="p">.</span><span class="nf">new</span><span class="p">(</span><span class="s2">"Any"</span><span class="p">)</span>
<span class="n">number</span> <span class="o">=</span> <span class="no">Type</span><span class="p">.</span><span class="nf">new</span><span class="p">(</span><span class="s2">"Number"</span><span class="p">,</span> <span class="n">any</span><span class="p">)</span>
<span class="n">complex</span> <span class="o">=</span> <span class="no">Type</span><span class="p">.</span><span class="nf">new</span><span class="p">(</span><span class="s2">"Complex"</span><span class="p">,</span> <span class="n">number</span><span class="p">)</span>
<span class="n">real</span> <span class="o">=</span> <span class="no">Type</span><span class="p">.</span><span class="nf">new</span><span class="p">(</span><span class="s2">"Real"</span><span class="p">,</span> <span class="n">complex</span><span class="p">)</span>

<span class="n">f1</span> <span class="o">=</span> <span class="no">Function</span><span class="p">.</span><span class="nf">new</span><span class="p">(</span><span class="s2">"f"</span><span class="p">,</span> <span class="no">Signature</span><span class="p">.</span><span class="nf">new</span><span class="p">([</span><span class="n">number</span><span class="p">,</span> <span class="n">number</span><span class="p">]))</span>
<span class="n">f2</span> <span class="o">=</span> <span class="no">Function</span><span class="p">.</span><span class="nf">new</span><span class="p">(</span><span class="s2">"f"</span><span class="p">,</span> <span class="no">Signature</span><span class="p">.</span><span class="nf">new</span><span class="p">([</span><span class="n">complex</span><span class="p">,</span> <span class="n">complex</span><span class="p">]))</span>
<span class="n">f3</span> <span class="o">=</span> <span class="no">Function</span><span class="p">.</span><span class="nf">new</span><span class="p">(</span><span class="s2">"f"</span><span class="p">,</span> <span class="no">Signature</span><span class="p">.</span><span class="nf">new</span><span class="p">([</span><span class="n">real</span><span class="p">,</span> <span class="n">real</span><span class="p">]))</span>

<span class="n">table</span> <span class="o">=</span> <span class="no">FunctionTable</span><span class="p">.</span><span class="nf">new</span>
<span class="n">table</span><span class="p">.</span><span class="nf">add</span><span class="p">(</span><span class="n">f1</span><span class="p">)</span>
<span class="n">table</span><span class="p">.</span><span class="nf">add</span><span class="p">(</span><span class="n">f2</span><span class="p">)</span>
<span class="n">table</span><span class="p">.</span><span class="nf">add</span><span class="p">(</span><span class="n">f3</span><span class="p">)</span>

<span class="n">call_signature</span> <span class="o">=</span> <span class="no">Signature</span><span class="p">.</span><span class="nf">new</span><span class="p">([</span><span class="n">real</span><span class="p">,</span> <span class="n">real</span><span class="p">])</span>
<span class="nb">puts</span> <span class="n">table</span><span class="p">.</span><span class="nf">find</span><span class="p">(</span><span class="no">Function</span><span class="p">.</span><span class="nf">new</span><span class="p">(</span><span class="s2">"f"</span><span class="p">,</span> <span class="n">call_signature</span><span class="p">))</span> <span class="o">==</span> <span class="n">f3</span> <span class="c1"># true</span>
</code></pre></div></div>
<p><a href="https://gist.github.com/mehmet-yuksel/22538bb8083682b0c8d383337e82f334">Full Code</a></p>

<h2 id="further-reading">Further Reading</h2>

<ul>
  <li><a href="https://journal.stuffwithstuff.com/2012/06/12/multimethods-global-scope-and-monkey-patching/">Multimethods, Global Scope, and Monkey-patching</a> - Bob Nystrom’s exploration of multimethods and their implications for language design</li>
  <li><a href="https://journal.stuffwithstuff.com/2010/10/01/solving-the-expression-problem/">Solving the Expression Problem</a> - Bob Nystrom on how multimethods can solve the expression problem</li>
  <li><a href="https://journal.stuffwithstuff.com/2011/04/21/multimethods-multiple-inheritance-multiawesome/">Multimethods, Multiple Inheritance, Multiawesome!</a> - Bob Nystrom on the relationship between multimethods and multiple inheritance</li>
  <li><a href="https://eli.thegreenplace.net/2016/the-expression-problem-and-its-solutions/">The Expression Problem and Its Solutions</a> - Eli Bendersky’s overview of different approaches to the expression problem</li>
  <li><a href="https://www.youtube.com/watch?v=kc9HwsxE1OY&amp;t=442s">The Unreasonable Effectiveness of Multiple Dispatch</a> - Stefan Karpinski’s JuliaCon talk on multiple dispatch in Julia</li>
</ul>]]></content><author><name></name></author><summary type="html"><![CDATA[Julia’s multiple dispatch feature sounds interesting. I’m going to implement it in this post.]]></summary></entry><entry><title type="html">Search Engine from Scratch</title><link href="https://mehmetyksl.com/2024/08/11/search-engine-from-scratch.html" rel="alternate" type="text/html" title="Search Engine from Scratch" /><published>2024-08-11T00:00:00+03:00</published><updated>2024-08-11T00:00:00+03:00</updated><id>https://mehmetyksl.com/2024/08/11/search-engine-from-scratch</id><content type="html" xml:base="https://mehmetyksl.com/2024/08/11/search-engine-from-scratch.html"><![CDATA[<p>Let’s define the problem at a high level first.</p>

<ol>
  <li>User enters a query.</li>
  <li>The search engine returns the most relevant document (HTML, PDF, or any other textual content).</li>
</ol>

<p>How do we define relevance? We can say it’s 0 if the document doesn’t contain the query and 1 if it does like this:</p>
<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">document</span> <span class="o">=</span> <span class="s2">"The quick brown fox jumps over the lazy dog."</span>
<span class="n">query</span> <span class="o">=</span> <span class="s2">"..."</span> <span class="c1"># comes from the user</span>
<span class="n">is_relevant</span> <span class="o">=</span> <span class="n">document</span><span class="p">.</span><span class="nf">include?</span><span class="p">(</span><span class="n">query</span><span class="p">)</span>
</code></pre></div></div>

<p>Entering “Quick” instead of “quick” shouldn’t matter.</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">document</span> <span class="o">=</span> <span class="s2">"The quick brown fox jumps over the lazy dog."</span><span class="p">.</span><span class="nf">downcase</span>
<span class="n">query</span> <span class="o">=</span> <span class="s2">"..."</span><span class="p">.</span><span class="nf">downcase</span> <span class="c1"># comes from the user</span>
<span class="n">is_relevant</span> <span class="o">=</span> <span class="n">document</span><span class="p">.</span><span class="nf">include?</span><span class="p">(</span><span class="n">query</span><span class="p">)</span>
</code></pre></div></div>

<p>Neither the order of the words, e.g. “dog lazy” vs. “lazy dog”.</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">is_relevant</span> <span class="o">=</span> <span class="n">query</span>
  <span class="p">.</span><span class="nf">split</span>
  <span class="p">.</span><span class="nf">all?</span> <span class="p">{</span> <span class="o">|</span><span class="n">word</span><span class="o">|</span> <span class="n">document</span><span class="p">.</span><span class="nf">include?</span><span class="p">(</span><span class="n">word</span><span class="p">)</span> <span class="p">}</span>
</code></pre></div></div>

<p>A word may not be present in the document and that’s okay.</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">is_relevant</span> <span class="o">=</span> <span class="n">query</span>
  <span class="p">.</span><span class="nf">split</span>
  <span class="p">.</span><span class="nf">any?</span> <span class="p">{</span> <span class="o">|</span><span class="n">word</span><span class="o">|</span> <span class="n">document</span><span class="p">.</span><span class="nf">include?</span><span class="p">(</span><span class="n">word</span><span class="p">)</span> <span class="p">}</span>
</code></pre></div></div>

<h2 id="coming-up-with-a-ranking-system">Coming up with a Ranking System</h2>

<p>Putting documents into two buckets: relevant and irrelevant won’t be very useful when there are billions of documents to sort through as in the case of the web.</p>

<p>It would be nice if we could give documents a ranking score. We can say a document has a higher score if it contains the words in the query more frequently. If the query contains the word “quick” for example, a document that contains 50 times is more relevant than one that contains it only once.</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">relevance</span> <span class="o">=</span> <span class="n">query</span>
  <span class="p">.</span><span class="nf">split</span>
  <span class="p">.</span><span class="nf">map</span> <span class="p">{</span> <span class="o">|</span><span class="n">word</span><span class="o">|</span> <span class="n">document</span><span class="p">.</span><span class="nf">scan</span><span class="p">(</span><span class="n">word</span><span class="p">).</span><span class="nf">count</span> <span class="p">}</span>
  <span class="p">.</span><span class="nf">sum</span>
</code></pre></div></div>

<p>With an example query and document:</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">query</span> <span class="o">=</span> <span class="s2">"the frequency of"</span>
<span class="n">document</span> <span class="o">=</span> <span class="s2">"the frequency of the word."</span>
<span class="n">relevance</span> <span class="o">=</span> <span class="n">query</span>
  <span class="p">.</span><span class="nf">split</span>
  <span class="p">.</span><span class="nf">map</span> <span class="p">{</span> <span class="o">|</span><span class="n">word</span><span class="o">|</span> <span class="n">document</span><span class="p">.</span><span class="nf">scan</span><span class="p">(</span><span class="n">word</span><span class="p">).</span><span class="nf">count</span> <span class="p">}</span>
  <span class="p">.</span><span class="nf">sum</span>
</code></pre></div></div>

<p>For the query “the frequency of”, the document “the frequency of the word and the importance of the term and the length of the word.” would have a higher score than the document “the frequency of the word” just because it contains the words “the” and “of” more times.</p>

<p>Instead of just counting the occurrences, we can count their frequencies. This gives a score between 0 and 1 and prevent the score to blow up as the document gets longer.
This is called <a href="https://en.wikipedia.org/wiki/Tf%E2%80%93idf#Term_frequency">term frequency</a>.</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">number_of_words</span> <span class="o">=</span> <span class="n">document</span><span class="p">.</span><span class="nf">split</span><span class="p">.</span><span class="nf">count</span>
<span class="n">relevance</span> <span class="o">=</span> <span class="n">query</span>
  <span class="p">.</span><span class="nf">split</span>
  <span class="p">.</span><span class="nf">map</span> <span class="p">{</span> <span class="o">|</span><span class="n">word</span><span class="o">|</span> <span class="n">document</span><span class="p">.</span><span class="nf">scan</span><span class="p">(</span><span class="n">word</span><span class="p">).</span><span class="nf">count</span> <span class="o">/</span> <span class="n">number_of_words</span><span class="p">.</span><span class="nf">to_f</span> <span class="p">}</span>
  <span class="p">.</span><span class="nf">sum</span>
</code></pre></div></div>

<p>By factoring out the common denominator:</p>
<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">relevance</span> <span class="o">=</span> <span class="n">query</span>
  <span class="p">.</span><span class="nf">split</span>
  <span class="p">.</span><span class="nf">map</span> <span class="p">{</span> <span class="o">|</span><span class="n">word</span><span class="o">|</span> <span class="n">document</span><span class="p">.</span><span class="nf">scan</span><span class="p">(</span><span class="n">word</span><span class="p">).</span><span class="nf">count</span> <span class="p">}</span>
  <span class="p">.</span><span class="nf">sum</span> <span class="o">/</span> <span class="n">number_of_words</span><span class="p">.</span><span class="nf">to_f</span>
</code></pre></div></div>

<p>Searching by the meaning of the document would be better but this would require an AI model.</p>

<h2 id="not-all-words-are-created-equal">Not All Words Are Created Equal</h2>

<p>Not all words carry the same weight. In the sentence ‘the quick brown fox jumps over the lazy dog,’ removing ‘the’ preserves the core meaning; removing ‘fox’ destroys it. Common articles like ‘the,’ ‘a,’ and ‘an’ appear in almost every document, making them useless for differentiating one text from another.</p>

<p>Storing them on a list only works if we’re only dealing with English. There is a language agnostic approach: calculating how frequently a word appears in all the documents.</p>

<div class="kdmath">$$
\frac{\text{number of documents}}{\text{number of documents containing the word}}
$$</div>

<p>The word “the” would appear in 99 documents out of 100 making its importance $100/99 = 1.01$. A word like “fox” would appear only in 10 documents giving it an importance score of $100/10 = 10$.</p>

<p>If the word doesn’t appear in any document at all then we get $100/0 = \infty$. Adding 1 to the numerator and denominator solves it.</p>

<div class="kdmath">$$
\frac{\text{number of documents}\ +\ 1}{\text{number of documents containing the word}\ +\ 1}
$$</div>

<p>Why did I add +1 to the numerator too? In the extreme case that the word appears in all documents, the old formula would’ve given 1. I’d like to preserve that. It’s not that important, though. in the limit, that +1 doesn’t matter.</p>

<p>To calculate the importance of a word, we can use this fraction along with the term frequency we calculated before.</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">def</span> <span class="nf">importance</span><span class="p">(</span><span class="n">word</span><span class="p">,</span> <span class="n">documents</span><span class="p">)</span>
  <span class="n">number_of_documents</span> <span class="o">=</span> <span class="n">documents</span><span class="p">.</span><span class="nf">count</span>
  <span class="n">number_of_documents_containing_word</span> <span class="o">=</span> <span class="n">documents</span><span class="p">.</span><span class="nf">count</span> <span class="p">{</span> <span class="o">|</span><span class="n">document</span><span class="o">|</span> <span class="n">document</span><span class="p">.</span><span class="nf">include?</span><span class="p">(</span><span class="n">word</span><span class="p">)</span> <span class="p">}</span>
  <span class="p">(</span><span class="n">number_of_documents</span> <span class="o">+</span> <span class="mi">1</span><span class="p">)</span> <span class="o">/</span> <span class="p">(</span><span class="n">number_of_documents_containing_word</span> <span class="o">+</span> <span class="mi">1</span><span class="p">).</span><span class="nf">to_f</span>
<span class="k">end</span>

<span class="k">def</span> <span class="nf">relevance</span><span class="p">(</span><span class="n">query</span><span class="p">,</span> <span class="n">document</span><span class="p">,</span> <span class="n">documents</span><span class="p">)</span>
  <span class="n">number_of_words</span> <span class="o">=</span> <span class="n">document</span><span class="p">.</span><span class="nf">split</span><span class="p">.</span><span class="nf">count</span>
  <span class="n">query</span>
    <span class="p">.</span><span class="nf">split</span>
    <span class="c1"># Less important words will contribute less to the relevance score.</span>
    <span class="p">.</span><span class="nf">map</span> <span class="p">{</span> <span class="o">|</span><span class="n">word</span><span class="o">|</span> <span class="n">document</span><span class="p">.</span><span class="nf">scan</span><span class="p">(</span><span class="n">word</span><span class="p">).</span><span class="nf">count</span> <span class="o">*</span> <span class="n">importance</span><span class="p">(</span><span class="n">word</span><span class="p">,</span> <span class="n">documents</span><span class="p">)</span> <span class="p">}</span>
    <span class="p">.</span><span class="nf">sum</span> <span class="o">/</span> <span class="n">number_of_words</span><span class="p">.</span><span class="nf">to_f</span>
<span class="k">end</span>
</code></pre></div></div>

<p>The <em>importance</em> formula is almost something called <a href="https://en.wikipedia.org/wiki/Tf%E2%80%93idf#Inverse_document_frequency">IDF(Inverse Document Frequency)</a>. There is only one difference. IDF takes the logarithm of the fraction. Why? If the word occurs too frequently or too rarely, it can dominate the scores. Using a logarithm dampens this effect.</p>

<p>The update is simple:</p>
<div class="language-diff highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">def importance(word, documents)
</span>  number_of_documents = documents.count
  number_of_documents_containing_word = documents.count { |document| document.include?(word) }
<span class="gi">+ Math.log((number_of_documents + 1) / (number_of_documents_containing_word + 1).to_f)
</span><span class="p">end
</span></code></pre></div></div>

<h2 id="partial-queries">Partial Queries</h2>

<p>What if the user enters a partial query like “tes”? The search engine should match the words “test”, “testing”, “tested” too. Whole words aren’t granular enough. <a href="https://en.wikipedia.org/wiki/Stemming">Stemming</a> reduces a word to its root form. For example, the stem of “fishing”, “fished” and “fisher” is “fish”.</p>

<p>Stemming is tedious and assumes a document uses a single language. <a href="https://en.wikipedia.org/wiki/N-gram">N-grams</a> offer a more general, though less semantic, alternative. The 2-grams of “quick” are: “qu”, “ui”, “ic” and “ck”. The 3-grams are: “qui”, “uic” and “ick”. It’s like a sliding window that moves to the right.</p>

<p>We wouldn’t search for “quick” by entering “uic” or “ick”(there are exceptions to this e.g. irresponsibility). Instead of moving the window, we’re going to extend it. For example, for the word “quick”, we get “qu”, “qui”, “quic”, “quick”. This achieves what stemming does but without knowing the language.</p>

<p>The average length of a root word in European languages is roughly 5. I’m going to use a window size between 2 and 5.</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">def</span> <span class="nf">n_grams</span><span class="p">(</span><span class="n">document</span><span class="p">,</span> <span class="ss">min_n: </span><span class="mi">2</span><span class="p">,</span> <span class="ss">max_n: </span><span class="mi">5</span><span class="p">)</span>
  <span class="n">words</span> <span class="o">=</span> <span class="n">document</span><span class="p">.</span><span class="nf">split</span>
  <span class="c1"># This will store the n-gram and the number of times it appears in the document.</span>
  <span class="n">terms</span> <span class="o">=</span> <span class="p">[]</span>
  <span class="n">words</span><span class="p">.</span><span class="nf">each</span> <span class="k">do</span> <span class="o">|</span><span class="n">word</span><span class="o">|</span>
    <span class="c1"># the word can be smaller than the max window size so we take the minimum.</span>
    <span class="n">max_window_size</span> <span class="o">=</span> <span class="p">[</span><span class="n">max_n</span><span class="p">,</span> <span class="n">word</span><span class="p">.</span><span class="nf">length</span><span class="p">].</span><span class="nf">min</span>
    <span class="c1"># for every window size take a substring and add it to the terms.</span>
    <span class="p">(</span><span class="n">min_n</span><span class="o">..</span><span class="n">max_window_size</span><span class="p">).</span><span class="nf">each</span> <span class="k">do</span> <span class="o">|</span><span class="n">window_size</span><span class="o">|</span>
      <span class="n">terms</span> <span class="o">&lt;&lt;</span> <span class="n">word</span><span class="p">[</span><span class="o">...</span><span class="n">window_size</span><span class="p">]</span>
    <span class="k">end</span>
  <span class="k">end</span>
  <span class="n">terms</span>
<span class="k">end</span>
</code></pre></div></div>

<p>A word isn’t the smallest unit anymore so these need an update:</p>

<div class="language-diff highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">def importance(term, documents)
</span>  number_of_documents = documents.count
<span class="gi">+ number_of_documents_containing_term = documents.count { |document| n_grams(document).include?(term) }
</span>  Math.log((number_of_documents + 1) / (number_of_documents_containing_term + 1).to_f)
<span class="p">end
</span><span class="err">
</span><span class="p">def relevance(query, document, documents)
</span><span class="gi">+ number_of_terms = n_grams(document).count
+ query_terms = n_grams(query)
</span>  query_terms
<span class="gi">+   .map { |term| n_grams(document).count(term) * importance(term, documents) }
</span>    .sum / number_of_terms.to_f
<span class="p">end
</span></code></pre></div></div>

<p>Punctuation isn’t important either:</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">def</span> <span class="nf">clean</span><span class="p">(</span><span class="n">document</span><span class="p">)</span>
  <span class="n">document</span><span class="p">.</span><span class="nf">gsub</span><span class="p">(</span><span class="sr">/[[:punct:]]/</span><span class="p">,</span> <span class="s2">" "</span><span class="p">)</span>
<span class="k">end</span>
</code></pre></div></div>

<h2 id="full-engine">Full Engine</h2>

<p>The API could look like this:</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">search_engine</span> <span class="o">=</span> <span class="no">SearchEngine</span><span class="p">.</span><span class="nf">new</span>
<span class="n">search_engine</span><span class="p">.</span><span class="nf">add_document</span><span class="p">(</span><span class="s2">"first_document"</span><span class="p">,</span> <span class="s2">"Peter,</span><span class="se">\n\n</span><span class="s2">I'm going to need those TPS reports on my desk first thing tomorrow! And clean up your desk!</span><span class="se">\n\n</span><span class="s2">Lumbergh"</span><span class="p">)</span>
<span class="n">search_engine</span><span class="p">.</span><span class="nf">add_document</span><span class="p">(</span><span class="s2">"second_document"</span><span class="p">,</span> <span class="s2">"Everyone,</span><span class="se">\n\n</span><span class="s2">M-m-m-m-my red stapler has gone missing. H-h-has a-an-anyone seen it?</span><span class="se">\n\n</span><span class="s2">Milton"</span><span class="p">)</span>
<span class="n">search_engine</span><span class="p">.</span><span class="nf">add_document</span><span class="p">(</span><span class="s2">"third_document"</span><span class="p">,</span> <span class="s2">"Peter,</span><span class="se">\n\n</span><span class="s2">Yeah, I'm going to need you to come in on Saturday. Don't forget those reports.</span><span class="se">\n\n</span><span class="s2">Lumbergh"</span><span class="p">)</span>

<span class="n">results</span> <span class="o">=</span> <span class="n">search_engine</span><span class="p">.</span><span class="nf">search</span><span class="p">(</span><span class="s2">"tps repor"</span><span class="p">)</span>
<span class="nb">puts</span> <span class="n">results</span>
<span class="o">&lt;&lt;-</span><span class="no">OUTPUT</span><span class="sh">
[
  { :name =&gt; "first_document", :relevance =&gt; SCORE_1, :content =&gt; "Peter,</span><span class="se">\n\n</span><span class="sh">I'm going to need those TPS reports on my desk first thing tomorrow! And clean up your desk!</span><span class="se">\n\n</span><span class="sh">Lumbergh" },
  { :name =&gt; "third_document",  :relevance =&gt; SCORE_2, :content =&gt; "Peter,</span><span class="se">\n\n</span><span class="sh">Yeah, I'm going to need you to come in on Saturday. Don't forget those reports.</span><span class="se">\n\n</span><span class="sh">Lumbergh" }
]
</span><span class="no">OUTPUT</span>
</code></pre></div></div>

<p>Where <code class="language-plaintext highlighter-rouge">SCORE_1</code> should be higher than <code class="language-plaintext highlighter-rouge">SCORE_2</code> and the documents should be sorted by relevance.</p>

<p>Here is the full implementation:</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">class</span> <span class="nc">SearchEngine</span>
  <span class="k">def</span> <span class="nf">initialize</span>
    <span class="vi">@documents</span> <span class="o">=</span> <span class="p">[]</span>
  <span class="k">end</span>

  <span class="k">def</span> <span class="nf">add_document</span><span class="p">(</span><span class="nb">name</span><span class="p">,</span> <span class="n">document</span><span class="p">)</span>
    <span class="vi">@documents</span> <span class="o">&lt;&lt;</span> <span class="p">{</span> <span class="ss">name: </span><span class="nb">name</span><span class="p">,</span> <span class="ss">content: </span><span class="n">clean</span><span class="p">(</span><span class="n">document</span><span class="p">)</span> <span class="p">}</span>
  <span class="k">end</span>

  <span class="k">def</span> <span class="nf">search</span><span class="p">(</span><span class="n">query</span><span class="p">)</span>
    <span class="n">document_contents</span> <span class="o">=</span> <span class="vi">@documents</span><span class="p">.</span><span class="nf">map</span> <span class="p">{</span> <span class="o">|</span><span class="n">document</span><span class="o">|</span> <span class="n">document</span><span class="p">[</span><span class="ss">:content</span><span class="p">]</span> <span class="p">}</span>
    <span class="vi">@documents</span>
      <span class="p">.</span><span class="nf">map</span> <span class="p">{</span> <span class="o">|</span><span class="n">document</span><span class="o">|</span> <span class="p">{</span> <span class="ss">name: </span><span class="n">document</span><span class="p">[</span><span class="ss">:name</span><span class="p">],</span> <span class="ss">relevance: </span><span class="n">relevance</span><span class="p">(</span><span class="n">query</span><span class="p">,</span> <span class="n">document</span><span class="p">[</span><span class="ss">:content</span><span class="p">],</span> <span class="n">document_contents</span><span class="p">),</span> <span class="ss">content: </span><span class="n">document</span><span class="p">[</span><span class="ss">:content</span><span class="p">]</span> <span class="p">}</span> <span class="p">}</span>
      <span class="p">.</span><span class="nf">sort_by</span> <span class="p">{</span> <span class="o">|</span><span class="n">document</span><span class="o">|</span> <span class="o">-</span><span class="n">document</span><span class="p">[</span><span class="ss">:relevance</span><span class="p">]</span> <span class="p">}</span>
  <span class="k">end</span>

  <span class="kp">private</span>

  <span class="k">def</span> <span class="nf">clean</span><span class="p">(</span><span class="n">document</span><span class="p">)</span>
    <span class="n">document</span><span class="p">.</span><span class="nf">gsub</span><span class="p">(</span><span class="sr">/[[:punct:]]/</span><span class="p">,</span> <span class="s2">" "</span><span class="p">).</span><span class="nf">downcase</span>
  <span class="k">end</span>

  <span class="k">def</span> <span class="nf">n_grams</span><span class="p">(</span><span class="n">document</span><span class="p">,</span> <span class="ss">min_n: </span><span class="mi">2</span><span class="p">,</span> <span class="ss">max_n: </span><span class="mi">5</span><span class="p">)</span>
    <span class="n">words</span> <span class="o">=</span> <span class="n">document</span><span class="p">.</span><span class="nf">split</span>
    <span class="n">terms</span> <span class="o">=</span> <span class="p">[]</span>
    <span class="n">words</span><span class="p">.</span><span class="nf">each</span> <span class="k">do</span> <span class="o">|</span><span class="n">word</span><span class="o">|</span>
      <span class="c1"># the word can be smaller than the max window size like the word 'and' so we take the minimum.</span>
      <span class="n">max_window_size</span> <span class="o">=</span> <span class="p">[</span><span class="n">max_n</span><span class="p">,</span> <span class="n">word</span><span class="p">.</span><span class="nf">length</span><span class="p">].</span><span class="nf">min</span>
      <span class="p">(</span><span class="n">min_n</span><span class="o">..</span><span class="n">max_window_size</span><span class="p">).</span><span class="nf">each</span> <span class="k">do</span> <span class="o">|</span><span class="n">window_size</span><span class="o">|</span>
        <span class="n">terms</span> <span class="o">&lt;&lt;</span> <span class="n">word</span><span class="p">[</span><span class="o">...</span><span class="n">window_size</span><span class="p">]</span>
      <span class="k">end</span>
    <span class="k">end</span>
    <span class="n">terms</span>
  <span class="k">end</span>

  <span class="k">def</span> <span class="nf">importance</span><span class="p">(</span><span class="n">term</span><span class="p">,</span> <span class="n">documents</span><span class="p">)</span>
    <span class="n">number_of_documents</span> <span class="o">=</span> <span class="n">documents</span><span class="p">.</span><span class="nf">count</span>
    <span class="c1"># `count` method can take a block and count the number of times the block returns true.</span>
    <span class="n">number_of_documents_containing_term</span> <span class="o">=</span> <span class="n">documents</span><span class="p">.</span><span class="nf">count</span> <span class="p">{</span> <span class="o">|</span><span class="n">document</span><span class="o">|</span> <span class="n">n_grams</span><span class="p">(</span><span class="n">document</span><span class="p">).</span><span class="nf">include?</span><span class="p">(</span><span class="n">term</span><span class="p">)</span> <span class="p">}</span>
    <span class="no">Math</span><span class="p">.</span><span class="nf">log</span><span class="p">((</span><span class="n">number_of_documents</span> <span class="o">+</span> <span class="mi">1</span><span class="p">)</span> <span class="o">/</span> <span class="p">(</span><span class="n">number_of_documents_containing_term</span> <span class="o">+</span> <span class="mi">1</span><span class="p">).</span><span class="nf">to_f</span><span class="p">)</span>
  <span class="k">end</span>

  <span class="k">def</span> <span class="nf">relevance</span><span class="p">(</span><span class="n">query</span><span class="p">,</span> <span class="n">document</span><span class="p">,</span> <span class="n">documents</span><span class="p">)</span>
    <span class="c1"># Instead of just using `.split` we generate the n-grams and work on that.</span>
    <span class="n">number_of_terms</span> <span class="o">=</span> <span class="n">n_grams</span><span class="p">(</span><span class="n">document</span><span class="p">).</span><span class="nf">count</span>
    <span class="n">query_terms</span> <span class="o">=</span> <span class="n">n_grams</span><span class="p">(</span><span class="n">query</span><span class="p">)</span>
    <span class="n">query_terms</span>
      <span class="p">.</span><span class="nf">map</span> <span class="p">{</span> <span class="o">|</span><span class="n">term</span><span class="o">|</span> <span class="n">n_grams</span><span class="p">(</span><span class="n">document</span><span class="p">).</span><span class="nf">count</span><span class="p">(</span><span class="n">term</span><span class="p">)</span> <span class="o">*</span> <span class="n">importance</span><span class="p">(</span><span class="n">term</span><span class="p">,</span> <span class="n">documents</span><span class="p">)</span> <span class="p">}</span>
      <span class="p">.</span><span class="nf">sum</span> <span class="o">/</span> <span class="n">number_of_terms</span><span class="p">.</span><span class="nf">to_f</span>
  <span class="k">end</span>
<span class="k">end</span>
</code></pre></div></div>

<h2 id="performance">Performance</h2>

<p>There is lots of data processing. A systems programming language like C, Rust, or Zig will be a better choice.</p>

<p>There is another optimization we could do with the current version. We process the documents for every query from scratch. Most of the time, the documents we’re searching for will largely stay the same relative to the queries.</p>

<p>We can preprocess the documents and store the n-grams and the importance of each term. This way, we can just use the stored data and calculate the relevance of the query. Most of the work will be a lookup.</p>

<p>OK, how are we going to store this preprocessed data? The trick is to look at the <code class="language-plaintext highlighter-rouge">relevance</code> method.
<code class="language-plaintext highlighter-rouge">number_of_terms = n_grams(document).count</code>:
We need a mapping between documents and their number of terms. We can have a <em>documents</em> collection and store the number of terms for each document. This document could look like this:</p>
<div class="language-json highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">{</span><span class="w">
  </span><span class="nl">"_id"</span><span class="p">:</span><span class="w"> </span><span class="s2">"first_document"</span><span class="p">,</span><span class="w">
  </span><span class="nl">"content"</span><span class="p">:</span><span class="w"> </span><span class="s2">"Peter,</span><span class="se">\n\n</span><span class="s2">I'm going to need those TPS reports on my desk first thing tomorrow! And clean up your desk!</span><span class="se">\n\n</span><span class="s2">Lumbergh"</span><span class="p">,</span><span class="w">
  </span><span class="nl">"number_of_terms"</span><span class="p">:</span><span class="w"> </span><span class="mi">20</span><span class="w">
</span><span class="p">}</span><span class="w">
</span></code></pre></div></div>

<p>In the web, <code class="language-plaintext highlighter-rouge">_id</code> would be the URL of the document, but here we’re using a simple string. The database can index based on the <code class="language-plaintext highlighter-rouge">_id</code> field, so we can quickly access the document.</p>

<p><code class="language-plaintext highlighter-rouge">query_terms = n_grams(query)</code>:
We can’t do much about this because the query is completely dynamic.</p>

<p><code class="language-plaintext highlighter-rouge">n_grams(document).count(term)</code>:
We need a mapping between terms and appearances. Since a term can appear in multiple documents, we need to have another mapping between document names and the number of appearances of the term. This structure could look like this:</p>
<div class="language-json highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">{</span><span class="w">
  </span><span class="nl">"_id"</span><span class="p">:</span><span class="w"> </span><span class="s2">"tes"</span><span class="p">,</span><span class="w">
  </span><span class="nl">"appearances"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
    </span><span class="nl">"first_document"</span><span class="p">:</span><span class="w"> </span><span class="mi">2</span><span class="p">,</span><span class="w">
    </span><span class="nl">"second_document"</span><span class="p">:</span><span class="w"> </span><span class="mi">0</span><span class="p">,</span><span class="w">
    </span><span class="nl">"third_document"</span><span class="p">:</span><span class="w"> </span><span class="mi">1</span><span class="w">
  </span><span class="p">}</span><span class="w">
</span><span class="p">}</span><span class="w">
</span></code></pre></div></div>

<p>Here I’m using the term’s name as the <code class="language-plaintext highlighter-rouge">_id</code> because it’s unique.</p>

<p><code class="language-plaintext highlighter-rouge">importance(term, documents)</code>:
We can just add another field to the above structure:</p>

<div class="language-diff highlighter-rouge"><div class="highlight"><pre class="highlight"><code>{
  "name": "tes",
  "appearances": {
    "first_document": 2,
    "second_document": 0,
    "third_document": 1
  },
<span class="gi">+ "importance": 0.5
</span>}
</code></pre></div></div>

<p>By the way, this is called an inverted index. You can read more about it on <a href="https://en.wikipedia.org/wiki/Inverted_index">Wikipedia</a>.</p>

<p>Let’s put these into code. I’m going to use MongoDB because its document structure is already JSON-like.</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">def</span> <span class="nf">initialize</span><span class="p">(</span><span class="n">url</span><span class="p">)</span>
  <span class="vi">@client</span> <span class="o">=</span> <span class="no">Mongo</span><span class="o">::</span><span class="no">Client</span><span class="p">.</span><span class="nf">new</span><span class="p">(</span><span class="n">url</span><span class="p">)</span>
<span class="k">end</span>
</code></pre></div></div>

<p><code class="language-plaintext highlighter-rouge">initialize</code> method is going to take a URL to connect to the MongoDB server.</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">def</span> <span class="nf">search</span><span class="p">(</span><span class="n">query</span><span class="p">)</span>
  <span class="n">document_contents</span> <span class="o">=</span> <span class="vi">@client</span><span class="p">[</span><span class="ss">:documents</span><span class="p">].</span><span class="nf">find</span><span class="p">.</span><span class="nf">map</span> <span class="p">{</span> <span class="o">|</span><span class="n">document</span><span class="o">|</span> <span class="n">document</span><span class="p">[</span><span class="ss">:content</span><span class="p">]</span> <span class="p">}</span>
  <span class="vi">@client</span><span class="p">[</span><span class="ss">:documents</span><span class="p">]</span>
    <span class="p">.</span><span class="nf">find</span>
    <span class="p">.</span><span class="nf">map</span> <span class="p">{</span> <span class="o">|</span><span class="n">document</span><span class="o">|</span> <span class="p">{</span> <span class="ss">name: </span><span class="n">document</span><span class="p">[</span><span class="ss">:_id</span><span class="p">],</span> <span class="ss">relevance: </span><span class="n">relevance</span><span class="p">(</span><span class="n">query</span><span class="p">,</span> <span class="n">document</span><span class="p">),</span> <span class="ss">content: </span><span class="n">document</span><span class="p">[</span><span class="ss">:content</span><span class="p">]</span> <span class="p">}</span> <span class="p">}</span>
    <span class="p">.</span><span class="nf">sort_by</span> <span class="p">{</span> <span class="o">|</span><span class="n">document</span><span class="o">|</span> <span class="o">-</span><span class="n">document</span><span class="p">[</span><span class="ss">:relevance</span><span class="p">]</span> <span class="p">}</span>
    <span class="p">.</span><span class="nf">reject</span> <span class="p">{</span> <span class="o">|</span><span class="n">document</span><span class="o">|</span> <span class="n">document</span><span class="p">[</span><span class="ss">:relevance</span><span class="p">]</span> <span class="o">==</span> <span class="mi">0</span> <span class="p">}</span>
<span class="k">end</span>
</code></pre></div></div>

<p><code class="language-plaintext highlighter-rouge">search</code> is almost the same. We get the documents from database instead of an instance variable.</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">def</span> <span class="nf">add_document</span><span class="p">(</span><span class="nb">name</span><span class="p">,</span> <span class="n">document</span><span class="p">)</span>
  <span class="n">document</span> <span class="o">=</span> <span class="n">clean</span><span class="p">(</span><span class="n">document</span><span class="p">)</span>
  <span class="n">terms</span> <span class="o">=</span> <span class="n">n_grams</span><span class="p">(</span><span class="n">document</span><span class="p">)</span>
  <span class="n">number_of_terms</span> <span class="o">=</span> <span class="n">terms</span><span class="p">.</span><span class="nf">count</span>
  <span class="vi">@client</span><span class="p">[</span><span class="ss">:documents</span><span class="p">].</span><span class="nf">insert_one</span><span class="p">({</span> <span class="ss">_id: </span><span class="nb">name</span><span class="p">,</span> <span class="ss">content: </span><span class="n">document</span><span class="p">,</span> <span class="ss">number_of_terms: </span><span class="n">number_of_terms</span> <span class="p">})</span>
  <span class="n">terms</span><span class="p">.</span><span class="nf">each</span> <span class="k">do</span> <span class="o">|</span><span class="n">term</span><span class="o">|</span>
    <span class="vi">@client</span><span class="p">[</span><span class="ss">:terms</span><span class="p">].</span><span class="nf">update_one</span><span class="p">(</span>
      <span class="p">{</span> <span class="ss">_id: </span><span class="n">term</span> <span class="p">},</span>
      <span class="c1"># Increment the count of this document by one.</span>
      <span class="p">{</span> <span class="s2">"$inc"</span><span class="p">:</span> <span class="p">{</span> <span class="s2">"appearances.</span><span class="si">#{</span><span class="nb">name</span><span class="si">}</span><span class="s2">"</span><span class="p">:</span> <span class="mi">1</span> <span class="p">}</span> <span class="p">},</span>
      <span class="ss">upsert: </span><span class="kp">true</span><span class="p">,</span>
    <span class="p">)</span>
  <span class="k">end</span>
<span class="k">end</span>
</code></pre></div></div>

<p>Here after we save the document, for each term we increment the count of the document by one or set it to one if it doesn’t exist.</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">def</span> <span class="nf">importance</span><span class="p">(</span><span class="n">term</span><span class="p">)</span>
  <span class="n">appearances</span> <span class="o">=</span> <span class="vi">@client</span><span class="p">[</span><span class="ss">:terms</span><span class="p">].</span><span class="nf">find</span><span class="p">(</span><span class="ss">_id: </span><span class="n">term</span><span class="p">).</span><span class="nf">first</span><span class="p">[</span><span class="s2">"appearances"</span><span class="p">]</span>
  <span class="n">number_of_documents</span> <span class="o">=</span> <span class="vi">@client</span><span class="p">[</span><span class="ss">:documents</span><span class="p">].</span><span class="nf">count</span>
  <span class="n">number_of_documents_containing_term</span> <span class="o">=</span> <span class="n">appearances</span><span class="p">.</span><span class="nf">count</span>
  <span class="no">Math</span><span class="p">.</span><span class="nf">log</span><span class="p">((</span><span class="n">number_of_documents</span> <span class="o">+</span> <span class="mi">1</span><span class="p">)</span> <span class="o">/</span> <span class="p">(</span><span class="n">number_of_documents_containing_term</span> <span class="o">+</span> <span class="mi">1</span><span class="p">).</span><span class="nf">to_f</span><span class="p">)</span>
<span class="k">end</span>
</code></pre></div></div>

<p>Not much to say here. We don’t have to search through the entire documents to find how many documents contain the term. We can just look at the appearances field’s length.</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">def</span> <span class="nf">relevance</span><span class="p">(</span><span class="n">query</span><span class="p">,</span> <span class="n">document</span><span class="p">)</span>
  <span class="n">number_of_terms</span> <span class="o">=</span> <span class="vi">@client</span><span class="p">[</span><span class="ss">:documents</span><span class="p">].</span><span class="nf">find</span><span class="p">(</span><span class="ss">_id: </span><span class="n">document</span><span class="p">[</span><span class="ss">:_id</span><span class="p">]).</span><span class="nf">first</span><span class="p">[</span><span class="s2">"number_of_terms"</span><span class="p">]</span>
  <span class="n">query_terms</span> <span class="o">=</span> <span class="n">n_grams</span><span class="p">(</span><span class="n">query</span><span class="p">)</span>
  <span class="n">query_terms</span><span class="p">.</span><span class="nf">map</span> <span class="p">{</span> <span class="o">|</span><span class="n">term</span><span class="o">|</span> <span class="n">term_relevance</span><span class="p">(</span><span class="n">term</span><span class="p">,</span> <span class="n">document</span><span class="p">[</span><span class="ss">:_id</span><span class="p">])</span> <span class="p">}.</span><span class="nf">sum</span> <span class="o">/</span> <span class="n">number_of_terms</span><span class="p">.</span><span class="nf">to_f</span>
<span class="k">end</span>

<span class="k">def</span> <span class="nf">term_relevance</span><span class="p">(</span><span class="n">term</span><span class="p">,</span> <span class="n">document_id</span><span class="p">)</span>
  <span class="n">appearances</span> <span class="o">=</span> <span class="vi">@client</span><span class="p">[</span><span class="ss">:terms</span><span class="p">].</span><span class="nf">find</span><span class="p">(</span><span class="ss">_id: </span><span class="n">term</span><span class="p">).</span><span class="nf">first</span><span class="p">[</span><span class="s2">"appearances"</span><span class="p">]</span>
  <span class="n">number_of_terms</span> <span class="o">=</span> <span class="vi">@client</span><span class="p">[</span><span class="ss">:documents</span><span class="p">].</span><span class="nf">find</span><span class="p">(</span><span class="ss">_id: </span><span class="n">document_id</span><span class="p">).</span><span class="nf">first</span><span class="p">[</span><span class="s2">"number_of_terms"</span><span class="p">]</span>
  <span class="k">if</span> <span class="n">appearances</span><span class="p">.</span><span class="nf">nil?</span> <span class="o">||</span> <span class="n">appearances</span><span class="p">[</span><span class="n">document_id</span><span class="p">].</span><span class="nf">nil?</span>
    <span class="k">return</span> <span class="mi">0</span>
  <span class="k">end</span>
  <span class="n">importance</span><span class="p">(</span><span class="n">term</span><span class="p">)</span> <span class="o">*</span> <span class="n">appearances</span><span class="p">[</span><span class="n">document_id</span><span class="p">]</span>
<span class="k">end</span>
</code></pre></div></div>

<p><code class="language-plaintext highlighter-rouge">relevance</code> doesn’t have to take the documents as an argument now; it can just look at the database. I refactored the block that calculates the relevance of a term into a separate method called <code class="language-plaintext highlighter-rouge">term_relevance</code> for readability. If the term doesn’t appear in the document, we return 0; otherwise, we calculate the relevance as before by multiplying the importance by the number of appearances.</p>

<p>The full implementation looks like this:</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">require</span> <span class="s2">"mongo"</span>

<span class="k">class</span> <span class="nc">SearchEngine</span>
  <span class="k">def</span> <span class="nf">initialize</span><span class="p">(</span><span class="n">url</span><span class="p">)</span>
    <span class="vi">@client</span> <span class="o">=</span> <span class="no">Mongo</span><span class="o">::</span><span class="no">Client</span><span class="p">.</span><span class="nf">new</span><span class="p">(</span><span class="n">url</span><span class="p">)</span>
  <span class="k">end</span>

  <span class="k">def</span> <span class="nf">add_document</span><span class="p">(</span><span class="nb">name</span><span class="p">,</span> <span class="n">document</span><span class="p">)</span>
    <span class="n">document</span> <span class="o">=</span> <span class="n">clean</span><span class="p">(</span><span class="n">document</span><span class="p">)</span>
    <span class="n">terms</span> <span class="o">=</span> <span class="n">n_grams</span><span class="p">(</span><span class="n">document</span><span class="p">)</span>
    <span class="n">number_of_terms</span> <span class="o">=</span> <span class="n">terms</span><span class="p">.</span><span class="nf">count</span>
    <span class="vi">@client</span><span class="p">[</span><span class="ss">:documents</span><span class="p">].</span><span class="nf">insert_one</span><span class="p">({</span> <span class="ss">_id: </span><span class="nb">name</span><span class="p">,</span> <span class="ss">content: </span><span class="n">document</span><span class="p">,</span> <span class="ss">number_of_terms: </span><span class="n">number_of_terms</span> <span class="p">})</span>
    <span class="n">terms</span><span class="p">.</span><span class="nf">each</span> <span class="k">do</span> <span class="o">|</span><span class="n">term</span><span class="o">|</span>
      <span class="vi">@client</span><span class="p">[</span><span class="ss">:terms</span><span class="p">].</span><span class="nf">update_one</span><span class="p">(</span>
        <span class="p">{</span> <span class="ss">_id: </span><span class="n">term</span> <span class="p">},</span>
        <span class="c1"># Increment the count of this document by one.</span>
        <span class="p">{</span> <span class="s2">"$inc"</span><span class="p">:</span> <span class="p">{</span> <span class="s2">"appearances.</span><span class="si">#{</span><span class="nb">name</span><span class="si">}</span><span class="s2">"</span><span class="p">:</span> <span class="mi">1</span> <span class="p">}</span> <span class="p">},</span>
        <span class="ss">upsert: </span><span class="kp">true</span><span class="p">,</span>
      <span class="p">)</span>
    <span class="k">end</span>
  <span class="k">end</span>

  <span class="k">def</span> <span class="nf">search</span><span class="p">(</span><span class="n">query</span><span class="p">)</span>
    <span class="n">document_contents</span> <span class="o">=</span> <span class="vi">@client</span><span class="p">[</span><span class="ss">:documents</span><span class="p">].</span><span class="nf">find</span><span class="p">.</span><span class="nf">map</span> <span class="p">{</span> <span class="o">|</span><span class="n">document</span><span class="o">|</span> <span class="n">document</span><span class="p">[</span><span class="ss">:content</span><span class="p">]</span> <span class="p">}</span>
    <span class="vi">@client</span><span class="p">[</span><span class="ss">:documents</span><span class="p">]</span>
      <span class="p">.</span><span class="nf">find</span>
      <span class="p">.</span><span class="nf">map</span> <span class="p">{</span> <span class="o">|</span><span class="n">document</span><span class="o">|</span> <span class="p">{</span> <span class="ss">name: </span><span class="n">document</span><span class="p">[</span><span class="ss">:_id</span><span class="p">],</span> <span class="ss">relevance: </span><span class="n">relevance</span><span class="p">(</span><span class="n">query</span><span class="p">,</span> <span class="n">document</span><span class="p">),</span> <span class="ss">content: </span><span class="n">document</span><span class="p">[</span><span class="ss">:content</span><span class="p">]</span> <span class="p">}</span> <span class="p">}</span>
      <span class="p">.</span><span class="nf">sort_by</span> <span class="p">{</span> <span class="o">|</span><span class="n">document</span><span class="o">|</span> <span class="o">-</span><span class="n">document</span><span class="p">[</span><span class="ss">:relevance</span><span class="p">]</span> <span class="p">}</span>
      <span class="p">.</span><span class="nf">reject</span> <span class="p">{</span> <span class="o">|</span><span class="n">document</span><span class="o">|</span> <span class="n">document</span><span class="p">[</span><span class="ss">:relevance</span><span class="p">]</span> <span class="o">==</span> <span class="mi">0</span> <span class="p">}</span>
  <span class="k">end</span>

  <span class="kp">private</span>

  <span class="k">def</span> <span class="nf">clean</span><span class="p">(</span><span class="n">document</span><span class="p">)</span>
    <span class="n">document</span><span class="p">.</span><span class="nf">gsub</span><span class="p">(</span><span class="sr">/[[:punct:]]/</span><span class="p">,</span> <span class="s2">" "</span><span class="p">).</span><span class="nf">downcase</span>
  <span class="k">end</span>

  <span class="k">def</span> <span class="nf">n_grams</span><span class="p">(</span><span class="n">document</span><span class="p">,</span> <span class="ss">min_n: </span><span class="mi">2</span><span class="p">,</span> <span class="ss">max_n: </span><span class="mi">5</span><span class="p">)</span>
    <span class="n">words</span> <span class="o">=</span> <span class="n">document</span><span class="p">.</span><span class="nf">split</span>
    <span class="n">terms</span> <span class="o">=</span> <span class="p">[]</span>
    <span class="n">words</span><span class="p">.</span><span class="nf">each</span> <span class="k">do</span> <span class="o">|</span><span class="n">word</span><span class="o">|</span>
      <span class="c1"># the word can be smaller than the max window size like the word 'and' so we take the minimum.</span>
      <span class="n">max_window_size</span> <span class="o">=</span> <span class="p">[</span><span class="n">max_n</span><span class="p">,</span> <span class="n">word</span><span class="p">.</span><span class="nf">length</span><span class="p">].</span><span class="nf">min</span>
      <span class="p">(</span><span class="n">min_n</span><span class="o">..</span><span class="n">max_window_size</span><span class="p">).</span><span class="nf">each</span> <span class="k">do</span> <span class="o">|</span><span class="n">window_size</span><span class="o">|</span>
        <span class="n">terms</span> <span class="o">&lt;&lt;</span> <span class="n">word</span><span class="p">[</span><span class="o">...</span><span class="n">window_size</span><span class="p">]</span>
      <span class="k">end</span>
    <span class="k">end</span>
    <span class="n">terms</span>
  <span class="k">end</span>

  <span class="k">def</span> <span class="nf">importance</span><span class="p">(</span><span class="n">term</span><span class="p">)</span>
    <span class="n">appearances</span> <span class="o">=</span> <span class="vi">@client</span><span class="p">[</span><span class="ss">:terms</span><span class="p">].</span><span class="nf">find</span><span class="p">(</span><span class="ss">_id: </span><span class="n">term</span><span class="p">).</span><span class="nf">first</span><span class="p">[</span><span class="s2">"appearances"</span><span class="p">]</span>
    <span class="n">number_of_documents</span> <span class="o">=</span> <span class="vi">@client</span><span class="p">[</span><span class="ss">:documents</span><span class="p">].</span><span class="nf">count</span>
    <span class="n">number_of_documents_containing_term</span> <span class="o">=</span> <span class="n">appearances</span><span class="p">.</span><span class="nf">count</span>
    <span class="no">Math</span><span class="p">.</span><span class="nf">log</span><span class="p">((</span><span class="n">number_of_documents</span> <span class="o">+</span> <span class="mi">1</span><span class="p">)</span> <span class="o">/</span> <span class="p">(</span><span class="n">number_of_documents_containing_term</span> <span class="o">+</span> <span class="mi">1</span><span class="p">).</span><span class="nf">to_f</span><span class="p">)</span>
  <span class="k">end</span>

  <span class="k">def</span> <span class="nf">relevance</span><span class="p">(</span><span class="n">query</span><span class="p">,</span> <span class="n">document</span><span class="p">)</span>
    <span class="n">number_of_terms</span> <span class="o">=</span> <span class="vi">@client</span><span class="p">[</span><span class="ss">:documents</span><span class="p">].</span><span class="nf">find</span><span class="p">(</span><span class="ss">_id: </span><span class="n">document</span><span class="p">[</span><span class="ss">:_id</span><span class="p">]).</span><span class="nf">first</span><span class="p">[</span><span class="s2">"number_of_terms"</span><span class="p">]</span>
    <span class="n">query_terms</span> <span class="o">=</span> <span class="n">n_grams</span><span class="p">(</span><span class="n">query</span><span class="p">)</span>
    <span class="n">query_terms</span><span class="p">.</span><span class="nf">map</span> <span class="p">{</span> <span class="o">|</span><span class="n">term</span><span class="o">|</span> <span class="n">term_relevance</span><span class="p">(</span><span class="n">term</span><span class="p">,</span> <span class="n">document</span><span class="p">[</span><span class="ss">:_id</span><span class="p">])</span> <span class="p">}.</span><span class="nf">sum</span> <span class="o">/</span> <span class="n">number_of_terms</span><span class="p">.</span><span class="nf">to_f</span>
  <span class="k">end</span>

  <span class="k">def</span> <span class="nf">term_relevance</span><span class="p">(</span><span class="n">term</span><span class="p">,</span> <span class="n">document_id</span><span class="p">)</span>
    <span class="n">appearances</span> <span class="o">=</span> <span class="vi">@client</span><span class="p">[</span><span class="ss">:terms</span><span class="p">].</span><span class="nf">find</span><span class="p">(</span><span class="ss">_id: </span><span class="n">term</span><span class="p">).</span><span class="nf">first</span><span class="p">[</span><span class="s2">"appearances"</span><span class="p">]</span>
    <span class="n">number_of_terms</span> <span class="o">=</span> <span class="vi">@client</span><span class="p">[</span><span class="ss">:documents</span><span class="p">].</span><span class="nf">find</span><span class="p">(</span><span class="ss">_id: </span><span class="n">document_id</span><span class="p">).</span><span class="nf">first</span><span class="p">[</span><span class="s2">"number_of_terms"</span><span class="p">]</span>
    <span class="k">if</span> <span class="n">appearances</span><span class="p">.</span><span class="nf">nil?</span> <span class="o">||</span> <span class="n">appearances</span><span class="p">[</span><span class="n">document_id</span><span class="p">].</span><span class="nf">nil?</span>
      <span class="k">return</span> <span class="mi">0</span>
    <span class="k">end</span>
    <span class="n">importance</span><span class="p">(</span><span class="n">term</span><span class="p">)</span> <span class="o">*</span> <span class="n">appearances</span><span class="p">[</span><span class="n">document_id</span><span class="p">]</span>
  <span class="k">end</span>
<span class="k">end</span>
</code></pre></div></div>

<h2 id="conclusion">Conclusion</h2>

<p>If you want to go deeper into search engines, you should look at <a href="https://en.wikipedia.org/wiki/Okapi_BM25">BM25</a>, <a href="https://en.wikipedia.org/wiki/PageRank">PageRank</a>, <a href="https://en.wikipedia.org/wiki/Word2vec">Word2Vec</a>. Maybe a next step would be using AI models but I’m not sure if this improves the search experience. For last few years current search engines gets shittier every day. Maybe it’s not such a good idea.</p>

<h2 id="inspiration">Inspiration</h2>

<ul>
  <li><a href="https://youtu.be/cY7pE7vX6MU?si=bV91B-T-rjK4hetr">Building A Python-Based Search Engine</a>. An excellent conference talk by Daniel Lindsley on building a search engine in Python. I’m heavily inspired by this talk. You can take a look at his source code <a href="https://github.com/toastdriven/microsearch/">here</a>.</li>
  <li><a href="https://www.youtube.com/watch?v=hm5xOJiVEeg&amp;list=PLpM-Dvs8t0VZXC-91PpIp-eAt0WF5SKEv">Search Engine in Rust by Tsoding</a>.</li>
</ul>]]></content><author><name></name></author><summary type="html"><![CDATA[Let’s define the problem at a high level first.]]></summary></entry></feed>