Study Vault Search notes, aliases and full text
Graph
LectureDBISAI-written · not yet proofread

VL08 Fuzzy Search, Prefix Edit Distance, Q-Gram Index

Error-tolerant (prefix) search over a dictionary using edit distance, prefix edit distance, the q-gram lemma and its corollaries, and a q-gram index for fast candidate filtering.

Held Source lecture-08

Organisation#

Problem setting#

Given a dictionary DD, a list of “names” of any kind (for ES8: 1,736,613 Wikidata entities), find the names from DD that match a query (Slide 5, Fuzzy Search):

QueryMatchKind
freifreiburgprefix search
breifurgfreiburgfuzzy search
breifreiburgfuzzy prefix search

The lecture solves the last two; normal prefix search is a special case. As in Lectures 1–2 there are two challenges:

  1. a good model of relevance (what matches), here (P)ED,
  2. an index to find the matches fast, here the q-gram index.

Possible origins of the dictionary (Slide 6): popular queries from a query log (basis of Google’s query suggestions); words and common phrases of a text collection (phrase extraction is a problem of its own, not covered); entities of a knowledge graph (ES8: Wikidata entities with popularity and synonyms).

Simple solution: linear scan#

Iterate over all strings of the dictionary and check each one (Slide 7). The command-line tools grep and agrep do this:

grep -x frei.* <file>          # -x: match whole line
agrep -x -2 breifurg <file>    # -2: allow up to 2 "errors"
agrep -x -2 brei.* <file>

This takes time linear in the size of the dictionary: good enough for medium-sized, not for large dictionaries. The rest of the lecture precomputes an index to do better.

Edit Distance#

Notation (Slide 9): ε\varepsilon is the empty word, ∣x∣|x| the length (number of characters) of xx, and x[i..j]x[i..j] the substring from position ii to jj, 1≤i≤j≤∣x∣1 \le i \le j \le |x|. Below, n:=∣x∣n := |x| and m:=∣y∣m := |y|.

ProofProof sketches (the slides recommend proving these as exam preparation)
  1. Every operation can be undone by one operation (insert ↔ delete, replace ↔ replace). Reversing an optimal sequence from xx to yy gives a sequence of the same length from yy to xx, so ED(y,x)≤ED(x,y)\mathrm{ED}(y,x) \le \mathrm{ED}(x,y), and vice versa.
  2. ∣x∣|x| deletions suffice. Each operation reduces the length by at most 1, so at least ∣x∣|x| operations are needed.
  3. Each operation changes the length by at most 1, and the length must change from ∣x∣|x| to ∣y∣|y|.
  4. Apply an optimal sequence for x[1..n−1]→y[1..m−1]x[1..n-1] \to y[1..m-1] to the first n−1n-1 characters of xx; this leaves x[n]x[n] as the last character. Then one replace turns x[n]x[n] into y[m]y[m] (or nothing is needed if x[n]=y[m]x[n] = y[m]).

Recursive formula#

Case (1) corresponds to inserting y[m]y[m] at the end, (2) to deleting x[n]x[n], (3a) to replacing x[n]x[n] by y[m]y[m], (3b) to keeping a matching last character.

Dynamic programming algorithm#

The recurrence leads to a DP algorithm: fill a table whose entry in row ii, column jj is ED(x[1..i],y[1..j])\mathrm{ED}(x[1..i], y[1..j]) (rows: prefixes of xx, columns: prefixes of yy, row/column 0 = ε\varepsilon). Each entry depends only on its left, upper and upper-left neighbour. The result is the bottom-right entry (Slide 11).

  • Time and space: Θ(∣x∣⋅∣y∣)\Theta(|x| \cdot |y|).
  • Space O(min⁡{∣x∣,∣y∣})O(\min\{|x|, |y|\}) also works: only the previous row is needed (row/column roles can be swapped).
lecture-08, PDF page 11
lecture-08, PDF page 11

Prefix Edit Distance#

PED is the relevance model for fuzzy search-as-you-type suggestions; all large web search engines have this feature because it is so convenient. The entity search on wikidata.org does not have it.

Computation#

(Slide 13)

  • Compute the same table as for ED.
  • The PED is the minimum of the entries in the last row (instead of the bottom-right entry), since entry (∣x∣,j)(|x|, j) is ED(x,y[1..j])\mathrm{ED}(x, y[1..j]).
  • Optimization: if ∣x∣≪∣y∣|x| \ll |y| and we only want to know whether PED(x,y)≤δ\mathrm{PED}(x, y) \le \delta for a given threshold δ\delta, it suffices to compute the first ∣x∣+δ+1|x| + \delta + 1 columns (column ε\varepsilon and the prefixes y[1..j]y[1..j] for j≤∣x∣+δj \le |x| + \delta).
ProofWhy the first ∣x∣+δ+1|x| + \delta + 1 columns suffice

For j>∣x∣+δj > |x| + \delta, property 3 of ED gives ED(x,y[1..j])≥j−∣x∣>δ\mathrm{ED}(x, y[1..j]) \ge j - |x| > \delta. So no column to the right can contribute an entry ≤δ\le \delta to the last row: if PED(x,y)≤δ\mathrm{PED}(x, y) \le \delta, the minimum is attained among the first ∣x∣+δ+1|x| + \delta + 1 columns, i.e. PED(x,y)=PED(x,y[1..∣x∣+δ])\mathrm{PED}(x, y) = \mathrm{PED}(x, y[1..|x|+\delta]).

lecture-08, PDF page 13
lecture-08, PDF page 13

Implementation for ES8#

The PED is performance-critical, so ES8 provides it (and merge_lists) ready-made: a Rust implementation (pip install ad-freiburg-qgram-utils) and a Python fallback; the code template uses Rust if installed (Slide 14).

Fuzzy Search with Q-Grams#

Motivation#

Goal: find fuzzy matches of xx in DD faster than by computing ED(x,y)\mathrm{ED}(x, y) or PED(x,y)\mathrm{PED}(x, y) for all y∈Dy \in D. Intuition: strings with a small edit distance overlap in many of their parts. ED(freiburg,stuttgart)\mathrm{ED}(\text{freiburg}, \text{stuttgart}) is large, the strings differ “everywhere”; ED(freiburg,breifurg)=2\mathrm{ED}(\text{freiburg}, \text{breifurg}) = 2 is small, the strings agree “at many places” (Slide 15).

Q-grams#

The q-gram lemma#

Proof

Case ED(x,y)=1\mathrm{ED}(x, y) = 1: one edit operation changes at most qq q-grams (those that overlap the edited position), hence ∣Qq(x)∖Qq(y)∣≤q|Q_q(x) \setminus Q_q(y)| \le q.

General case ED(x,y)=k\mathrm{ED}(x, y) = k: let x=x0→x1→⋯→xk=yx = x_0 \to x_1 \to \dots \to x_k = y be a sequence of kk edit operations, so ED(xi−1,xi)=1\mathrm{ED}(x_{i-1}, x_i) = 1 and therefore ∣Qq(xi−1)∖Qq(xi)∣≤q|Q_q(x_{i-1}) \setminus Q_q(x_i)| \le q. It remains to show that for any sequence of sets A0,…,AkA_0, \dots, A_k

A0∖Ak⊆⋃i=1k(Ai−1∖Ai).A_0 \setminus A_k \subseteq \bigcup_{i=1}^{k} \bigl(A_{i-1} \setminus A_i\bigr).

Let a∈A0∖Aka \in A_0 \setminus A_k and let i≥1i \ge 1 be the smallest index with a∉Aia \notin A_i (it exists because a∉Aka \notin A_k). Then a∈Ai−1a \in A_{i-1}, so a∈Ai−1∖Aia \in A_{i-1} \setminus A_i. With Ai:=Qq(xi)A_i := Q_q(x_i):

∣Qq(x)∖Qq(y)∣≤∑i=1k∣Qq(xi−1)∖Qq(xi)∣≤k⋅q.|Q_q(x) \setminus Q_q(y)| \le \sum_{i=1}^{k} |Q_q(x_{i-1}) \setminus Q_q(x_i)| \le k \cdot q.

(The last step after the inclusion is the usual completion; the slides end with the inclusion.)

Corollaries: lower bounds on common q-grams#

Proof

For arbitrary sets A,BA, B: ∣A∖B∣=∣A∣−∣A∩B∣|A \setminus B| = |A| - |A \cap B|. With the lemma:

∣Qq(x)∣−∣Qq(x)∩Qq(y)∣=∣Qq(x)∖Qq(y)∣≤q⋅ED(x,y).|Q_q(x)| - |Q_q(x) \cap Q_q(y)| = |Q_q(x) \setminus Q_q(y)| \le q \cdot \mathrm{ED}(x, y).

Since ED(x,y)=ED(y,x)\mathrm{ED}(x, y) = \mathrm{ED}(y, x), the lemma applied to (y,x)(y, x) gives

∣Qq(y)∣−∣Qq(x)∩Qq(y)∣=∣Qq(y)∖Qq(x)∣≤q⋅ED(y,x).|Q_q(y)| - |Q_q(x) \cap Q_q(y)| = |Q_q(y) \setminus Q_q(x)| \le q \cdot \mathrm{ED}(y, x).

Both inequalities together give the corollary.

ProofProof sketch

PED(x,y)=ED(x,y′)\mathrm{PED}(x, y) = \mathrm{ED}(x, y') for some prefix y′y' of yy. As for Corollary 1:

∣Qq(x)∣−∣Qq(x)∩Qq(y′)∣=∣Qq(x)∖Qq(y′)∣≤q⋅ED(x,y′)=q⋅PED(x,y).|Q_q(x)| - |Q_q(x) \cap Q_q(y')| = |Q_q(x) \setminus Q_q(y')| \le q \cdot \mathrm{ED}(x, y') = q \cdot \mathrm{PED}(x, y).

Since Qq(y)⊇Qq(y′)Q_q(y) \supseteq Q_q(y'), we have ∣Qq(x)∩Qq(y)∣≥∣Qq(x)∩Qq(y′)∣|Q_q(x) \cap Q_q(y)| \ge |Q_q(x) \cap Q_q(y')|, which gives the corollary.

Filter algorithms#

For a fixed q∈Nq \in \mathbb{N} (Slide 21):

ED. Given xx and a threshold δ\delta, find all y∈Dy \in D with ED(x,y)≤δ\mathrm{ED}(x, y) \le \delta. For each y∈Dy \in D:

  1. Is ∣Qq(x)∩Qq(y)∣≥max⁡{∣Qq(x)∣,∣Qq(y)∣}−q⋅δ|Q_q(x) \cap Q_q(y)| \ge \max\{|Q_q(x)|, |Q_q(y)|\} - q \cdot \delta?
  2. No → by Corollary 1, ED(x,y)>δ\mathrm{ED}(x, y) > \delta, so yy is not a match.
  3. Yes → compute ED(x,y)\mathrm{ED}(x, y) to decide whether yy is a match.

PED. Given xx and δ\delta, find all y∈Dy \in D with PED(x,y)≤δ\mathrm{PED}(x, y) \le \delta: the same, with the test ∣Qq(x)∩Qq(y)∣≥∣Qq(x)∣−q⋅δ|Q_q(x) \cap Q_q(y)| \ge |Q_q(x)| - q \cdot \delta (Corollary 2).

The filter has no false negatives; it only saves the (P)ED computations for the words it rules out.

Padding#

(Slide 22)

PaddingExample (q=3q = 3)Why it is correct
EDq−1q-1 times $ on both sidesfreiburg → $$freiburg$$for padded x′,y′x', y': ED(x,y)=ED(x′,y′)\mathrm{ED}(x, y) = \mathrm{ED}(x', y')
PEDq−1q-1 times $ on the left side onlyfreiburg → $$freiburgfor padded x′′,y′′x'', y'': PED(x′′,y′′)=PED(x,y)\mathrm{PED}(x'', y'') = \mathrm{PED}(x, y)

The algorithm gives the same result with padding, and padding is an improvement: it yields more q-grams (including distinctive ones for the word boundaries), so step 2 of the filter applies more often.

Q-Gram Index#

Motivation#

The filter algorithm needs ∣Qq(x)∩Qq(y)∣|Q_q(x) \cap Q_q(y)| for a given xx and all y∈Dy \in D; for ES8, DD contains 1.7 million strings. This is done efficiently with a q-gram index (Slide 23).

Definition#

A q-gram index is an Inverted Index with different objects than a document index:

  • document index: for each word, a list of document ids,
  • q-gram index: for each q-gram, a list of word ids.

Accordingly, the code from Lecture 1 carries over almost unchanged (lecture code public/code/lecture-08/qgram_index.py):

def q_grams(self, x: str) -> list[str]:
    x = self.padding + x + self.padding        # padding = "$" * (q - 1)
    return [x[i : i + self.q] for i in range(len(x) - self.q + 1)]

def build_from_file(self, file_name: str):
    with open(file_name, "r") as file:
        word_id = 0
        for line in file:
            word_id += 1
            word = line.split("\t", 2)[0].lower()
            for q_gram in self.q_grams(word):
                if q_gram not in self.inverted_lists:
                    self.inverted_lists[q_gram] = []
                self.inverted_lists[q_gram].append(word_id)

Using the index#

For the (appropriately padded) input xx (Slide 25):

  1. Compute Qq(x)Q_q(x).
  2. For each q-gram of Qq(x)Q_q(x), fetch its inverted list.
  3. Compute the union of these ∣Qq(x)∣|Q_q(x)| inverted lists and count how often each word occurs.

If a word yy occurs kk times in the union, it contains exactly kk q-grams of Qq(x)Q_q(x), hence ∣Qq(x)∩Qq(y)∣=k|Q_q(x) \cap Q_q(y)| = k. Words that do not occur in the union at all have ∣Qq(x)∩Qq(y)∣=0|Q_q(x) \cap Q_q(y)| = 0; they are only relevant if the right-hand side of the filter test (step 1 on Slide 21) is ≤0\le 0.

The complete fuzzy prefix search (Fuzzy Search): left-pad xx, fetch and merge the inverted lists of its q-grams, keep the words with at least ∣Qq(x)∣−q⋅δ|Q_q(x)| - q \cdot \delta occurrences, and compute PED only for those. The filter phase takes time linear in the total length of the fetched inverted lists, which is typically much smaller than computing PED for every word.

References#

Course textbook, Section 3 “Tolerant Retrieval”, in particular 3.2 (wildcard queries) and 3.3 (spelling correction); Wikipedia on N-gram, approximate string matching and Levenshtein distance (Slide 27).

Concepts#

  • Fuzzy Search — problem setting (fuzzy and fuzzy prefix search over a dictionary) and the overall filter-then-verify algorithm
  • Edit Distance — relevance model for fuzzy search; definition, properties, recurrence, Θ(∣x∣⋅∣y∣)\Theta(|x| \cdot |y|) DP
  • Prefix Edit Distance — relevance model for search-as-you-type; min of the last DP row, ∣x∣+δ+1|x| + \delta + 1 column cut-off
  • Q-Gram — multiset Qq(x)Q_q(x) of length-qq substrings, ∣x∣−q+1|x| - q + 1 elements, padding
  • Q-Gram Lemma — ∣Qq(x)∖Qq(y)∣≤q⋅ED(x,y)|Q_q(x) \setminus Q_q(y)| \le q \cdot \mathrm{ED}(x, y) and Corollaries 1 (ED) and 2 (PED) as filter bounds
  • Q-Gram Index — inverted lists from q-grams to word ids; merging them yields all common-q-gram counts
  • Inverted Index — the q-gram index is an inverted index over q-grams instead of words
  • Dynamic programming — technique behind the ED/PED table
  • Q-gram padding — $-padding (both sides for ED, left only for PED) that sharpens the filter

Search

Type In
↑ ↓ move ↵ open ⇥ next section Ctrl↵ results page Prefix # to search text only