1<!DOCTYPE html><html lang="en"><head><meta charSet="utf-8"/><meta http-equiv="x-ua-compatible" content="ie=edge"/><meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no"/><style data-href="/styles.c46fedd1aa2353b79597.css" data-identity="gatsby-global-css">@font-face{font-display:swap;font-family:Open Sans;font-style:normal;font-weight:400;src:url(/static/open-sans-cyrillic-ext-400-normal-fd5b41f2c6471bf872fdc7c05405526e.woff2) format("woff2"),url(/static/open-sans-all-400-normal-3ad2fdabf53442ab007908cdd4f3ef99.woff) format("woff");unicode-range:U+0460-052f,U+1c80-1c88,U+20b4,U+2de0-2dff,U+a640-a69f,U+fe2e-fe2f}@font-face{font-display:swap;font-family:Open Sans;font-style:normal;font-weight:400;src:url(/static/open-sans-cyrillic-400-normal-915eeba2b3a6fb0238947a507de022b4.woff2) format("woff2"),url(/static/open-sans-all-400-normal-3ad2fdabf53442ab007908cdd4f3ef99.woff) format("woff");unicode-range:U+0400-045f,U+0490-0491,U+04b0-04b1,U+2116}@font-face{font-display:swap;font-family:Open Sans;font-style:normal;font-weight:400;src:url(data:font/woff2;base64,d09GMgABAAAAAA5gABEAAAAAGsQAAA4BAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAGjIbgS4cPAZgP1NUQVReADwIgXwJnxQKgiyCKAsKAAE2AiQDEAQgBYR4ByAMhR8bjhkjEcLGAYBIuyfAXyaYOnQ0p/OiLa60Kt02vPLK7ZEcTnP/0T1vFAybvkD0KyMkmfWf5089932ErQdnYFb9SbWy9k9KCiUpPFD+iLDbpSzWPJAwlID89rlfvuQIFWFmVx+Q/DORBwStcLUpBDG30GYK/ESkcQuNJNbphBDJZVammaRSpM7OcBjUOGP33kphuUkOm04HUA4Auavw+Dm+TzX+fRqOlH9xa8qSfH61hX0+sIVGPXusFwojixCqxQH149RP+mQnF91afCzDA3CxI7SE5D1ZiZ8U/yj6U7697Nt2xxzoCOiaAy1U1rICzKpOVBznkAFdfSSNbcPZwcwgRCHI2pezQWBwRGIrY44xELi4GKhoneGcKBbxj9GiNeIJgcGRiTD0LbSmw2C29k+DwdK/jxWG9N72TBjAA4CiQAMgsMrthmNG2h6HO9YCQnQfRDSR+CCCGgQJehBaw5WDadeJl4XtEOHsVns2ek7ct6xZv7NFEEvgQ6UOscGiV403bRs3iZvETTpNrpG8P89PookQAKEZfw7AbDhj8VKJTUbciq/PHLJjQuhgraha99Xj/q0E1P/ZGIAWKD4WKmwHPGSpzeRr6w59lqZg+f3sFiIg/1rsBNA/8IxTdS8FV9TxJsCoKvrBBevA08S23XsVbGcNwL9oQ1iwBC/m0QhEkqc6UpvTw4GH2x7Wl21CdIdO2I6K+YgPtQDltZm38J7/Ffsf4wp1/Q2AlVab/l683GBeY39mjgUmWGW5k8gw2mxpxpujyFLj0hZDzbbYaO8xTEFCMUyR2eZbbYuG6jATzbJAEVbZqC2WW6sNmflgjQTzZJQXnk9aZIk1NhhirJlpixwDjTDhXH6ZUsONMa383uu9R4EyI01JKAoNjP+0qWZbMQ5zRYIW7kprNp9b4Rs+a+aM6SOGDxs6ZPCggeVlpSXFRYUF+Xm5dps1JzsrMyM9LTXFYu6J3MymVRn8N19/9eV14ezzq2eT8XDwxeeffXrZ73U77Y/ezdZWG2lMoxd8MVtrNpIYjX3cehNYDWNhvw2GDSEMrb4c2byltHaKNQIYeX4wTGWdmDkFJ0oPo+a1Lsd8OSws5bISsHurCUbzDHfpujQDsiegy5xrXuLu/vG2elhm9b4iE2AucRrBB2xfUBEL76ztz7MYNo6xZliznfFr0clGfeIvqlnZK6XUAVmqy5Mynqiec42hVliIv3FdxT7QQHbWuIYef1v+Qh5SEcGAy4GVGuCdFW7xyLJ7KKTJmpyri0MkfiHWzUYkOY916cNoDOnDeGHr8gS1hwn7C6m5C3/uYj3eHSCxF2mcChHVViaj9HIUhXc7VQd67CK/dbh9VY2Cj8PwKJW0aJA2FjUiKnSLxzwJ4W+5lms3i3BUfsYjaJXgliY+Qu+82+na4unK6erpxtzmnIqI5l/my/6FkkRU8mtcbaYQvMSj3TW6f/4xVaPgoJ7C0UM4/Z5Dck1lmXuljb/Fs1ZcflPYXzPZVApCljlPnqbZyCNNDQ/tbpwBAQDc651iFCgm34swtvx7vNIuONfS0wj7DmR5ptjF6pflJI/l2cWltA8Li6BuZ8fVpQ+8kWFHg22Gs/1bg0Kv4ran7WEwAvfateqKPNa8KfdQ1hZKIVWQ+EmQieR7P6L//fQ4OY6olzaQmNk5UnNOXXaSnEQG7HZgxvO/c845vX4v61wSIQzsrbpxAVIhcACbcxVZzrV2r6fNBuURpqbVbFwOY/jCDK5tF3AFSWzR0YUPQhXO57S0Yi3kS+taLSeV5e7yUy57lsydUskMvGMnB5oiDMEgwIpLXxRy32VSWMnePOWxjeFUVqkKiiqnZLby+u5Ws5EGSG6xuELpo5mb9wHVTXJ2LUnpuUQ+NsTfbvM28A3zWtFz36SCYzwifZxPWZRnYTWUHVlNU8f5+DJgbmfuuDHuIygY2CnlB5g24sT7GJLfHjt3pp2v9GA8VUdPUn41jT2FA2fYXi3gpSSRjPcZUApEBzqsi8ZLFUCAGX/YFP2LyZZKOzB9TEZdd2Kvb+3RODJqX4UJuLYahCbV8TIg78h7BBZOq1zogm7CAKOl1wzahOniwlIIMq73cJhHe2DXluxmrDQab51vVe98QjD6gJUFFCWy5OkBjLagQdCEYQ/E2zgxHGYQbzMTI3mkDsRDpPGIylk7mif4k5y3tyYpgd+qkrpAWPQGGjtSKbInM49wyRL4WV15w5oyalN3u6A65AjTC84dWMosI1A6VHY14tDExeX3sWmYODYCc8I8nwBLixE2ayPoyqkBZXcJ5IAzcFlYcGd5KBbX81oIXisN+wi0jjm/TE88V1jgljEuiXSvznFMG7YmizPw4NyRsa5UsxjbpJ5eVZ4cMXKg8tNG4CBzOVEjnXtEwZjR069EagSM9yIEzkuqLg62lfccvHNVsM4YSSn1Vs7UaHrcvnMHUftZlNpeM65Fskm0tt6I62bdhna+9uC2ujjoPXXWN6LuUHgxM/Xcg03YGUwl86VVPBBVdxtVwy2O2rXVmvWyAgSdHQjjRtbbN3jh2UF/hcDqKpS2OWlDHxuQSSCmmwK7kxpwOZB30gb/QpKGefcKkdMjqWVgZkEqZZsbss5hwk+/rKsDCxiqQ5gcAl74VyeuDAPWdlEtuMt+Q1x3qyVETL0jovgDY9abxoWO0F0FMGAUhHb7K5IPQVwxCM6mmfX0EdPuxIq5wXwnbeuDwcGcdu5HijSvvcTlrYiLW7M0ZNzBqk+QYA82Uk2Vf44+GMDcvHVYdfUT25g87boR1kHO+Pbv4Y0tYcFYxUm3UBfKf9cS2hE46INXs2gNRDE2slumnbP/mnZIZxMOyCi79srfNwGe4nUwDFkhfdkPWKvGuIj+XhO7oBYdElIGhX2f7uE6bM9JC2l+6INIA+2UuP4dQMdjjyBesoH5mHIQ5POBR9NsSFxJUG9zhWI3T7qnh5swEfSUOJpYpHYbhgPX+EFwzlNo/PT/unMfDOdE+j6gKDtfh1v7IJk6NvJtwlZ757rj4on9ibwV/gfLBRYCdW6U+8iiGueTx2Zjl3/ierypxGxDovsAtin7FV8OEkiBVy10QWIATCimuzLZ68tfjraQX2dcWM4oPUwOd4hy3tcoWQLDAs09B7zXxStrFh08OnllIkLZDlwXeQJ84NOsNlvGn2CCISdW+ExI5KUWNDbmpH8M5l5ZtrZ4bfV3sh7wn2ExpKMJNDX88rfaH5UarvydhioMoS8t1QGeyuQtJ5Hepm6PavzKQooBEJqF0m57pi8D5VszEuL/QfL0URdrsLbd+Yc/VRqTR3dv+WcRmOYEqWxcaf59Q4CpDLa1/aosyBxrJpjKR9eJcJDcBuYjmmKSZI5f8nCJjajjFr2Fs9CLDl+XKQq0q5POtXjgzatXtaOvX79Ng+480Y15dKtg4K1r+dvfuENDbmtHX71ZTFtuJOmwlinbJ3ZndmP+75/DwL9yq3bgJVPXw/dWmdxXHv+Vtmz137TlJ
1+IrrHo4Ja7pnie3Gu2aGRt68NnuFKfbU1atvjnF7LS72tuOf1bfmZSqd3ZuOPVBlbQWSxwDd1Ro07F/mKVnuXOWzpJradHSz8s416fyp+1dKtYJ8RVqoCGI2gOkAvd/kjLbAACkvBV/Cud21HDkrBUFjRVGySh5+2zjmOTIsLN/0RnG5s199dV1OlRxcRH5gQR4SVXPSpJXM5dmsSv4C0EAOODw6957ezq1/6bWqQEAt84e/gHA3bClnn/m/rsSmavuA0AEAwAQ8LOqkWY5A5o/BgjJe/P8ORwS26B05oL45ZTN9dl5TFIRPQlgcIYXZgPb7zQEXCwcsb404jVn+VpYS3DFWma89v61HFri7FoedYitFVCV6q0VYaCOaxzRnHqcQ4CeJoe1BC3NvwVvY0RVR3slVx/trVxD89kPIkCWbIWsUphZ2BkgwTsPBiR2Rf8QIzvnTAsJehvbKo7VhSypmO37Cn5yc7b0rZXHNNKIh7M7oS283oVZChdyj/Thib7HZFAw56x9JF1/A6pkJjsbL203t51vr3FNOL630ixX+plWzeAJ73RlO4SI4ZBKS/bPt+ERrrd918XFnTe1y6bU8BYGWzdOv/aCPm+z6etrm91K66k1pPN0Ya17IUawSEnW6w9Oqmw/v9lozpCht4rDupijb+sr3pNUg3EOyWUpcyHha/i26LDQVCjntY6pldtaqWVvvWYHbMs5rEstc5ZE9qqQNScyxx5kjC596su0SaYOiZTOjKV7GXNnZUAXRP++dv16falvH0OfViN7t65XM1zPRid7NKXuTUs+9XBdK1x2qUpyZSa59FJSOYXOZc+ZiiETc5eQa2R8nojLBhKbcUwuo1MXlUQZGbuIuJcIj30Ji2Ro6EICl8GBCwr0iUB/CgF+wp9z+OF8wWUnr9TR+9LhZK790RLtDj/aHi5qc7jQavsvStd8rEo+XUme5X+DRrprf9JqNUm8UV9w7E9UtSe0IbB3avPFwEatg2V8TpUPHM2d6sZOtcHmbbU21rcTInNGr3JG/70v1Ue5yj7SVcGXnWUHSW8SJN6kl3nZiS/nP/Cck+J41Ulak0oSTdQTpiVrZVG+IH4QGeSHcht8AO8M0khqEycxk1pmshMrZx8Y5wzO11egnTRxY6IxYqdKib/cqIntupFGbqyfAP2JvnFdNoojN8J0uiZvIhovDxs3DjX8IzZOTEjezAE1/OVNjAXEJW/iufGyvw1GGI1Gmw3Goocx0ZT0cEtKDVAq79GHRgJAJ4TRp+u/6fO1X/ptKgM=) format("woff2"),url(/static/open-sans-all-400-normal-3ad2fdabf53442ab007908cdd4f3ef99.woff) format("woff");unicode-range:U+1f??}@font-face{font-display:swap;font-family:Open Sans;font-style:normal;font-weight:400;src:url(data:font/woff2;base64,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) format("woff2"),url(/static/open-sans-all-400-normal-3ad2fdabf53442ab007908cdd4f3ef99.woff) format("woff");unicode-range:U+0370-03ff}@font-face{font-display:swap;font-family:Open Sans;font-style:normal;font-weight:400;src:url(data:font/woff2;base64,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) format("woff2"),url(/static/open-sans-all-400-normal-3ad2fdabf53442ab007908cdd4f3ef99.woff) format("woff");unicode-range:U+0590-05ff,U+20aa,U+25cc,U+fb1d-fb4f}@font-face{font-display:swap;font-family:Open Sans;font-style:normal;font-weight:400;src:url(data:font/woff2;base64,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) format("woff2"),url(/static/open-sans-all-400-normal-3ad2fdabf53442ab007908cdd4f3ef99.woff) format("woff");unicode-range:U+0102-0103,U+0110-0111,U+0128-0129,U+0168-0169,U+01a0-01a1,U+01af-01b0,U+1ea0-1ef9,U+20ab}@font-face{font-display:swap;font-family:Open Sans;font-style:normal;font-weight:400;src:url(/static/open-sans-latin-ext-400-normal-8e997721e23c4a12c54cd8d6e4ed8a46.woff2) format("woff2"),url(/static/open-sans-all-400-normal-3ad2fdabf53442ab007908cdd4f3ef99.woff) format("woff");unicode-range:U+0100-024f,U+0259,U+1e??,U+2020,U+20a0-20ab,U+20ad-20cf,U+2113,U+2c60-2c7f,U+a720-a7ff}@font-face{font-display:swap;font-family:Open Sans;font-style:normal;font-weight:400;src:url(/static/open-sans-latin-400-normal-36a546a98dbdb166cdce9a4f41078c3b.woff2) format("woff2"),url(/static/open-sans-all-400-normal-3ad2fdabf53442ab007908cdd4f3ef99.woff) format("woff");unicode-range:U+00??,U+0131,U+0152-0153,U+02bb-02bc,U+02c6,U+02da,U+02dc,U+2000-206f,U+2074,U+20ac,U+2122,U+2191,U+2193,U+2212,U+2215,U+feff,U+fffd} 2 3 4/*! normalize.css v8.0.1 | MIT License | github.com/necolas/normalize.css */html{-webkit-text-size-adjust:100%;line-height:1.15}body{margin:0}main{display:block}h1{font-size:2em;margin:.67em 0}hr{box-sizing:content-box;height:0;overflow:visible}pre{font-family:monospace,monospace;font-size:1em}a{background-color:transparent}abbr[title]{border-bottom:none;text-decoration:underline;-webkit-text-decoration:underline dotted;text-decoration:underline dotted}b,strong{font-weight:bolder}code,kbd,samp{font-family:monospace,monospace;font-size:1em}small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline}sub{bottom:-.25em}sup{top:-.5em}img{border-style:none}button,input,optgroup,select,textarea{font-family:inherit;font-size:100%;line-height:1.15;margin:0}button,input{overflow:visible}button,select{text-transform:none}[type=button],[type=reset],[type=submit],button{-webkit-appearance:button}[type=button]::-moz-focus-inner,[type=reset]::-moz-focus-inner,[type=submit]::-moz-focus-inner,button::-moz-focus-inner{border-style:none;padding:0}[type=button]:-moz-focusring,[type=reset]:-moz-focusring,[type=submit]:-moz-focusring,button:-moz-focusring{outline:1px dotted ButtonText}fieldset{padding:.35em .75em .625em}legend{box-sizing:border-box;color:inherit;display:table;max-width:100%;padding:0;white-space:normal}progress{vertical-align:baseline}textarea{overflow:auto}[type=checkbox],[type=radio]{box-sizing:border-box;padding:0}[type=number]::-webkit-inner-spin-button,[type=number]::-webkit-outer-spin-button{height:auto}[type=search]{-webkit-appearance:textfield;outline-offset:-2px}[type=search]::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{-webkit-appearance:button;font:inherit}
4details{display:block}summary{display:list-item}[hidden]{display:none}*{box-sizing:border-box}body{background-attachment:fixed;background-color:#333;background-image:url(https://www.transparenttextures.com/patterns/axiom-pattern.png);color:#fff;font-family:Open Sans,Helvetica Neue,Helvetica,Arial,sans-serif;font-size:16px;line-height:1.8}code[class*=language-],pre[class*=language-]{font-size:14px!important}pre[class*=language-]{border:1px solid #545454!important;border-radius:0!important;box-shadow:none!important}a{background-color:#333;color:#fff;display:inline-block;padding:0 5px}a:hover{background-color:#444;text-decoration:underline}a.anchor{background-color:transparent;margin-right:10px}a.anchor svg path{fill:#fff}.ff-container,img{display:block!important;margin:0 auto;max-width:500px}.ff-container img{margin:0;max-width:none;width:100%}.ff-container canvas{width:100%}@media(max-width:768px){.post-image{max-width:100%}}blockquote{border-left:8px solid #ccc;margin-left:15px;padding-left:15px}table{border-collapse:collapse}table td,table th{border:1px solid rgba(255,255,255,.2);padding:0 10px}input{background-color:rgba(255,255,255,.2);border:2px solid #fff;border-radius:3px;color:#fff;font-family:Courier;margin:0 5px;max-width:100%;outline:none;padding:2px 5px;width:500px}.interactive-label{background-color:#ff5f00;border-radius:2px;float:right;font-size:12px;font-weight:700;padding:3px 7px;text-transform:uppercase}code[class*=language-],pre[class*=language-]{word-wrap:normal;background:none;color:#fff;font-family:Consolas,Monaco,Andale Mono,Ubuntu Mono,monospace;font-size:1em;-webkit-hyphens:none;-ms-hyphens:none;hyphens:none;line-height:1.5;-moz-tab-size:4;-o-tab-size:4;tab-size:4;text-align:left;text-shadow:0 -.1em .2em #000;white-space:pre;word-break:normal;word-spacing:normal}:not(pre)>code[class*=language-],pre[class*=language-]{background:#141414}pre[class*=language-]{border:.3em solid #545454;border-radius:.5em;box-shadow:inset 1px 1px .5em #000;margin:.5em 0;overflow:auto;padding:1em}pre[class*=language-]::selection{background:#27292a}code[class*=language-]::selection,code[class*=language-] ::selection,pre[class*=language-]::selection,pre[class*=language-] ::selection{background:hsla(0,0%,93%,.15);text-shadow:none}:not(pre)>code[class*=language-]{border:.13em solid #545454;border-radius:.3em;box-shadow:inset 1px 1px .3em -.1em #000;padding:.15em .2em .05em;white-space:normal}.token.cdata,.token.comment,.token.doctype,.token.prolog{color:#787878}.token.namespace,.token.punctuation{opacity:.7}.token.boolean,.token.deleted,.token.number,.token.tag{color:#cf694a}.token.builtin,.token.constant,.token.keyword,.token.property,.token.selector,.token.symbol{color:#f9ee9a}.language-css .token.string,.style .token.string,.token.attr-name,.token.attr-value,.token.char,.token.entity,.token.inserted,.token.operator,.token.string,.token.url,.token.variable{color:#919e6b}.token.atrule{color:#7386a5}.token.important,.token.regex{color:#e9c163}.token.bold,.token.important{font-weight:700}.token.italic{font-style:italic}.token.entity{cursor:help}.language-markup .token.attr-name,.language-markup .token.punctuation,.language-markup .token.tag{color:#ad895c}.token{position:relative;z-index:1}.line-highlight.line-highlight{background:hsla(0,0%,33%,.25);background:linear-gradient(90deg,hsla(0,0%,33%,.1) 70%,hsla(0,0%,33%,0));border-bottom:1px dashed #545454;border-top:1px dashed #545454;margin-top:.75em;z-index:0}.line-highlight.line-highlight:before,.line-highlight.line-highlight[data-end]:after{background-color:#8794a6;color:#f5f2f0}.component-BlogPost .post-title{margin-bottom:0}.component-BlogPost .post-date{margin-top:0;text-align:right}.component-BlogPost .read-more-posts{padding:40px 0 10px;text-align:center}.page-index .about-me{margin:0 auto;max-width:500px;padding:30px 0 10px}.page-index .about-me img{float:right}.page-index article{clear:both;margin:20px 0}.page-index article a{display:block;padding:5px 15px;text-decoration:none}.page-index article .title{float:left;margin:0}.page-index article .date{float:right;margin:5px 0 0}.page-index article .description{clear:both;margin:0}.component-Layout{background-color:rgba(0,0,0,.3);margin:0 auto;max-width:900px;padding:20px}.component-Layout header{text-align:right}.component-Layout header a{color:#fff;font-size:25px}.component-Layout footer{padding-top:30px;text-align:center}.component-Layout footer a{margin:5px 10px}</style><meta name="generator" content="Gatsby 3.14.1"/><style>.gatsby-image-wrapper{position:relative;overflow:hidden}.gatsby-image-wrapper img{bottom:0;height:100%;left:0;margin:0;max-width:none;padding:0;position:absolute;right:0;top:0;width:100%;object-fit:cover}.gatsby-image-wrapper [data-main-image]{opacity:0;transform:translateZ(0);transition:opacity .25s linear;will-change:opacity}.gatsby-image-wrapper-constrained{display:inline-block;vertical-align:top}</style><noscript><style>.gatsby-image-wrapper noscript [data-main-image]{opacity:1!important}.gatsby-image-wrapper [data-placeholder-image]{opacity:0!important}</style></noscript>
4<script type="module">const e="undefined"!=typeof HTMLImageElement&&"loading"in HTMLImageElement.prototype;e&&document.body.addEventListener("load",(function(e){if(void 0===e.target.dataset.mainImage)return;if(void 0===e.target.dataset.gatsbyImageSsr)return;const t=e.target;let a=null,n=t;for(;null===a&&n;)void 0!==n.parentNode.dataset.gatsbyImageWrapper&&(a=n.parentNode),n=n.parentNode;const o=a.querySelector("[data-placeholder-image]"),r=new Image;r.src=t.currentSrc,r.decode().catch((()=>{})).then((()=>{t.style.opacity=1,o&&(o.style.opacity=0,o.style.transition="opacity 500ms linear")}))}),!0);</script>
4<title data-react-helmet="true">Full text search explained | jyelewis.com</title><meta data-react-helmet="true" name="description" content="Finding a needle in a haystack, very quickly"/><meta data-react-helmet="true" property="og:title" content="Full text search explained"/><meta data-react-helmet="true" property="og:description" content="Finding a needle in a haystack, very quickly"/><meta data-react-helmet="true" property="og:type" content="website"/><meta data-react-helmet="true" name="twitter:card" content="summary"/><meta data-react-helmet="true" name="twitter:creator" content="@jyelew15"/><meta data-react-helmet="true" name="twitter:title" content="Full text search explained"/><meta data-react-helmet="true" name="twitter:description" content="Finding a needle in a haystack, very quickly"/><link rel="sitemap" type="application/xml" href="/sitemap/sitemap-index.xml"/><link rel="alternate" type="application/rss+xml" title="jyelewis.com" href="/rss.xml"/><link rel="icon" href="/favicon-32x32.png?v=023d62ffd4c5f162e76f9b30e3bdec7a" type="image/png"/><link rel="manifest" href="/manifest.webmanifest" crossorigin="anonymous"/><link rel="apple-touch-icon" sizes="48x48" href="/icons/icon-48x48.png?v=023d62ffd4c5f162e76f9b30e3bdec7a"/><link rel="apple-touch-icon" sizes="72x72" href="/icons/icon-72x72.png?v=023d62ffd4c5f162e76f9b30e3bdec7a"/><link rel="apple-touch-icon" sizes="96x96" href="/icons/icon-96x96.png?v=023d62ffd4c5f162e76f9b30e3bdec7a"/><link rel="apple-touch-icon" sizes="144x144" href="/icons/icon-144x144.png?v=023d62ffd4c5f162e76f9b30e3bdec7a"/><link rel="apple-touch-icon" sizes="192x192" href="/icons/icon-192x192.png?v=023d62ffd4c5f162e76f9b30e3bdec7a"/><link rel="apple-touch-icon" sizes="256x256" href="/icons/icon-256x256.png?v=023d62ffd4c5f162e76f9b30e3bdec7a"/><link rel="apple-touch-icon" sizes="384x384" href="/icons/icon-384x384.png?v=023d62ffd4c5f162e76f9b30e3bdec7a"/><link rel="apple-touch-icon" sizes="512x512" href="/icons/icon-512x512.png?v=023d62ffd4c5f162e76f9b30e3bdec7a"/><link as="script" rel="preload" href="/webpack-runtime-7d0cf3d0c70c21dcb02c.js"/><link as="script" rel="preload" href="/framework-2142241f49842bb6f519.js"/><link as="script" rel="preload" href="/app-45c865905e28484bcf72.js"/><link as="script" rel="preload" href="/7d5d0410a46ffedd5bd679912f5ba9541a8ddc57-b7e6b94aa36eb2edbc5f.js"/><link as="script" rel="preload" href="/component---src-pages-blog-mdx-slug-tsx-fec480f768f080eb8f63.js"/><link as="fetch" rel="preload" href="/page-data/blog/2023-01-30-full-text-search/page-data.json" crossorigin="anonymous"/><link as="fetch" rel="preload" href="/page-data/app-data.json" crossorigin="anonymous"/></head><body><div id="___gatsby"><div style="outline:none" tabindex="-1" id="gatsby-focus-wrapper"><div class="component-Layout"><header><a class="header-link-home" href="/">Jye Lewis's blog</a></header><main><article class="component-BlogPost"><h1 class="post-title">Full text search explained</h1><p class="post-date">12<!-- --> min read.<!-- --> <!-- -->30 Jan, 2023</p><div class="post-content"><h2 id="what-is-full-text-search" style="position:relative"><a href="/blog/2023-01-30-full-text-search/#what-is-full-text-search" aria-label="what is full text search permalink" class="anchor before"><svg aria-hidden="true" focusable="false" height="16" version="1.1" viewBox="0 0 16 16" width="16"><path fill-rule="evenodd" d="M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z"></path></svg></a>What is full text search?</h2><p>Full text search (FTS for short) is a method of storing & querying blocks of text 5such that a user can search for any word of phrase and efficiently have all documents matching that phrase returned.</p><p>Search engines such as Google are one of the largest examples of FTS. Effectively indexing all the visible content on the internet and performing searches across that dataset in under a second.</p><h2 id="how-full-text-search-works" style="position:relative"><a href="/blog/2023-01-30-full-text-search/#how-full-text-search-works" aria-label="how full text search works permalink" class="anchor before"><svg aria-hidden="true" focusable="false" height="16" version="1.1" viewBox="0 0 16 16" width="16"><path fill-rule="evenodd" d="M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z"></path></svg></a>How full text search works</h2><blockquote><p><em>Disclaimer: text search is a large and complicated topic, this has been intentionally reduced to a very simple version</em>. For a more complete & modern technique read up on <a href="https://en.wikipedia.org/wiki/Inverted_index" target="_blank" rel="noopener">Inverted indexes</a></p></blockquote><p>Consider the phrase <code class="language-text">"That fox is quick & brown!"</code></p><p>There are many matching words & phrases in here:</p><ul><li><code class="language-text">"That fox"</code></li><li><code class="language-text">"Fox"</code></li><li><code class="language-text">"Quick & brown"</code></li><li><code class="language-text">"Fox is quick"</code></li><li><code class="language-text">"Brown"</code></li><li>etc.</li></ul><p>Complicating further, someone searching may not use the exact same grammar in their search term, but expect a match.</p><p>i.e. <code class="language-text">"Fox: quick, brown"</code> should probably match <code class="language-text">"That fox is quick & brown!"</code></p><h3 id="tokenisation" style="position:relative"><a href="/blog/2023-01-30-full-text-search/#tokenisation" aria-label="tokenisation permalink" class="anchor before"><svg aria-hidden="true" focusable="false" height="16" version="1.1" viewBox="0 0 16 16" width="16"><path fill-rule="evenodd" d="M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z"></path></svg></a>Tokenisation</h3><p>To achieve this, full text breaks text into many <em>tokens</em>. 6Tokenisation is a multi-step process.</p><div><div class="interactive-label">Interactive</div><label>Input text:<input type="text" value="That fox is QUICK & brown!"/><small>(Change me)</small></label><div><ol><li>Convert to lowercase<br/><span style="margin-left:10px;margin-bottom:10px;display:block"><code class="language-text">That fox is QUICK & brown!</code> â¡ï¸Â <code class="language-text">that fox is quick & brown!</code></span></li><li>Flatten symbols & boundary characters into a standard character & reduce to 1 sequential delimiter<br/><span style="margin-left:10px;margin-bottom:10px;display:block"><code class="language-text">that fox is quick & brown!</code> â¡ï¸Â <code class="language-text">that fox is quick brown</code></span></li><li>Remove<!-- --> <a href="https://en.wikipedia.org/wiki/Stop_word" target="_blank" rel="noreferrer">stop words</a> <!-- -->(words insignificant to searching, i.e 'the', 'is', 'a', 'their')<br/><span style="margin-left:10px;margin-bottom:10px;display:block"><code class="language-text">that fox is quick brown</code> â¡ï¸Â <code class="language-text">fox quick brown</code></span></li><li>Break into tokens on boundaries (delimiters such as hyphen, space, parentheses)<br/><ul><li><code class="language-text">fox quick brown</code></li><li><code class="language-text">quick brown</code></li><li><code class="language-text">brown</code></li></ul></li></ol></div></div><p>These tokens are then all stored in a format that allows efficie
6ntly searching for records based on the characters a token starts with.</p><p>The easiest example of one of these formats is a paper dictionary, by storing all words alphabetically it's easy to find all words starting with <code class="language-text">"tra"</code> by navigating back and forth through the book.</p><h3 id="searching-tokens" style="position:relative"><a href="/blog/2023-01-30-full-text-search/#searching-tokens" aria-label="searching tokens permalink" class="anchor before"><svg aria-hidden="true" focusable="false" height="16" version="1.1" viewBox="0 0 16 16" width="16"><path fill-rule="evenodd" d="M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z"></path></svg></a>Searching tokens</h3><p>When a search term is entered, it goes through a similar clean up process as tokenisation.</p><ol><li>Convert to lowercase</li><li>Flatten symbols & boundary characters</li><li>Remove stop words</li></ol><div><div class="interactive-label">Interactive</div><label><input type="text" value="Fox: quick, brown" style="width:200px"/> â¡ï¸Â <code class="language-text">fox quick brown</code></label></div><p>We can then use our paper dictionary trick to find any tokens we've stored starting with, or matching <code class="language-text">fox quick brown</code>. 7Once we find a matching token, we look across to see what record it references and find out original document: <code class="language-text">That fox is QUICK & brown!</code></p><h3 id="index-storage-formats" style="position:relative"><a href="/blog/2023-01-30-full-text-search/#index-storage-formats" aria-label="index storage formats permalink" class="anchor before"><svg aria-hidden="true" focusable="false" height="16" version="1.1" viewBox="0 0 16 16" width="16"><path fill-rule="evenodd" d="M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z"></path></svg></a>Index storage formats</h3><p>Now that we have a set of tokens, we need to store them in a format we can look up quickly.</p><ul><li>Use a big array<ul><li>This is the easiest solution, however inserting new tokens requires moving all subsequent items around to make room. For write heavy workloads this isn't optimal</li></ul></li><li>Use a <a href="https://en.wikipedia.org/wiki/Linked_list" target="_blank" rel="noopener">linked-list</a><ul><li>Solves our insert problem, we can insert anywhere with minimal cost, however now searches require iterating every item in the list to find items that match, making search slow</li></ul></li><li>Use a <a href="https://en.wikipedia.org/wiki/Binary_tree" target="_blank" rel="noopener">binary tree</a><ul><li>We can insert new items quickly, and efficiently search this tree to find items that match our search text!</li><li>However, every token still needs to be stored in full, causing our index to take up significantly more space than our original items</li></ul></li><li>Use a <a href="https://en.wikipedia.org/wiki/Radix_tree" target="_blank" rel="noopener">radix tree</a><ul><li>This allows us to insert new items quickly, similar to the binary tree - however duplicate text from tokens is stored on the nodes as we descend the tree, allowing us to save disk space</li></ul></li></ul><a href="https://commons.wikimedia.org/w/index.php?curid=2118795" style="background-color:white;color:#000;display:block;margin:0 auto;max-width:400px;text-align:right;border-radius:3px;padding:0 5px" target="_blank" rel="noopener"><img src="/7b8d0f29fbec7873784d05162a4c6d60/radix-tree.svg" alt="Radix tree example" class="post-image"/><p> Example radix tree<br/>By Claudio Rocchini - Own work, CC BY 2.5</p></a><h2 id="implementation" style="position:relative"><a href="/blog/2023-01-30-full-text-search/#implementation" aria-label="implementation permalink" class="anchor before"><svg aria-hidden="true" focusable="false" height="16" version="1.1" viewBox="0 0 16 16" width="16"><path fill-rule="evenodd" d="M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z"></path></svg></a>Implementation</h2><h3 id="clean-text--remove-stop-words" style="position:relative"><a href="/blog/2023-01-30-full-text-search/#clean-text--remove-stop-words" aria-label="clean text remove stop words permalink" class="anchor before"><svg aria-hidden="true" focusable="false" height="16" version="1.1" viewBox="0 0 16 16" width="16"><path fill-rule="evenodd" d="M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z"></path></svg></a>Clean text & remove stop words</h3><div class="gatsby-highlight" data-language="typescript"><pre class="language-typescript"><code class="language-typescript"><span class="token keyword">const</span> stopWords <span class="token operator">=</span> <span class="token template-string"><span class="token template-punctuation string">`</span><span class="token string">i 8a 9an 10and 11this 12...etc 13</span><span class="token template-punctuation string">`</span></span><span class="token punctuation">;</span> 14 15<span class="token keyword">export</span> <span class="token keyword">function</span> <span class="token function">cleanText</span><span class="token punctuation">(</span>inputText<span class="token operator">:</span> <span class="token builtin">string</span><span class="token punctuation">)</span><span class="token operator">:</span> <span class="token builtin">string</span> <span class="token punctuation">{</span> 16 <span class="token keyword">const</span> cleanTextWithStopwords <span class="token operator">=</span> inputText 17 <span class="token comment">// 1. Convert to lowercase</span> 18 <span class="token punctuation">.</span><span class="token function">toLocaleLowerCase</span><span class="token punctuation">(</span><span class="token punctuation">)</span> 19 <span class="token comment">// 2a. Flatten symbols & boundary characters into a standard character</span> 20 <span class="token punctuation">.</span><span class="token function">replace</span><span class="token punctuation">(</span><span class="token regex"><span class="token regex-delimiter">/</span><span class="token regex-source language-regex">[^a-z0-9 ]</span><span class="token regex-delimiter">/</span><span class="token regex-flags">g</span></span><span class="token punctuation">,</span> <span class="token string">" "</span><span class="token punctuation">)</span> 21 <span class="token comment">// 2b. reduce to 1 sequential delimiter</span> 22 <span class="token punctuation">.</span><span class="token function">replace</span><span class="token punctuation">(</span><span class="token regex"><span class="token regex-delimiter">/</span><span class="token regex-source language-regex"> +</span><span class="token regex-delimiter">/</span><span class="token regex-flags">g</span></span><span class="token punctuation">,</span> <span class="token string">" "</span><span class="token punctuation">)</span> 23 <span class="token punctuation">.</span><span class="token function">trim</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span> 24 25 <span class="token comment">// 3. Remove stop words (words insignificant to searching, i.e 'the', 'is', 'a', 'their')</span> 26 <span class="token keyword">return</span> stopWords 27 <span class="token punctuation">.</span><span class="token function">reduce</span><span class="token punctuation">(</span>
28 <span class="token punctuation">(</span>str<span class="token punctuation">,</span> stopWord<span class="token punctuation">)</span> <span class="token operator">=></span> <span class="token template-string"><span class="token template-punctuation string">`</span><span class="token string"> </span><span class="token interpolation"><span class="token interpolation-punctuation punctuation">${</span>str<span class="token interpolation-punctuation punctuation">}</span></span><span class="token string"> </span><span class="token template-punctuation string">`</span></span><span class="token punctuation">.</span><span class="token function">replace</span><span class="token punctuation">(</span><span class="token template-string"><span class="token template-punctuation string">`</span><span class="token string"> </span><span class="token interpolation"><span class="token interpolation-punctuation punctuation">${</span>stopWord<span class="token interpolation-punctuation punctuation">}</span></span><span class="token string"> </span><span class="token template-punctuation string">`</span></span><span class="token punctuation">,</span> <span class="token string">" "</span><span class="token punctuation">)</span><span class="token punctuation">,</span> 29 cleanTextWithStopwords 30 <span class="token punctuation">)</span> 31 <span class="token punctuation">.</span><span class="token function">trim</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span> 32<span class="token punctuation">}</span> 33 34<span class="token function">assert</span><span class="token punctuation">(</span><span class="token function">cleanText</span><span class="token punctuation">(</span><span class="token string">"This is a nice day!"</span><span class="token punctuation">)</span> <span class="token operator">===</span> <span class="token string">"nice day"</span><span class="token punctuation">)</span></code></pre></div><p>This should produce the same output as the interactive example in <a href="/blog/2023-01-30-full-text-search/#searching-tokens">'Searching tokens'</a></p><h3 id="split-cleaned-text-into-tokens" style="position:relative"><a href="/blog/2023-01-30-full-text-search/#split-cleaned-text-into-tokens" aria-label="split cleaned text into tokens permalink" class="anchor before"><svg aria-hidden="true" focusable="false" height="16" version="1.1" viewBox="0 0 16 16" width="16"><path fill-rule="evenodd" d="M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z"></path></svg></a>Split cleaned text into tokens</h3><div class="gatsby-highlight" data-language="typescript"><pre class="language-typescript"><code class="language-typescript"><span class="token keyword">export</span> <span class="token keyword">function</span> <span class="token function">tokenise</span><span class="token punctuation">(</span>inputText<span class="token operator">:</span> <span class="token builtin">string</span><span class="token punctuation">)</span> <span class="token punctuation">{</span> 35 <span class="token comment">// using our function from above</span> 36 <span class="token keyword">const</span> cleanedText <span class="token operator">=</span> <span class="token function">cleanText</span><span class="token punctuation">(</span>inputText<span class="token punctuation">)</span><span class="token punctuation">;</span> 37 38 <span class="token comment">// 4. Break into tokens on boundaries (delimiters such as hyphen, space, parentheses)</span> 39 <span class="token keyword">let</span> splitIndex <span class="token operator">=</span> <span class="token number">0</span><span class="token punctuation">;</span> 40 <span class="token keyword">const</span> tokens<span class="token operator">:</span> <span class="token builtin">string</span><span class="token punctuation">[</span><span class="token punctuation">]</span> <span class="token operator">=</span> <span class="token punctuation">[</span><span class="token punctuation">]</span><span class="token punctuation">;</span> 41 <span class="token keyword">
41for</span> <span class="token punctuation">(</span><span class="token keyword">const</span> word <span class="token keyword">of</span> cleanedText<span class="token punctuation">.</span><span class="token function">split</span><span class="token punctuation">(</span><span class="token string">" "</span><span class="token punctuation">)</span><span class="token punctuation">)</span> <span class="token punctuation">{</span> 42 tokens<span class="token punctuation">.</span><span class="token function">push</span><span class="token punctuation">(</span>cleanedText<span class="token punctuation">.</span><span class="token function">substring</span><span class="token punctuation">(</span>splitIndex<span class="token punctuation">)</span><span class="token punctuation">)</span><span class="token punctuation">;</span> 43 splitIndex <span class="token operator">+=</span> word<span class="token punctuation">.</span>length <span class="token operator">+</span> <span class="token number">1</span><span class="token punctuation">;</span> 44 <span class="token punctuation">}</span> 45 46 <span class="token keyword">return</span> tokens<span class="token punctuation">;</span> 47<span class="token punctuation">}</span></code></pre></div><p>This should produce the same output as the interactive example in <a href="/blog/2023-01-30-full-text-search/#tokenisation">'Tokenisation'</a></p><h3 id="storing-our-tokens-in-a-searchable-structure" style="position:relative"><a href="/blog/2023-01-30-full-text-search/#storing-our-tokens-in-a-searchable-structure" aria-label="storing our tokens in a searchable structure permalink" class="anchor before"><svg aria-hidden="true" focusable="false" height="16" version="1.1" viewBox="0 0 16 16" width="16"><path fill-rule="evenodd" d="M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z"></path></svg></a>Storing our tokens in a searchable structure</h3><p>A simple structure we can use for searching is a <a href="https://en.wikipedia.org/wiki/Binary_tree" target="_blank" rel="noopener">binary tree</a>. 48This allows both efficiently inserting new tokens and quickly searching for matching tokens: <code class="language-text">O(log(n))</code> at best, <code class="language-text">O(n)</code> at worst.</p><p>Let's use the open source <a href="https://www.npmjs.com/package/binary-search-tree" target="_blank" rel="noopener">Binary search tree</a> from NPM to avoid implementing this from scratch (possibly a topic for another post).</p><p>This package allows us to (among other features)</p><ul><li>Add items: <code class="language-text">bst.insert(key, value)</code></li><li>Query items within a range: <code class="language-text">bst.betweenBounds({ $gte: 2, $lte: 6, })</code></li><li>Provide custom compare functions: <code class="language-text">new BinarySearchTree({ compareKeys: myCustomFunction })</code></li></ul><p>This should be enough to create a custom search tree for our text tokens</p><h3 id="putting-it-all-together" style="position:relative"><a href="/blog/2023-01-30-full-text-search/#putting-it-all-together" aria-label="putting it all together permalink" class="anchor before"><svg aria-hidden="true" focusable="false" height="16" version="1.1" viewBox="0 0 16 16" width="16"><path fill-rule="evenodd" d="M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z"></path></svg></a>Putting it all together</h3><div class="gatsby-highlight" data-language="typescript"><pre class="language-typescript"><code class="language-typescript"><span class="token keyword">
48import</span> <span class="token punctuation">{</span> BinarySearchTree <span class="token punctuation">}</span> <span class="token keyword">from</span> <span class="token string">"binary-search-tree"</span><span class="token punctuation">;</span> 49<span class="token keyword">import</span> <span class="token punctuation">{</span> cleanText<span class="token punctuation">,</span> tokenise <span class="token punctuation">}</span> <span class="token keyword">from</span> <span class="token string">"./utils/tokenise"</span><span class="token punctuation">;</span> 50 51<span class="token comment">// custom type for the documents we want to store in this db</span> 52<span class="token comment">// 'id' would allow us to look up the original document in another DB (i.e Postgresql)</span> 53<span class="token keyword">interface</span> <span class="token class-name">Document</span> <span class="token punctuation">{</span> 54 id<span class="token operator">:</span> <span class="token builtin">string</span><span class="token punctuation">;</span> 55 text<span class="token operator">:</span> <span class="token builtin">string</span><span class="token punctuation">;</span> 56<span class="token punctuation">}</span> 57 58<span class="token keyword">export</span> <span class="token keyword">class</span> <span class="token class-name">FullTextSearch</span> <span class="token punctuation">{</span> 59 <span class="token keyword">private</span> tokensBST <span class="token operator">=</span> <span class="token keyword">new</span> <span class="token class-name">BinarySearchTree</span><span class="token punctuation">(</span><span class="token punctuation">{</span> 60 <span class="token comment">// provide a custom compare function for when we are searching</span> 61 <span class="token comment">// this allows us to search by matching the _start_ of a token</span> 62 <span class="token function-variable function">compareKeys</span><span class="token operator">:</span> <span class="token punctuation">(</span>searchText<span class="token operator">:</span> <span class="token builtin">string</span><span class="token punctuation">,</span> tokenText<span class="token operator">:</span> <span class="token builtin">string</span><span class="token punctuation">)</span> <span class="token operator">=></span> <span class="token punctuation">{</span> 63 <span class="token comment">// we consider tokens equal if they start with the search text</span> 64 <span class="token keyword">if</span> <span class="token punctuation">(</span>searchText<span class="token punctuation">.</span><span class="token function">startsWith</span><span class="token punctuation">(</span>tokenText<span class="token punctuation">)</span><span class="token punctuation">)</span> <span class="token keyword">return</span> <span class="token number">0</span><span class="token punctuation">;</span> 65 66 <span class="token comment">// otherwise sort alphabetically</span> 67 <span class="token keyword">return</span> searchText<span class="token punctuation">.</span><span class="token function">localeCompare</span><span class="token punctuation">(</span>tokenText<span class="token punctuation">)</span><span class="token punctuation">;</span> 68 <span class="token punctuation">}</span><span class="token punctuation">,</span> 69 <span class="token punctuation">}</span><span class="token punctuation">)</span><span class="token punctuation">;</span> 70 71 <span class="token keyword">public</span> <span class="token function">indexDocument</span><span class="token punctuation">(</span>document<span class="token operator">:</span> Document<span class="token punctuation">)</span> <span class="token punctuation">{</span> 72 <span class="token comment">// create a list of tokens from our document</span> 73 <span class="token function">tokenise</span><span class="token punctuation">(</span>document<span class="token punctuation">.</span>text<span class="token punctuation">)</span> 74 <span class="token comment">// insert each token into our BST for future lookups</span>
75 <span class="token punctuation">.</span><span class="token function">forEach</span><span class="token punctuation">(</span><span class="token punctuation">(</span>token<span class="token punctuation">)</span> <span class="token operator">=></span> <span class="token keyword">this</span><span class="token punctuation">.</span>tokensBST<span class="token punctuation">.</span><span class="token function">insert</span><span class="token punctuation">(</span>token<span class="token punctuation">,</span> document<span class="token punctuation">)</span><span class="token punctuation">)</span><span class="token punctuation">;</span> 76 <span class="token punctuation">}</span> 77 78 <span class="token keyword">public</span> <span class="token function">search</span><span class="token punctuation">(</span>searchTerm<span class="token operator">:</span> <span class="token builtin">string</span><span class="token punctuation">)</span><span class="token operator">:</span> Set<span class="token operator"><</span>Document<span class="token operator">></span> <span class="token punctuation">{</span> 79 <span class="token comment">// clean our search term</span> 80 <span class="token comment">// this will remove any characters that are never found in indexed tokens</span> 81 <span class="token keyword">const</span> cleanedSearchTerm <span class="token operator">=</span> <span class="token function">cleanText</span><span class="token punctuation">(</span>searchTerm<span class="token punctuation">)</span><span class="token punctuation">;</span> 82 83 <span class="token comment">// perform a binary search of our sorted tokens to find everything matching</span> 84 <span class="token comment">// return as a set, we shouldn't have any duplicate documents</span> 85 <span class="token keyword">return</span> <span class="token keyword">new</span> <span class="token class-name">Set<span class="token operator"><</span>Document<span class="token operator">></span></span><span class="token punctuation">(</span> 86 <span class="token keyword">this</span><span class="token punctuation">.</span>tokensBST<span class="token punctuation">.</span><span class="token function">betweenBounds</span><span class="token punctuation">(</span><span class="token punctuation">{</span> 87 $gte<span class="token operator">:</span> cleanedSearchTerm<span class="token punctuation">,</span> 88 $lte<span class="token operator">:</span> cleanedSearchTerm<span class="token punctuation">,</span> 89 <span class="token punctuation">}</span><span class="token punctuation">)</span> 90 <span class="token punctuation">)</span><span class="token punctuation">;</span> 91 <span class="token punctuation">}</span> 92<span class="token punctuation">}</span></code></pre></div><p>Done! We can now create FullTextSearch instances and add some documents for searching</p><p><img src="/7cd292f3cede35255d07cbd0051897e1/search-book.ani.webp" alt="Searching book" class="post-image"/></p><h3 id="using-it" style="position:relative"><a href="/blog/2023-01-30-full-text-search/#using-it" aria-label="using it permalink" class="anchor before"><svg aria-hidden="true" focusable="false" height="16" version="1.1" viewBox="0 0 16 16" width="16"><path fill-rule="evenodd" d="M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z"></path></svg></a>Using it</h3><div class="gatsby-highlight" data-language="typescript"><pre class="language-typescript"><code class="language-typescript"><span class="token keyword">const</span> fts <span class="token operator">=</span> <span class="token keyword">new</span> <span class="token class-name">FullTextSearch</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span> 93 94<span class="token comment">// load our 'db' up with some example documents</span> 95fts<span class="token punctuation">.</span><span class="token function">indexDocument</span><span class="token punctuation">(</span><span class="token punctuation">{</span> 96 id<span class="token operator">:</span> <span class="token string">"1"</span><span class="token punctuation">,</span> 97 text<span class="token operator">:</span> <span class="token string">"Hello world"</span><span class="token punctuation">,</span> 98<span class="token punctuation">}</span><span class="token punctuation">)</span><span class="token punctuation">;</span> 99fts<span class="token punctuation">.</span><span class="token function">indexDocument</span><span class="token punctuation">(</span><span class="token punctuation">{</span> 100 id<span class="token operator">:</span> <span class="token string">"2"</span><span class="token punctuation">,</span> 101 text<span class="token operator">:</span> <span class="token string">"Hello Friends"</span><span class="token punctuation">,</span> 102<span class="token punctuation">}</span><span class="token punctuation">)</span><span class="token punctuation">;</span> 103fts<span class="token punctuation">.</span><span class="token function">indexDocument</span><span class="token punctuation">(</span><span class="token punctuation">{</span> 104 id<span class="token operator">:</span> <span class="token string">"4"</span><span class="token punctuation">,</span> 105 text<span class="token operator">:</span> <span class="token string">"Hello, hello!"</span><span class="token punctuation">,</span> 106<span class="token punctuation">}</span><span class="token punctuation">)</span><span class="token punctuation">;</span> 107fts<span class="token punctuation">.</span><span class="token function">indexDocument</span><span class="token punctuation">(</span><span class="token punctuation">{</span> 108 id<span class="token operator">:</span> <span class="token string">"3"</span><span class="token punctuation">,</span> 109 text<span class="token operator">:</span> <span class="token string">"Foo bar"</span><span class="token punctuation">,</span> 110<span class="token punctuation">}</span><span class="token punctuation">)</span><span class="token punctuation">;</span> 111 112<span class="token comment">// throw some example queries at it</span> 113<span class="token keyword">const</span> exampleQueries <span class="token operator">=</span> <span class="token punctuation">[</span><span class="token string">"hello!"</span><span class="token punctuation">,</span> <span class="token string">"world"</span><span class="token punctuation">,</span> <span class="token string">"friends"</span><span class="token punctuation">,</span> <span class="token string">"foo"</span><span class="token punctuation">,</span> <span class="token string">"bar"</span><span class="token punctuation">]</span><span class="token punctuation">;</span> 114exampleQueries<span class="token punctuation">.</span><span class="token function">forEach</span><span class="token punctuation">(</span><span class="token punctuation">(</span>searchTerm<span class="token punctuation">)</span> <span class="token operator">=></span> 115 <span class="token builtin">console</span><span class="token punctuation">.</span><span class="token function">log</span><span class="token punctuation">(</span><span class="token template-string"><span class="token template-punctuation string">`</span><span class="token string">'</span><span class="token interpolation"><span class="token interpolation-punctuation punctuation">${</span>searchTerm<span class="token interpolation-punctuation punctuation">}</span></span><span class="token string">':</span><span class="token template-punctuation string">`</span></span><span class="token punctuation">,</span> fts<span class="token punctuation">.</span><span class="token function">search</span><span class="token punctuation">(</span>searchTerm<span class="token punctuation">)</span><span class="token punctuation">)</span> 116<span class="token punctuation">)</span><span class="token punctuation">;</span></code></pre></div><p>Produces, as expected</p><div class="gatsby-highlight" data-language="text"><pre class="language-text"><code class="language-text">'hello!': Set(3) { 117 { id: '2', text: 'Hello Friends' }, 118 { id: '4', text: 'Hello, hello!' }, 119 { id: '1', text: 'Hello world' } 120} 121'world': Set(1) { { id: '1', text: 'Hello world' } } 122'friends': Set(1) { { id: '2', text: 'Hello Friends' } } 123'foo': Set(1) { { id: '3', text: 'Foo bar' } } 124'bar': Set(1) { { id: '3', text: 'Foo bar' } }</code></pre></div><h2 id="fight-me-ill-could-have-done-this-with-a-big-old-text-file--grep" style="position:relative"><a href="/blog/2023-01-30-full-text-search/#fight-me-ill-could-have-done-this-with-a-big-old-text-file--grep" aria-label="fight me ill could have done this with a big old text file grep permalink" class="anchor before"><svg aria-hidden="true" focusable="false" height="16" version="1.1" viewBox="0 0 16 16" width="16"><path fill-rule="evenodd" d="M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z"></path></svg></a>Fight me, I'll could have done this with a big old text file & grep</h2><p>Of course, we could have also solved this problem by simply searching for substrings in every document</p><div class="gatsby-highlight" data-language="typescript"><pre class="language-typescript"><code class="language-typescript"><span class="token keyword">export</span> <span class="token keyword">function</span> <span class="token function">search</span><span class="token punctuation">(</span>documents<span class="token operator">:</span> Document<span class="token punctuation">[</span><span class="token punctuation">]</span><span class="token punctuation">,</span> searchTerm<span class="token operator">:</span> <span class="token builtin">string</span><span class="token punctuation">)</span><span class="token operator">:</span> Document<span class="token punctuation">[</span><span class="token punctuation">]</span> <span class="token punctuation">{</span> 125 <span class="token keyword">return</span> documents<span class="token punctuation">.</span><span class="token function">filter</span><span class="token punctuation">(</span>document <span class="token operator">=></span> document<span class="token punctuation">.</span>text<span class="token punctuation">.</span><span class="token function">contains</span><span class="token punctuation">(</span>searchTerm<span class="token punctuation">)</span><span class="token punctuation">)</span><span class="token punctuation">;</span> 126<span class="token punctuation">}</span></code></pre></div><p>However, this would punish the user for even slightly mis-matching their search term from what is in the document. And critically, wouldn't scale as documents[] grew in size. Imagine executing this function over a database of 1 million books (i.e for searching a library).</p><p>Here we can provide kind search results, ignoring punctuation, capitalisation and word that don't significantly change the meaning of a sentence. 127This solution will find results quickly, with a better storage system (e.g. AVL tree) we
127can also ensure searches can always complete with in <code class="language-text">O(log(num_tokens))</code>.</p><p>I recently used a similar technique to <a href="https://github.com/jyelewis/DynamoDB-text-search" target="_blank" rel="noopener">(ab)use AWS DynamoDB as a search engine</a>.</p><h2 id="in-reality" style="position:relative"><a href="/blog/2023-01-30-full-text-search/#in-reality" aria-label="in reality permalink" class="anchor before"><svg aria-hidden="true" focusable="false" height="16" version="1.1" viewBox="0 0 16 16" width="16"><path fill-rule="evenodd" d="M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z"></path></svg></a>In reality</h2><p>Text search is a very solved problem, if you're looking to implement search within your application 128I'd recommend checking out <a href="https://www.elastic.co/" target="_blank" rel="noopener">Elastic Search</a> or <a href="https://www.postgresql.org/docs/current/textsearch.html" target="_blank" rel="noopener">Postgres text search</a>.</p></div><div class="read-more-posts"><a href="/">Read more posts</a></div></article></main><footer><div class="copyright">© 2021-<!-- -->2024<!-- --> Jye Lewis</div><div class="socials"><a href="mailto:[email protected]" title="[email protected]" target="_blank" rel="noopener noreferrer">email</a><a href="https://twitter.com/jyelew15" title="twitter: @jyelew15" target="_blank" rel="noopener">twitter</a><a href="https://fosstodon.org/@jyelewis" title="mastodon: @[email protected]" target="_blank" rel="me">mastodon</a><a href="https://github.com/jyelewis" title="github: @jyelewis" target="_blank" rel="noopener">github</a><a href="https://www.linkedin.com/in/jye-lewis-680ab6120/" title="LinkedIn: Jye Lewis" target="_blank" rel="noopener">linkedin</a><a title="RSS Feed" target="_blank" href="/rss.xml">rss</a></div></footer></div></div><div id="gatsby-announcer" style="position:absolute;top:0;width:1px;height:1px;padding:0;overflow:hidden;clip:rect(0, 0, 0, 0);white-space:nowrap;border:0" aria-live="assertive" aria-atomic="true"></div></div>
128<script id="gatsby-script-loader">/*<![CDATA[*/window.pagePath="/blog/2023-01-30-full-text-search/";/*]]>*/</script>
128<script id="gatsby-chunk-mapping">/*<![CDATA[*/window.___chunkMapping={"polyfill":["/polyfill-dcb1a3dc5aaae995d686.js"],"app":["/app-45c865905e28484bcf72.js"],"component---src-pages-404-tsx":["/component---src-pages-404-tsx-b9d5695a1cf13506e7ca.js"],"component---src-pages-blog-mdx-slug-tsx":["/component---src-pages-blog-mdx-slug-tsx-fec480f768f080eb8f63.js"],"component---src-pages-index-tsx":["/component---src-pages-index-tsx-fa35c34bfcc5330915a6.js"]};/*]]>*/</script>
128<script src="/polyfill-dcb1a3dc5aaae995d686.js" nomodule=""></script>
128<script src="/component---src-pages-blog-mdx-slug-tsx-fec480f768f080eb8f63.js" async=""></script>
128<script src="/7d5d0410a46ffedd5bd679912f5ba9541a8ddc57-b7e6b94aa36eb2edbc5f.js" async=""></script>
128<script src="/app-45c865905e28484bcf72.js" async=""></script>
128<script src="/framework-2142241f49842bb6f519.js" async=""></script>
128<script src="/webpack-runtime-7d0cf3d0c70c21dcb02c.js" async=""></script>
128<!-- Cloudflare Pages Analytics -->
vendor: 99 bytes, line 128
128<script defer src='https://static.cloudflareinsights.com/beacon.min.js' data-cf-beacon='{"token": "
12842612473447940728f67a4936940ae9f
vendor: 13 bytes, line 128
128"}'></script>
128<!-- Cloudflare Pages Analytics --></body></html>
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.