Building a full-text search engine in TypeScript
Michele Riva
Michele Riva
Senior Software Architect @NearForm
Google Developer Expert
Microsoft MVP
MicheleRivaCode
MicheleRivaCode
Why?
MicheleRivaCode
MicheleRivaCode
What I cannot create, I do not understand
Richard Feynman
MicheleRivaCode
A journey through algorithms and data structures
MicheleRivaCode
MicheleRivaCode
MicheleRivaCode
There's no slow programming language, just bad DSA design
MicheleRivaCode
What is "full-text" search?
sybase.com
Full-text search is a more advanced way to search a database.
Full-text search quickly finds all instances of a term (word) in a table without having to scan rows and without having to know which column a term is stored in.
Full-text search works by using text indexes.
A text index stores positional information for all terms found in the columns you create the text index on.
MicheleRivaCode
What is "full-text" search?
sybase.com
Full-text search is a more advanced way to search a database.
Full-text search quickly finds all instances of a term (word) in a table without having to scan rows and without having to know which column a term is stored in.
Full-text search works by using text indexes.
A text index stores positional information for all terms found in the columns you create the text index on.
MicheleRivaCode
What is "full-text" search?
sybase.com
Full-text search is a more advanced way to search a database.
Full-text search quickly finds all instances of a term (word) in a table without having to scan rows and without having to know which column a term is stored in.
Full-text search works by using text indexes.
A text index stores positional information for all terms found in the columns you create the text index on.
MicheleRivaCode
What is "full-text" search?
sybase.com
Full-text search is a more advanced way to search a database.
Full-text search quickly finds all instances of a term (word) in a table without having to scan rows and without having to know which column a term is stored in.
Full-text search works by using text indexes.
A text index stores positional information for all terms found in the columns you create the text index on.
Popular full-text search engines
MicheleRivaCode
"New generation" full-text search engines
MicheleRivaCode
Sonic
Meilisearch
JavaScript-based full-text search engines
MicheleRivaCode
Lunr.js
MiniSearch
Fuse.js
MicheleRivaCode
Where to start?
MicheleRivaCode
Understand what kind of data we want to store and retrieve
MicheleRivaCode
[
{
"id": 1,
"quote": "It's alive! It's alive!",
"movie": "Frankenstein",
"year": 1931
},
{
"id": 2,
"quote": "You've got to ask yourself one question: 'Do I feel lucky?' Well, do ya, punk?",
"movie": "Dirty Harry",
"year": 1971
},
{
"id": 3,
"quote": "Mama always said life was like a box of chocolates. You never know what you're gonna get.",
"movie": "Forrest Gump",
"year": 1994
}
]
Example documents
MicheleRivaCode
// "It's alive! It's alive!"
["Its", "alive", "Its", "alive"]
// "You've got to ask yourself one question: 'Do I feel lucky?' Well, do ya, punk?"
[
"Youve", "got", "to", "ask", "yourself", "one", "question",
"Do", "I", "feel", "lucky", "Well", "do", "ya", "punk"
]
// "Mama always said life was like a box of chocolates. You never know what you're gonna get."
[
"Mama", "always", "said", "life", "was", "like", "a", "box", "of",
"chocolates", "You", "never", "know", "what", "youre", "gonna", "get"
]
Tokenizer
Break the sentences into individual tokens
MicheleRivaCode
// "It's alive! It's alive!"
["its", "alive", "its", "alive"]
// "You've got to ask yourself one question: 'Do I feel lucky?' Well, do ya, punk?"
[
"youve", "got", "to", "ask", "yourself", "one", "question",
"do", "i", "feel", "lucky", "well", "do", "ya", "punk"
]
// "Mama always said life was like a box of chocolates. You never know what you're gonna get."
[
"mama", "always", "said", "life", "was", "like", "a", "box", "of",
"chocolates", "you", "never", "know", "what", "youre", "gonna", "get"
]
Tokenizer
Lowercase all tokens
MicheleRivaCode
// "It's alive! It's alive!"
["its", "alive"]
// "You've got to ask yourself one question: 'Do I feel lucky?' Well, do ya, punk?"
[
"youve", "got", "to", "ask", "yourself", "one", "question",
"do", "i", "feel", "lucky", "well", "ya", "punk"
]
// "Mama always said life was like a box of chocolates. You never know what you're gonna get."
[
"mama", "always", "said", "life", "was", "like", "a", "box", "of",
"chocolates", "you", "never", "know", "what", "youre", "gonna", "get"
]
Tokenizer
Remove duplicates
MicheleRivaCode
// "It's alive! It's alive!"
["alive"]
// "You've got to ask yourself one question: 'Do I feel lucky?' Well, do ya, punk?"
[
"youve", /* "got", */ /* "to", */ "ask", "yourself", "one", "question",
/* "do", */ /* "i", */ "feel", "lucky", "well", "ya", "punk"
]
// "Mama always said life was like a box of chocolates. You never know what you're gonna get."
[
"mama", "always", "said", "life", /* "was", */, "like", /* "a", */ "box", /* "of", */
"chocolates", "you", "never", "know", /* "what", */ "youre", /* "gonna", */ "get"
]
Tokenizer
Remove stop-words*
MicheleRivaCode
What is a stop word?
Stop words are a set of commonly used words in a language. Examples of stop words in English are “a”, “the”, “is”, “are” and etc. Stop words are commonly used in Text Mining and Natural Language Processing (NLP) to eliminate words that are so commonly used that they carry very little useful information.
https://www.opinosis-analytics.com/knowledge-base/stop-words-explained/
MicheleRivaCode
// "It's alive! It's alive!"
["alive"]
// "You've got to ask yourself one question: 'Do I feel lucky?' Well, do ya, punk?"
[
"youve", /* "got", */ /* "to", */ "ask", "yourself", "one", "question",
/* "do", */ /* "i", */ "feel", "lucky", "well", "ya", "punk"
]
// "Mama always said life was like a box of chocolates. You never know what you're gonna get."
[
"mama", "always", "said", "life", /* "was", */, "like", /* "a", */ "box", /* "of", */
"chocolates", "you", "never", "know", /* "what", */ "youre", /* "gonna", */ "get"
]
Tokenizer
Remove stop-words*
MicheleRivaCode
// "It's alive! It's alive!"
["alive"]
// "You've got to ask yourself one question: 'Do I feel lucky?' Well, do ya, punk?"
[
"you" /* was "youve" */, "ask", "yourself", "one", "question",
"feel", "luck" /* was "lucky" */, "well", /* "ya" becomes "you", duplicate */ "punk"
]
// "Mama always said life was like a box of chocolates. You never know what you're gonna get."
[
"mom" /* was "mama" */, "always", "say" /* was "said" */, "life", "like", "box",
"chocolate" /* was "chocolates" */, "you", "never", "know", /*"you", was "youre", duplicate */, "get"
]
Tokenizer
Stemming*
MicheleRivaCode
Snowball
https://snowballstem.org
MicheleRivaCode
English 🇺🇸🇬🇧🇦🇺
http://snowball.tartarus.org/algorithms/english/stemmer.html
MicheleRivaCode
German 🇩🇪
http://snowball.tartarus.org/algorithms/german/stemmer.html
MicheleRivaCode
Italian 🇮🇹
http://snowball.tartarus.org/algorithms/italian/stemmer.html
MicheleRivaCode
Finnish 🇫🇮
http://snowball.tartarus.org/algorithms/finnish/stemmer.html
MicheleRivaCode
MicheleRivaCode
[
{
"id": 1,
"quote": ["alive"],
...
},
{
"id": 2,
"quote": ["you", "ask", "yourself", "one", "question", "feel", "luck", "well", "punk"],
...
},
{
"id": 3,
"quote": ["mom", "always", "say", "life", "like", "box", "chocolate", "you", "never", "know", "get"],
...
}
]
Final Result
Remaining tokens
MicheleRivaCode
How do we want to store this data?
MicheleRivaCode
MicheleRivaCode
MicheleRivaCode
Find document containing the word "chocolate" in linear time
MicheleRivaCode
Find document containing the word "chocolate" in linear time
MicheleRivaCode
Find document containing the word "chocolate" in linear time
MicheleRivaCode
Find document containing the word "chocolate" in linear time
MicheleRivaCode
Find document containing the word "chocolate" in linear time
MicheleRivaCode
Find document containing the word "chocolate" in linear time
MicheleRivaCode
Find document containing the word "chocolate" in linear time
MicheleRivaCode
Time complexity is O(n)
MicheleRivaCode
MicheleRivaCode
animal = dog
book = algorithms to live by
color = green
language = javascript
city = florence
food = chocolate
HashMaps are used to store data in key-value pairs
MicheleRivaCode
MicheleRivaCode
MicheleRivaCode
MicheleRivaCode
MicheleRivaCode
MicheleRivaCode
MicheleRivaCode
function hash(key: string, size: number): number {
let hash = 0;
for (let i = 0; i < key.length; i++) {
let char = key[i];
hash = (hash << 5) + char.charCodeAt(0);
hash = (hash & hash) % size;
}
return hash;
}
Example of an hashing algorithm
MicheleRivaCode
function hash(key: string, size: number): number {
let hash = 0;
for (let i = 0; i < key.length; i++) {
let char = key[i];
hash = (hash << 5) + char.charCodeAt(0);
hash = (hash & hash) % size;
}
return hash;
}
const size = 10;
hash("food", size); // => 2
hash("book", size); // => 7
hash("hello, Berlin!", size); // => 9
Example of an hashing algorithm
MicheleRivaCode
When asking for a key, we know the exact position of its value inside of the array.
Hence, time complexity is O(1)
MicheleRivaCode
MicheleRivaCode
MicheleRivaCode
But that's not enough to find "chocolate" inside of our array of documents in O(1)
MicheleRivaCode
We need an inverted index
MicheleRivaCode
{
1 => ["alive"],
2 => ["you", "ask", "yourself", "one", "question", "feel", "luck", "well", "punk"],
3 => ["mom", "always", "say", "life", "like", "box", "chocolate", "you", "never", "know", "get"],
}
Regular HashMap
MicheleRivaCode
{
"alive" => [1],
"you" => [2, 3],
"ask" => [1],
"yourself" => [2],
"chocolate" => [3],
"punk" => [2],
"one" => [2],
"question" => [2],
"feel" => [2],
"mom" => [3],
"always" => [3],
"say" => [3],
"know" => [3],
"luck" => [2],
"life" => [3],
"like" => [3],
"well" => [2],
"box" => [3],
"never" => [3],
"get" => [3]
}
Inverted Index
MicheleRivaCode
Optimizing space
MicheleRivaCode
{
"intersect" => [10,32,12,2,3],
"interstellar" => [2,6,20,23,42],
"intergalactic" => [12,3,54,29,32],
"international" => [32,12,34,64,2],
"intervene" => [92,12,42,54,6],
"internal" => [102,32,543,6,1],
"telecommunication" => [91,2,4,23],
"television" => [10,8,6,15,3,2],
"telephone" => [1,85,14,54,76]
}
Many tokens are sharing a common prefix
MicheleRivaCode
Trees to the rescue!
MicheleRivaCode
MicheleRivaCode
Prefix tree
MicheleRivaCode
Private
Primark
Prime
Primate
MicheleRivaCode
MicheleRivaCode
MicheleRivaCode
MicheleRivaCode
MicheleRivaCode
MicheleRivaCode
MicheleRivaCode
We can use a prefix tree as an "inverted index" to store the reference of a token with the document
MicheleRivaCode
"primark" => [1, 3] "primate" => [2, 4] "prime" => [1, 5] "private" => [2, 6] "art" => [4, 5] "artist" => [4, 7]
MicheleRivaCode
"primark" => [1, 3] "primate" => [2, 4] "prime" => [1, 5] "private" => [2, 6] "art" => [4, 5] "artist" => [4, 7]
MicheleRivaCode
"Talk is cheap! Show me the code!"
MicheleRivaCode
type Nullable<T> = T | null;
type Children = Map<string, TrieNode>;
type Docs = Set<string>;
type NodeContent = [string, Docs];
interface ITrieNode {
key: string;
parent: Nullable<TrieNode>;
children: Nullable<Children>;
docs: Docs;
end: boolean;
getWord: () => NodeContent;
removeDoc: (id: string) => boolean;
}
trieNode.ts
MicheleRivaCode
type FindResult = {
[key: string]: Set<string>;
}
interface ITrie {
root: TrieNode;
insert: (word: string, docId: string) => void;
contains: (word: string) => boolean;
find: (prefix: string) => FindResult;
removeDocByWord: (word: string, docId: string) => boolean;
remove: (word: string) => boolean;
}
trie.ts
MicheleRivaCode
class TrieNode implements ITrieNode {
public key;
public parent = null;
public children = new Map();
public docs = new Set();
public end = false;
}
trieNode.ts
MicheleRivaCode
class TrieNode implements ITrieNode {
public key;
public parent = null;
public children = {};
public docs = new Set();
public end = false;
constructor(key: string) {
this.key = key;
}
}
trieNode.ts
MicheleRivaCode
class TrieNode implements ITrieNode {
public key;
public parent = null;
public children = {};
public docs = new Set();
public end = false;
constructor(key: string) {
this.key = key;
}
getWord(): NodeContent {
let node: TrieNode = this;
let output = "";
while (node !== null) {
output = node.key + output;
node = node.parent!;
}
return [output, this.docs];
}
}
trieNode.ts
MicheleRivaCode
MicheleRivaCode
class TrieNode implements ITrieNode {
public key;
public parent = null;
public children = {};
public docs = new Set();
public end = false;
constructor(key: string) {
this.key = key;
}
getWord() {
let output = "";
let node = this;
while (node !== null) {
output = node.key + output;
node = node.parent!;
}
return [output, this.docs];
}
removeDoc(docID: string): boolean {
return this.docs.delete(docID);
}
}
trieNode.ts
MicheleRivaCode
MicheleRivaCode
TC39 has standardized TCE
(tail-call elimination) with ES6
MicheleRivaCode
MicheleRivaCode
class Trie implements ITrie {
private root = new TrieNode("");
}
trie.ts
MicheleRivaCode
insert(word: string, docId: string): void {
const wordLength = word.length;
let node = this.root;
for (let i = 0; i < wordLength; i++) {
const char = word[i];
if (!node.children?.has(char)) {
const newTrieNode = new TrieNode(char);
newTrieNode.setParent(node);
node.children!.set(char, newTrieNode);
}
node = node.children!.get(char)!;
if (i === wordLength - 1) {
node.setEnd(true);
node.docs.add(docId);
}
}
}
trie.ts
find(prefix: string): FindResult {
let node = this.root;
const output: FindResult = {};
for (const char of prefix) {
if (node?.children?.has(char)) {
node = node.children.get(char)!;
} else {
return output;
}
}
findAllWords(node, output);
function findAllWords(_node: TrieNode, _output: FindResult) {
if (_node.end) {
const [word, docIDs] = _node.getWord();
if (!(word in _output)) {
_output[word] = new Set();
}
if (docIDs?.size) {
for (const doc of docIDs) {
_output[word].add(doc);
}
}
}
for (const childNode of _node.children?.values() ?? []) {
findAllWords(childNode, _output);
}
}
return output;
}
MicheleRivaCode
✅ Tokenizer
✅ Prefix-tree
❌ Typo-tolerance
MicheleRivaCode
trie.find("wrld");
// Resuls:
[
{
id: 1,
quote: "Hello, World!"
},
{
id: 2,
quote: "What a wonderful world"
}
]
Dynamic programming
MicheleRivaCode
Dynamic Programming
An algorithmic technique for solving an optimization problem by breaking it down into simpler subproblems and utilizing the fact that the optimal solution to the overall problem depends upon the optimal solution to its subproblems.
https://educative.io
MicheleRivaCode
Levenshtein distance
MicheleRivaCode
Levenshtein distance
MicheleRivaCode
The Levenshtein algorithm calculates the least number of edit operations that are necessary to modify one string to obtain another string.
MicheleRivaCode
const word1 = "moon";
const word2 = "lions";
levenshtein(word1, word2); // => 3
MicheleRivaCode
Allowed operations
Insert
Delete
Replace
MicheleRivaCode
Edit distance of "Moon" and "Lions"
1)
MOON
LIONS
REPLACE
MicheleRivaCode
Edit distance of "Moon" and "Lions"
1)
MOON
LIONS
REPLACE
2)
LOON
LIONS
REPLACE
MicheleRivaCode
Edit distance of "Moon" and "Lions"
1)
MOON
LIONS
REPLACE
2)
LOON
LIONS
REPLACE
3)
LION
LIONS
INSERT
MicheleRivaCode
Λ | L | I | O | N | S | |
---|---|---|---|---|---|---|
Λ | ||||||
M | ||||||
O | ||||||
O | ||||||
N |
Insert
Delete
Replace
MicheleRivaCode
Λ | L | I | O | N | S | |
---|---|---|---|---|---|---|
Λ | ||||||
M | ||||||
O | ||||||
O | ||||||
N |
Insert
Delete
Replace
MO -> L
1
2
3
4
5
6
2
3
4
1
D(2,2)
MicheleRivaCode
Λ | L | I | O | N | S | |
---|---|---|---|---|---|---|
Λ | ||||||
M | ||||||
O | ||||||
O | ||||||
N |
MOO -> O
Insert
Delete
Replace
1
2
3
4
5
6
2
3
4
1
D(4,3)
MicheleRivaCode
Λ | L | I | O | N | S | |
---|---|---|---|---|---|---|
Λ | 0 | |||||
M | ||||||
O | ||||||
O | ||||||
N |
"" -> ""
Insert
Delete
Replace
MicheleRivaCode
Λ | L | I | O | N | S | |
---|---|---|---|---|---|---|
Λ | 0 | 1 | ||||
M | ||||||
O | ||||||
O | ||||||
N |
"" -> "L"
Insert
Delete
Replace
MicheleRivaCode
Λ | L | I | O | N | S | |
---|---|---|---|---|---|---|
Λ | 0 | 1 | 2 | |||
M | ||||||
O | ||||||
O | ||||||
N |
"" -> "LI"
Insert
Delete
Replace
MicheleRivaCode
Λ | L | I | O | N | S | |
---|---|---|---|---|---|---|
Λ | 0 | 1 | 2 | 3 | ||
M | ||||||
O | ||||||
O | ||||||
N |
"" -> "LIO"
Insert
Delete
Replace
MicheleRivaCode
Λ | L | I | O | N | S | |
---|---|---|---|---|---|---|
Λ | 0 | 1 | 2 | 3 | 4 | |
M | ||||||
O | ||||||
O | ||||||
N |
"" -> "LION"
Insert
Delete
Replace
MicheleRivaCode
Λ | L | I | O | N | S | |
---|---|---|---|---|---|---|
Λ | 0 | 1 | 2 | 3 | 4 | 5 |
M | ||||||
O | ||||||
O | ||||||
N |
"" -> "LIONS"
Insert
Delete
Replace
MicheleRivaCode
Λ | L | I | O | N | S | |
---|---|---|---|---|---|---|
Λ | 0 | 1 | 2 | 3 | 4 | 5 |
M | 1 | |||||
O | ||||||
O | ||||||
N |
"M" -> ""
Insert
Delete
Replace
MicheleRivaCode
Λ | L | I | O | N | S | |
---|---|---|---|---|---|---|
Λ | 0 | 1 | 2 | 3 | 4 | 5 |
M | 1 | |||||
O | 2 | |||||
O | ||||||
N |
"MO" -> ""
Insert
Delete
Replace
MicheleRivaCode
Λ | L | I | O | N | S | |
---|---|---|---|---|---|---|
Λ | 0 | 1 | 2 | 3 | 4 | 5 |
M | 1 | |||||
O | 2 | |||||
O | 3 | |||||
N |
"MOO" -> ""
Insert
Delete
Replace
MicheleRivaCode
Λ | L | I | O | N | S | |
---|---|---|---|---|---|---|
Λ | 0 | 1 | 2 | 3 | 4 | 5 |
M | 1 | |||||
O | 2 | |||||
O | 3 | |||||
N | 4 |
"MOON" -> ""
Insert
Delete
Replace
MicheleRivaCode
Λ | L | I | O | N | S | |
---|---|---|---|---|---|---|
Λ | 0 | 1 | 2 | 3 | 4 | 5 |
M | 1 | |||||
O | 2 | |||||
O | 3 | |||||
N | 4 |
Insert
Delete
Replace
1
2
3
4
5
6
2
3
4
1
D(2,1)
MicheleRivaCode
MicheleRivaCode
Λ | L | I | O | N | S | |
---|---|---|---|---|---|---|
Λ | 0 | 1 | 2 | 3 | 4 | 5 |
M | 1 | |||||
O | 2 | |||||
O | 3 | |||||
N | 4 |
+1
2
Insert
Delete
Replace
1
2
3
4
5
6
2
3
4
1
D(2,1)
MicheleRivaCode
Λ | L | I | O | N | S | |
---|---|---|---|---|---|---|
Λ | 0 | 1 | 2 | 3 | 4 | 5 |
M | 1 | |||||
O | 2 | |||||
O | 3 | |||||
N | 4 |
+1
2
+1
2
Insert
Delete
Replace
1
2
3
4
5
6
2
3
4
1
D(2,1)
MicheleRivaCode
Λ | L | I | O | N | S | |
---|---|---|---|---|---|---|
Λ | 0 | 1 | 2 | 3 | 4 | 5 |
M | 1 | |||||
O | 2 | |||||
O | 3 | |||||
N | 4 |
+1
2
+1
2
1
Insert
Delete
Replace
1
2
3
4
5
6
2
3
4
1
D(2,1)
MicheleRivaCode
Λ | L | I | O | N | S | |
---|---|---|---|---|---|---|
Λ | 0 | 1 | 2 | 3 | 4 | 5 |
M | 1 | 1 | ||||
O | 2 | |||||
O | 3 | |||||
N | 4 |
Insert
Delete
Replace
1
2
3
4
5
6
2
3
4
1
MicheleRivaCode
Λ | L | I | O | N | S | |
---|---|---|---|---|---|---|
Λ | 0 | 1 | 2 | 3 | 4 | 5 |
M | 1 | 1 | ||||
O | 2 | |||||
O | 3 | |||||
N | 4 |
+1
2
+1
2
2
Insert
Delete
Replace
1
2
3
4
5
6
2
3
4
1
D(3,1)
MicheleRivaCode
Λ | L | I | O | N | S | |
---|---|---|---|---|---|---|
Λ | 0 | 1 | 2 | 3 | 4 | 5 |
M | 1 | 1 | 2 | 3 | 4 | 5 |
O | 2 | |||||
O | 3 | |||||
N | 4 |
Insert
Delete
Replace
1
2
3
4
5
6
2
3
4
1
MicheleRivaCode
Λ | L | I | O | N | S | |
---|---|---|---|---|---|---|
Λ | 0 | 1 | 2 | 3 | 4 | 5 |
M | 1 | 1 | 2 | 3 | 4 | 5 |
O | 2 | 2 | 2 | |||
O | 3 | |||||
N | 4 |
Insert
Delete
Replace
1
2
3
4
5
6
2
3
4
1
D(4,2)
MicheleRivaCode
MicheleRivaCode
Λ | L | I | O | N | S | |
---|---|---|---|---|---|---|
Λ | 0 | 1 | 2 | 3 | 4 | 5 |
M | 1 | 1 | 2 | 3 | 4 | 5 |
O | 2 | 2 | 2 | |||
O | 3 | |||||
N | 4 |
2
2
Insert
Delete
Replace
1
2
3
4
5
6
2
3
4
1
D(4,2) = D(3,1)
MicheleRivaCode
Λ | L | I | O | N | S | |
---|---|---|---|---|---|---|
Λ | 0 | 1 | 2 | 3 | 4 | 5 |
M | 1 | 1 | 2 | 3 | 4 | 5 |
O | 2 | 2 | 2 | 2 | 3 | 4 |
O | 3 | 3 | 3 | 2 | 3 | 4 |
N | 4 | 4 | 4 | 3 | 2 | 3 |
Insert
Delete
Replace
1
2
3
4
5
6
2
3
4
1
MicheleRivaCode
Λ | L | I | O | N | S | |
---|---|---|---|---|---|---|
Λ | 0 | 1 | 2 | 3 | 4 | 5 |
M | 1 | 1 | 2 | 3 | 4 | 5 |
O | 2 | 2 | 2 | 2 | 3 | 4 |
O | 3 | 3 | 3 | 2 | 3 | 4 |
N | 4 | 4 | 4 | 3 | 2 |
3
Insert
Delete
Replace
1
2
3
4
5
6
2
3
4
1
MicheleRivaCode
Λ | L | I | O | N | S | |
---|---|---|---|---|---|---|
Λ | 0 | 1 | 2 | 3 | 4 | 5 |
M | 1 | 1 | 2 | 3 | 4 | 5 |
O | 2 | 2 | 2 | 2 | 3 | 4 |
O | 3 | 3 | 3 | 2 | 3 | 4 |
N | 4 | 4 | 4 | 3 | 2 |
0
Insert
Delete
Replace
1
2
2
2
3
1
2
3
4
5
6
2
3
4
1
MicheleRivaCode
Edit distance of "Moon" and "Lions"
1)
MOON
LIONS
REPLACE
2)
LOON
LIONS
REPLACE
3)
LION
LIONS
INSERT
MicheleRivaCode
Λ | P | O | S | E | R | |
---|---|---|---|---|---|---|
Λ | 0 | 1 | 2 | 3 | 4 | 5 |
H | 1 | 1 | 2 | 3 | 4 | 5 |
O | 2 | 2 | 1 | 2 | 3 | 4 |
R | 3 | 3 | 2 | 2 | 3 | 3 |
S | 4 | 4 | 3 | 2 | 3 | 4 |
E | 5 | 5 | 4 | 3 | 2 |
Levenshtein distance of Horse - Poser
3
MicheleRivaCode
Levenshtein distance of Race - Raise
Λ | R | A | I | S | E | |
---|---|---|---|---|---|---|
Λ | 0 | 1 | 2 | 3 | 4 | 5 |
R | 1 | 0 | 1 | 2 | 3 | 4 |
I | 2 | 1 | 0 | 1 | 2 | 3 |
C | 3 | 2 | 1 | 1 | 2 | 3 |
E | 4 | 3 | 2 | 2 | 2 |
2
MicheleRivaCode
export function levenshtein(a: string, b: string): number {
if (!a.length) return b.length;
if (!b.length) return a.length;
let tmp;
if (a.length > b.length) {
tmp = a;
a = b;
b = tmp;
}
const row = Array.from({ length: a.length + 1 }, (_, i) => i);
let val = 0;
for (let i = 1; i <= b.length; i++) {
let prev = i;
for (let j = 1; j <= a.length; j++) {
if (b[i - 1] === a[j - 1]) {
val = row[j - 1];
} else {
val = Math.min(row[j - 1] + 1, Math.min(prev + 1, row[j] + 1));
}
row[j - 1] = prev;
prev = val;
}
row[a.length] = prev;
}
return row[a.length];
}
We can perform these operations on both strings and trees
MicheleRivaCode
Tree Edit Distance (and Levenshtein Distance)
Simple fast algorithms for the editing distance between trees and related problems
Kaizhong Zhang and Dennis Shasha
https://shorturl.at/otBMY
MicheleRivaCode
MicheleRivaCode
import { Lyra } from '@nearform/lyra';
const db = new Lyra({
schema: {
author: 'string',
quote: 'string'
}
});
MicheleRivaCode
await db.insert({
quote: 'It is during our darkest moments that we must focus to see the light.',
author: 'Aristotle'
});
await db.insert({
quote: 'If you really look closely, most overnight successes took a long time.',
author: 'Steve Jobs'
});
await db.insert({
quote: 'If you are not willing to risk the usual, you will have to settle for the ordinary.',
author: 'Jim Rohn'
});
await db.insert({
quote: 'You miss 100% of the shots you don\'t take',
author: 'Wayne Gretzky - Michael Scott'
});
MicheleRivaCode
const searchResult = await db.search({
term: 'if',
properties: ['quote']
});
// Result
{
elapsed: '99μs',
hits: [
{
id: 'ckAOPGTA5qLXx0MgNr1Zy',
quote: 'If you really look closely, most overnight successes took a long time.',
author: 'Steve Jobs'
},
{
id: 'fyl-_1veP78IO-wszP86Z',
quote: 'If you are not willing to risk the usual, you will have to settle for the ordinary.',
author: 'Jim Rohn'
}
],
count: 2
}
MicheleRivaCode
const searchResult = await db.search({
term: 'Michael',
properties: '*'
});
// Result
{
elapsed: '111μs',
hits: [
{
id: 'L1tpqQxc0c2djrSN2a6TJ',
quote: "You miss 100% of the shots you don't take",
author: 'Wayne Gretzky - Michael Scott'
}
],
count: 1
}
MicheleRivaCode
MicheleRivaCode
npm i @nearform/lyra
MicheleRivaCode
MicheleRivaCode
Real-World Next.js
Build scalable, high performances and modern web applications using Next.js, the React framework for production
MicheleRivaCode
MicheleRivaCode
@MicheleRiva
@MicheleRivaCode
/in/MicheleRiva95
www.micheleriva.dev
Building a full-text search engine in TypeScript
By Michele Riva
Building a full-text search engine in TypeScript
- 546