1"use strict";(globalThis.webpackChunkwebsite=globalThis.webpackChunkwebsite||[]).push([[119],{79389(e,n,t){t.r(n),t.d(n,{assets:()=>c,contentTitle:()=>s,default:()=>h,frontMatter:()=>a,metadata:()=>i,toc:()=>l});var i=t(30554),r=t(74848),o=t(28453);t(67555);const a={slug:"incremental",title:"Incremental indexing with Glean",author:"Simon Marlow",author_title:"Simon Marlow",author_url:"https://simonmar.github.io/",author_image_url:"http://simonmar.github.io/images/simonmarlow.jpg",tags:["glean","incremental"]},s="Incremental Indexing with Glean",c={authorsImageUrls:[void 0]},l=[{value:"Background",id:"background",level:2},{value:"How incrementality works",id:"how-incrementality-works",level:2},{value:"What about derived facts?",id:"what-about-derived-facts",level:3},{value:"Performance",id:"performance",level:3},{value:"Stacked incremental DBs",id:"stacked-incremental-dbs",level:3},{value:"Derived facts in stacked incremental DBs",id:"derived-facts-in-stacked-incremental-dbs",level:3}];function d(e){const n={a:"a",code:"code",em:"em",h2:"h2",h3:"h3",img:"img",li:"li",p:"p",pre:"pre",strong:"strong",ul:"ul",...(0,o.R)(),...e.components};return(0,r.jsxs)(r.Fragment,{children:[(0,r.jsxs)(n.p,{children:["This post describes how Glean supports ",(0,r.jsx)(n.em,{children:"incremental indexing"})," of\nsource code, to maintain an up-to-date index of a large repository\nusing minimal resources. This is an overview of the problems and\nsome aspects of how we solved them; for the technical details of the\nimplementation see ",(0,r.jsx)(n.a,{href:"/docs/implementation/incrementality",children:"Implementation Notes:\nIncrementality"}),"."]}
1),"\n",(0,r.jsx)(n.h2,{id:"background",children:"Background"}),"\n",(0,r.jsx)(n.p,{children:"Indexing a large amount of source code can take a long time. It's not\nuncommon for very large indexing jobs to take multple\nhours. Furthermore, a monolithic indexing job produces a large DB that\ncan be slow to ship around, for example to replicate across a fleet of\nGlean servers."}),"\n",(0,r.jsxs)(n.p,{children:["Source code changes often, and we would like to keep the index up to\ndate, but redoing the monolithic indexing job for every change is out\nof the question. Instead what we would like to do is to update a\nmonolithic index with the ",(0,r.jsx)(n.em,{children:"changes"})," between the base revision and the\ncurrent revision, which should hopefully be faster than a full\nrepository indexing job because we only have to look at the things\nthat have changed. The goal is to be able to index the changes in\n",(0,r.jsx)(n.em,{children:"O(changes)"})," rather than ",(0,r.jsx)(n.em,{children:"O(repository)"}),", or as close to that as we\ncan get."]}),"\n",(0,r.jsx)(n.h2,{id:"how-incrementality-works",children:"How incrementality works"}),"\n",(0,r.jsx)(n.p,{children:"To produce a modified DB, we hide a portion of the data in the\noriginal DB, and then stack a DB with the new data on top. Like this:"}),"\n",(0,r.jsx)(n.p,{children:(0,r.jsx)(n.img,{alt:"Incremental stack",src:t(94085).A+"",width:"569",height:"458"})}),"\n",(0,r.jsx)(n.p,{children:'The user asks for the DB "new", and they can then work with the data\nin exactly the same way as they would for a single DB. The fact that\nthe DB is a stack is invisible to a user making queries. Furthermore,\nthe DB "old" still exists and can be used simultaneously, giving us\naccess to multiple versions of the DB at the same time. We can even\nhave many different versions of "new", each replacing a different\nportion of "old".'}),"\n",(0,r.jsx)(n.p,{children:"All of the interesting stuff is in how we hide part of the data in the\nold DB. How do we hide part of the data?"}),"\n",(0,r.jsxs)(n.p,{children:["When facts are added to a Glean DB, the producer of the facts can\nlabel facts with a ",(0,r.jsx)(n.em,{children:"unit"}),". A unit is just a string;
1 Glean doesn't impose\nany particular meaning on units so the indexer can use whatever\nconvention it likes, but typically a unit might be a filename or\nmodule name. For example, when indexing a file F, the indexer would\nlabel all the facts it produces with the unit F."]}),"\n",(0,r.jsx)(n.p,{children:"To hide some of the data in a DB, we specify which units to exclude\nfrom the base DB, like this:"}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{children:"glean create --repo <new> --incremental <old> --exclude A,B,C\n"})}),"\n",(0,r.jsxs)(n.p,{children:["would create a DB ",(0,r.jsx)(n.code,{children:"<new>"})," that stacks on top of ",(0,r.jsx)(n.code,{children:"<old>"}),", hiding units A, B\nand C."]}),"\n",(0,r.jsx)(n.p,{children:"So to index some code incrementally, we would first decide which files\nneed to be reindexed, create an incremental DB that hides those files\nfrom the base DB and then add the new facts."}),"\n",(0,r.jsxs)(n.p,{children:['To implement hiding correctly, Glean has to remember which facts are\nowned by which units. But it\'s not quite that simple, because facts\ncan refer to each other, and the DB contents must be valid (which has\na formal definition but informally means "no dangling\nreferences"). For example, if we have facts ',(0,r.jsx)(n.strong,{children:"x"})," and ",(0,r.jsx)(n.strong,{children:"y"}),", where ",(0,r.jsx)(n.strong,{children:"x"})," refers to\n",(0,r.jsx)(n.strong,{children:"y"}),":"]}),"\n",(0,r.jsx)(n.p,{children:(0,r.jsx)(n.img,{alt:"Fact dependency",src:t(55787).A+"",width:"380",height:"229"})}),"\n",(0,r.jsxs)(n.p,{children:["and we hide unit B, then ",(0,r.jsx)(n.strong,{children:"y"})," must still be visible, otherwise the\nreference from ",(0,r.jsx)(n.strong,{children:"x"})," would be dangling and the DB would not be valid."]}),"\n",(0,r.jsxs)(n.p,{children:["So after the indexer has finished producing facts, Glean propagates\nall the units through the graph of facts, resulting in a mapping from\nfacts to ",(0,r.jsx)(n.strong,{children:"ownership sets"}),"."]}),"\n",(0,r.jsx)(n.p,{children:(0,r.jsx)(n.img,{alt:"Fact dependency with ownership sets",src:t(88092).A+"",width:"389",height:"227"})}),"\n",(0,r.jsx)(n.p,{children:"It turns out that while there are lots of facts, there are relatively\nfew distinct ownership sets. Furthermore, facts produced together tend\nto have the same ownership set, and we can use this to store the\nmapping from facts to ownership sets efficiently. To summarise:"}),"\n",(0,r.jsxs)(n.ul,{children:["\n",(0,r.jsxs)(n.li,{children:["Ownership sets are assigned unique IDs and stored in the DB using ",(0,r.jsx)("a",{href:"https://github.com/facebook/folly/blob/main/folly/experimental/EliasFanoCoding.h",children:"Elias Fano Coding"})]}),"\n",(0,r.jsx)(n.li,{children:"The mapping from facts to ownership sets is\nstored as an interval map"}),"\n"]}),"\n",(0,r.jsx)(n.p,{children:"As a result, keeping all this information only adds about 7% to the DB\nsize."}),"\n",(0,r.jsx)(n.h3,{id:"what-about-derived-facts",children:"What about derived facts?"}),"\n",(0,r.jsx)(n.p,{children:"Derived facts must also have ownership sets, because we have to know\nwhich derived facts to hide. When is a derived fact visible? When all\nof the facts that it was derived from are visible."}),"\n",(0,r.jsxs)(n.p,{children:["For example, if we have a fact ",(0,r.jsx)(n.strong,{children:"r"})," that was derived from ",(0,r.jsx)(n.strong,{children:"p"})," and ",(0,r.jsx)(n.strong,{children:"q"}),":"]}),"\n",(0,r.jsx)(n.p,{children:(0,r.jsx)(n.img,{alt:"Derived fact",src:t(22642).A+"",width:"881",height:"268"})}),"\n",(0,r.jsxs)(n.p,{children:["The ownership set of ",(0,r.jsx)(n.strong,{children:"r"})," is ",(0,r.jsx)(n.code,{children:"{ P } & { Q }"}),", indicating that it should be\nvisible if both P and Q are visible. Note that facts might be derived\nfrom other derived facts, so these ownership expressions can get\narbitrarily large. Normalising them to disjunctive normal form would\nbe possible, but we haven't found that to be necessary so far."]}),"\n",(0,r.jsx)(n.h3,{id:"performance",children:"Performance"}),"\n",(0,r.jsx)(n.p,{children:"There are three aspects to performance:"}),"\n",(0,r.jsxs)(n.ul,{children:["\n",(0,r.jsxs)(n.li,{children:[(0,r.jsx)(n.strong,{children:"Indexing performance"}),". We measured the impact of computing\nownership at indexing time to be 2-3% for Python, for Hack it was in\nthe noise, and we don't\nexpect other languages to be significantly different."]}),"\n",(0,r.jsxs)(n.li,{children:[(0,r.jsx)(n.strong,{children:"Query performance"}),". Initially query performance for an\nincremental stack was much slower because we have to calculate the visibility of every fact discovered during a query. However, with some ",(0,r.jsx)(n.a,{href:"/docs/implementation/incrementality#caching-fact-ownership",children:"caching optimisations"}),' we were able to get the overhead to less than 10% for "typical" queries, of the kind that Glass does. Queries that do a lot of searching may be impacted by around 3x, but these are not typically found in production use cases.']}),"\n",(0,r.jsxs)(n.li,{children:[(0,r.jsx)(n.strong,{children:"Incremental derivation performance"}),". We would like derivation in the incremental DB to take time proportional to the number of facts in the increment. We implemented incremental derivation for some kinds of query; optimising queries to achieve this in general is a hard problem that we'll return to probably next year."]}),"\n"]}),"\n",(0,r.jsx)(n.h3,{id:"stacked-incremental-dbs",children:"Stacked incremental DBs"}),"\n",(0,r.jsx)(n.p,{children:"So far we have only been considering how to stack a single increment\non top of a base DB. What if we want to create deeper stacks?"}),"\n",(0,r.jsx)(n.p,{children:(0,r.jsx)(n.img,{alt:"Stacked incremental database",src:t(32313).A+"",width:"564",height:"633"})}),"\n",(0,r.jsx)(n.p,{children:'The DB "newer" stacks on top of "new", and hides some more units. So\nthere are now portions of both "new" and "old" that need to be hidden\n(the darker grey boxes), in addition to the original portion of "old"\nthat we hid (light grey box).'}),"\n",(0,r.jsx)(n.p,{children:'As before, we might have multiple versions of "newer" stacked on top\nof the same "new", and in general these DB stacks form a tree. All the\nintermediate nodes of the tree are usable simultaneously: no data is\nbeing modified, only shared and viewed differently depending on which\nnode we choose as the top of our stack.'}),"\n",(0,r.jsx)(n.p,{children:"One interesting aspect that arises when we consider how to track\nownership of facts in this model is fact dependencies across the\nboundaries between DBs. For instance, suppose we have"}),"\n",(0,r.jsx)(n.p,{children:(0,r.jsx)(n.img,{alt:"Stacked dependency",src:t(26338).A+"",width:"612",he
1ight:"323"})}),"\n",(0,r.jsx)(n.p,{children:'If we hide B, then considering the ownership data for "old" alone\nwould tell us that y is invisible. But we must make it visible,\nbecause x depends on it. So when computing the ownership data for\n"new", we have to transitively propagate ownership to facts in the\nbase DB(s), store that in "new", and consult it when deciding which\nfacts in "old" are visible.'}),"\n",(0,r.jsx)(n.h3,{id:"derived-facts-in-stacked-incremental-dbs",children:"Derived facts in stacked incremental DBs"}),"\n",(0,r.jsx)(n.p,{children:"A fact might be derived from multiple facts in different DBs in the\nstack, and we have to represent its ownership correctly. Therefore"}),"\n",(0,r.jsxs)(n.ul,{children:["\n",(0,r.jsx)(n.li,{children:"There must be a single namespace of ownership sets for the whole DB stack. That is, stacked DBs add more sets. (or else we copy ownership sets from the base DB, which doesn't seem attractive)."}),"\n",(0,r.jsx)(n.li,{children:"Since a fact may be owned multiple different ways (see previous section) we have to take this into account when computing the ownership expression for a derived fact."}),"\n"]}),"\n",(0,r.jsx)(n.p,{children:"This is the most complex diagram in the post (phew!):"}),"\n",(0,r.jsx)(n.p,{children:(0,r.jsx)(n.img,{alt:"Stacked derived",src:t(15818).A+"",width:"804",height:"446"})}),"\n",(0,r.jsx)(n.p,{children:'Here the dashed arrow means "derived from" and the solid arrow means\n"refers to".'}),"\n",(0,r.jsxs)(n.p,{children:["The fact ",(0,r.jsx)(n.strong,{children:"d"})," should be visible if both ",(0,r.jsx)(n.strong,{children:"x"})," and ",(0,r.jsx)(n.strong,{children:"y"})," are visible. The\nownership of ",(0,r.jsx)(n.strong,{children:"x"})," is ",(0,r.jsx)(n.code,{children:"{A}"})," and ",(0,r.jsx)(n.strong,{children:"y"})," is ",(0,r.jsx)(n.code,{children:"{B,C}"})," (because it is referred to from ",(0,r.jsx)(n.strong,{children:"z"}),"\nwhich has owner B), so the final owner of ",(0,r.jsx)(n.strong,{children:"d"})," is ",(0,r.jsx)(n.code,{children:"{A} && {B,C}"}),"."]}),"\n",(0,r.jsx)(n.p,{children:"Tracking all this shouldn't be too expensive, but it's tricky to get\nright!"})]})}function h(e={}){const{wrapper:n}={...(0,o.R)(),...e.components};return n?(0,r.jsx)(n,{...e,children:(0,r.jsx)(d,{...e})}):d(e)}},67555(e,n,t){t.d(n,{D4:()=>a,Lw:()=>s,gD:()=>c});t(96540);var i=t(14423),r=t(74848);let o;function a(e){return(0,r.jsx)("a",{href:o+e.file,children:e.file})}function s(e){return(0,r.jsx)("a",{href:o+e.file,children:e.children})}o=(0,i.isInternal)()?"https://www.internalfb.com/code/fbsource/fbcode/":"https://github.com/facebookincubator/Glean/tree/master/";const c=({children:e,internal:n,external:t})=>(0,i.fbContent)({internal:(0,r.jsx)("code",{children:n}),external:(0,r.jsx)("code",{children:t})})},80510(e,n,t){var i=this&&this.__awaiter||function(e,n,t,i){return new(t||(t=Promise))(function(r,o){function a(e){try{c(i.next(e))}catch(n){o(n)}}function s(e){try{c(i.throw(e))}catch(n){o(n)}}function c(e){var n;e.done?r(e.value):(n=e.value,n instanceof t?n:new t(function(e){e(n)})).then(a,s)}c((i=i.apply(e,n||[])).next())})};Object.defineProperty(n,"__esModule",{value:!0}),n.getSpecInfo=void 0;const r=t(88266);n.getSpecInfo=function(e){return i(this,void 0,void 0,function*(){return yield r.call({module:"bloks",api:"getSpecInfo",args:{styleId:e}})})}},88266(e,n){var t=this&&this.__awaiter||function(e,n,t,i){return new(t||(t=Promise))(function(r,o){function a(e){try{c(i.next(e))}catch(n){o(n)}}function s(e){try{c(i.throw(e))}catch(n){o(n)}}function c(e){var n;e.done?r(e.value):(n=e.value,n instanceof t?n:new t(function(e){e(n)})).then(a,s)}c((i=i.apply(e,n||[])).next())})};Object.defineProperty(n,"__esModule",{value:!0}),n.call=void 0;let i=!1,r=0;const o={};n.call=function(e){return t(this,void 0,void 0,function*(){if("staticdocs.thefacebook.com"!==window.location.hostname&&"localhost"!==window.location.hostname)return Promise.reject(new Error("Not running on static docs"));i||(i=!0,window.addEventListener("message",e=>{if("static-docs-bridge-response"!==e.data.event)return;const n=e.data.id;n in o||console.error(`Recieved response for id: ${n} with no matching receiver`),"response"in e.data?o[n].resolve(e.data.response):o[n].reject(new Error(e.data.error)),delete o[n]}));const n=r++,t=new Promise((e,t)=>
1{o[n]={resolve:e,reject:t}}),a={event:"static-docs-bridge-call",id:n,module:e.module,api:e.api,args:e.args},s="localhost"===window.location.hostname?"*":"https://www.internalfb.com";return window.parent.postMessage(a,s),t})}},70680(e,n,t){var i=this&&this.__awaiter||function(e,n,t,i){return new(t||(t=Promise))(function(r,o){function a(e){try{c(i.next(e))}catch(n){o(n)}}function s(e){try{c(i.throw(e))}catch(n){o(n)}}function c(e){var n;e.done?r(e.value):(n=e.value,n instanceof t?n:new t(function(e){e(n)})).then(a,s)}c((i=i.apply(e,n||[])).next())})};Object.defineProperty(n,"__esModule",{value:!0}),n.reportFeatureUsage=n.reportContentCopied=void 0;const r=t(88266);n.reportContentCopied=function(e){return i(this,void 0,void 0,function*(){const{textContent:n}=e;try{yield r.call({module:"feedback",api:"reportContentCopied",args:{textContent:n}})}catch(t){}})},n.reportFeatureUsage=function(e){return i(this,void 0,void 0,function*(){const{featureName:n,id:t}=e;console.log("used feature");try{yield r.call({module:"feedback",api:"reportFeatureUsage",args:{featureName:n,id:t}})}catch(i){}})}},14423(e,n,t){var i=this&&this.__createBinding||(Object.create?function(e,n,t,i){void 0===i&&(i=t),Object.defineProperty(e,i,{enumerable:!0,get:function(){return n[t]}})}:function(e,n,t,i){void 0===i&&(i=t),e[i]=n[t]}),r=this&&this.__setModuleDefault||(Object.create?function(e,n){Object.defineProperty(e,"default",{enumerable:!0,value:n})}:function(e,n){e.default=n}),o=this&&this.__importStar||function(e){if(e&&e.__esModule)return e;var n={};if(null!=e)for(var t in e)"default"!==t&&Object.prototype.hasOwnProperty.call(e,t)&&i(n,e,t);return r(n,e),n};Object.defineProperty(n,"__esModule",{value:!0}),n.OssOnly=n.FbInternalOnly=n.getEphemeralDiffNumber=n.hasEphemeralDiffNumber=n.isInternal=n.validateFbContentArgs=n.fbInternalOnly=n.fbContent=n.inpageeditor=n.feedback=n.uidocs=n.bloks=void 0,n.bloks=o(t(80510)),n.uidocs=o(t(3730)),n.feedback=o(t(70680)),n.inpageeditor=o(t(45458));const a=["internal","external"];function s(e){return l(e),d()?"internal"in e?c(e.internal):[]:"external"in e?c(e.external):[]}function c(e){return"function"==typeof e?e():e}function l(e){if("object"!=typeof e)throw new Error(`fbContent() args must be an object containing keys: ${a}. Instead got ${e}`);if(!Object.keys(e).find(e=>a.find(n=>n===e)))throw new Error(`No valid args found in ${JSON.stringify(e)}. Accepted keys: ${a}`);const n=Object.keys(e).filter(e=>!a.find(n=>n===e));if(n.length>0)throw new Error(`Unexpected keys ${n} found in fbContent() args. Accepted keys: ${a}`)}function d(){try{return Boolean(!1)}catch(e){return console.log("process.env.FB_INTERNAL couldn't be read, maybe you forgot to add the required webpack EnvironmentPlugin config?",e),!1}}function h(){try{return null}catch(e){return console.log("process.env.PHABRICATOR_DIFF_NUMBER couldn't be read, maybe you forgot to add the required webpack EnvironmentPlugin config?",e),null}}n.fbContent=s,n.fbInternalOnly=function(e){return s({internal:e})},n.validateFbContentArgs=l,n.isInternal=d,n.hasEphemeralDiffNumber=function(){return Boolean(h())},n.getEphemeralDiffNumber=h,n.FbInternalOnly=function(e){return d()?e.children:null},n.OssOnly=function(e){return d()?null:e.children}},45458(e,n,t){var i=this&&this.__awaiter||function(e,n,t,i){return new(t||(t=Promise))(function(r,o){function a(e){try{c(i.next(e))}catch(n){o(n)}}function s(e){try{c(i.throw(e))}catch(n){o(n)}}function c(e){var n;e.done?r(e.value):(n=e.value,n instanceof t?n:new t(function(e){e(n)})).then(a,s)}c((i=i.apply(e,n||[])).next())})};Object.defineProperty(n,"__esModule",{value:!0}),n.submitDiff=void 0;const r=t(88266);n.submitDiff=function(e){return i(this,void 0,void 0,function*(){const{file_path:n,new_content:t,project_name:i,diff_number:o}=e;try{return yield r.call({module:"inpageeditor",api:"createPhabricatorDiffApi",args:{file_path:n,new_content:t,project_name:i,diff_number:o}})}catch(a){throw new Error(`Error occurred while trying to submit diff. Stack trace: ${a}`)}})}},3730(e,n,t){var i=this&&this.__awaiter||function(e,n,t,i){return new(t||(t=Promise))(function(r,o){function a(e){try{c(i.next(e))}catch(n){o(n)}}function s(e){try{c(i.throw(e))}catch(n){o(n)}}function c(e){var n;e.done?r(e.value):(n=e.value,n instanceof t?n:new t(function(e){e(n)})).then(a,s)}c((i=i.apply(e,n||[])).next())})};Object.defineProperty(n,"__esModule",{value:!0}),n.getApi=n.docsets=void 0;const r=t(88266);n.docsets={BLOKS_CORE:"887372105406659"},n.getApi=function(e){return i(this,void 0,void 0,function*(){const{name:n,framework:t,docset:i}=e;return yield r.call({module:"uidocs",api:"getApi",args:{name:n,framework:t,docset:i}})})}},22642(e,n,t){t.d(n,{A:()=>i});const i=t.p+"assets/images/derived-e46b2a6e3925959133e37b75619495a6.jpg"},55787(e,n,t){t.d(n,{A:()=>i});const i=t.p+"assets/images/factdep1-59c2c6ff2bb70cadfffaa8ed1c3c725c.jpg"},88092(e,n,t){t.d(n,{A:()=>i});const i=t.p+"assets/images/factdep2-ee89a8cde4e367c8d2e99fde1b9553a0.jpg"},94085(e,n,t){t.d(n,{A:()=>i});const i=t.p+"assets/images/stack-f1841961d75a1c2d4d1338f3d18f0179.jpg"},26338(e,n,t){t.d(n,{A:()=>i});const i=t.p+"assets/images/stacked-dependency-37bdf977436a2c7f69570eff127cb537.jpg"},15818(e,n,t){t.d(n,{A:()=>i});const i=t.p+"assets/images/stacked-derived-fc2ba3ed856040135fc526e8a7ff6391.jpg"},32313(e,n,t){t.d(n,{A:()=>i});const i=t.p+"assets/images/stacked-incremental-a591470e7df3a05595e75f76fad1a7ac.jpg"},28453(e,n,t){t.d(n,{R:()=>a,x:()=>s});var i=t(96540);const r={},o=i.createContext(r);function a(e){const n=i.useContext(o);return i.useMemo(function(){return"function"==typeof e?e(n):{...n,...e}},[n,e])}function s(e){let n;return n=e.disableParentContext?"function"==typeof e.components?e.components(r):e.components||r:a(e.components),i.createElement(o.Provider,{value:n},e.children)}},30554(e){e.exports=JSON.parse('{"permalink":"/blog/incremental","source":"@site/blog/2022-12-01-incremental.md","title":"Incremental indexing with Glean","description":"This post describes how Glean supp
1orts incremental indexing of","date":"2022-12-01T00:00:00.000Z","tags":[{"inline":true,"label":"glean","permalink":"/blog/tags/glean"},{"inline":true,"label":"incremental","permalink":"/blog/tags/incremental"}],"readingTime":7.23,"hasTruncateMarker":false,"authors":[{"name":"Simon Marlow","title":"Simon Marlow","url":"https://simonmar.github.io/","imageURL":"http://simonmar.github.io/images/simonmarlow.jpg","key":null,"page":null}],"frontMatter":{"slug":"incremental","title":"Incremental indexing with Glean","author":"Simon Marlow","author_title":"Simon Marlow","author_url":"https://simonmar.github.io/","author_image_url":"http://simonmar.github.io/images/simonmarlow.jpg","tags":["glean","incremental"]},"unlisted":false,"prevItem":{"title":"Glean Release 0.2","permalink":"/blog/release-0-2"}}')}}]);
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.