PageSourceSearch

https://ampersandtarski.github.io/assets/js/3118fcc4.a84a8bf0.js

js ampersandtarski.github.io collected 2026-10-03 08:39:32 UTC 15,928 bytes, 1 lines download raw bytes

1"use strict";(self.webpackChunkampersand_docs=self.webpackChunkampersand_docs||[]).push([[9297],{3905:(e,t,n)=>{n.d(t,{Zo:()=>d,kt:()=>h});var a=n(7294);function r(e,t,n){return t in e?Object.defineProperty(e,t,{value:n,enumerable:!0,configurable:!0,writable:!0}):e[t]=n,e}function i(e,t){var n=Object.keys(e);if(Object.getOwnPropertySymbols){var a=Object.getOwnPropertySymbols(e);t&&(a=a.filter((function(t){return Object.getOwnPropertyDescriptor(e,t).enumerable}))),n.push.apply(n,a)}return n}function o(e){for(var t=1;t<arguments.length;t++){var n=null!=arguments[t]?arguments[t]:{};t%2?i(Object(n),!0).forEach((function(t){r(e,t,n[t])})):Object.getOwnPropertyDescriptors?Object.defineProperties(e,Object.getOwnPropertyDescriptors(n)):i(Object(n)).forEach((function(t){Object.defineProperty(e,t,Object.getOwnPropertyDescriptor(n,t))}))}return e}function l(e,t){if(null==e)return{};var n,a,r=function(e,t){if(null==e)return{};var n,a,r={},i=Object.keys(e);for(a=0;a<i.length;a++)n=i[a],t.indexOf(n)>=0||(r[n]=e[n]);return r}(e,t);if(Object.getOwnPropertySymbols){var i=Object.getOwnPropertySymbols(e);for(a=0;a<i.length;a++)n=i[a],t.indexOf(n)>=0||Object.prototype.propertyIsEnumerable.call(e,n)&&(r[n]=e[n])}return r}var s=a.createContext({}),p=function(e){var t=a.useContext(s),n=t;return e&&(n="function"==typeof e?e(t):o(o({},t),e)),n},d=function(e){var t=p(e.components);return a.createElement(s.Provider,{value:t},e.children)},m={inlineCode:"code",wrapper:function(e){var t=e.children;return a.createElement(a.Fragment,{},t)}},c=a.forwardRef((function(e,t){var n=e.components,r=e.mdxType,i=e.originalType,s=e.parentName,d=l(e,["components","mdxType","originalType","parentName"]),c=p(n),h=r,u=c["".concat(s,".").concat(h)]||c[h]||m[h]||i;return n?a.createElement(u,o(o({ref:t},d),{},{components:n})):a.createElement(u,o({ref:t},d))}));function h(e,t){var n=arguments,r=t&&t.mdxType;if("string"==typeof e||r){var i=n.length,o=new Array(i);o[0]=c;var l={};for(var s in t)hasOwnProperty.call(t,s)&&(l[s]=t[s]);l.originalType=e,l.mdxType="string"==typeof e?e:r,o[1]=l;for(var p=2;p<i;p++)o[p]=n[p];return a.createElement.apply(null,o)}return a.createElement.apply(null,n)}c.displayName="MDXCreateElement"},2508:(e,t,n)=>{n.r(t),n.d(t,{assets:()=>s,contentTitle:()=>o,default:()=>m,frontMatter:()=>i,metadata:()=>l,toc:()=>p});var a=n(7462),r=(n(7294),n(3905));const i={},o="Full-text search module",l={unversionedId:"prototype/reference-material/search-module",id:"prototype/reference-material/search-module",title:"Full-text search module",description:"The search module lets a user search across all stored data of a running prototype from the",source:"@site/docs/prototype/reference-material/search-module.md",sourceDirName:"prototype/reference-material",slug:"/prototype/reference-material/search-module",permalink:"/prototype/reference-material/search-module",draft:!1,tags:[],version:"current",frontMatter:{},sidebar:"mainSidebar",previous:{title:"PROPBUTTON Template",permalink:"/prototype/reference-material/propbutton-template"},next:{title:"Transactional Interfaces",permalink:"/prototype/reference-material/transactional-interfaces"}},s={},p=[{value:"User-facing behaviour",id:"user-facing-behaviour",level:2},{value:"Design",id:"design",level:2},{value:"Searching is column-based and TType-aware",id:"searching-is-column-based-and-ttype-aware",level:3},{value:"A match belongs to the entity on the other side",id:"a-match-belongs-to-the-entity-on-the-other-side",level:3},{value:"Results carry their interfaces",id:"results-carry-their-interfaces",level:3},{value:"Limits",id:"limits",level:3},{value:"Implementation",id:"implementation",level:2},{value:"Extending the search",id:"extending-the-search",level:2}],d={toc:p};
1function m(e){let{components:t,...n}=e;return(0,r.kt)("wrapper",(0,a.Z)({},d,n,{components:t,mdxType:"MDXLayout"}),(0,r.kt)("h1",{id:"full-text-search-module"},"Full-text search module"),(0,r.kt)("p",null,"The search module lets a user search across ",(0,r.kt)("strong",{parentName:"p"},"all stored data")," of a running prototype from the\nhome screen, and open each matching atom in any interface that can display it. It is a fixed\nframework feature (not generated), wired into the application's home screen."),(0,r.kt)("p",null,"This page is written for contributors who maintain or extend the search feature. It explains the\ndesign, points to every file involved, and shows where to change behaviour. It builds on the\nbackend runtime described in ",(0,r.kt)("a",{parentName:"p",href:"/prototype/reference-material/prototype-framework"},"The Prototype Framework")," (TTypes, concepts,\nrelations) and the Angular structure in\n",(0,r.kt)("a",{parentName:"p",href:"/prototype/reference-material/frontend-component-internals"},"Frontend component internals"),"."),(0,r.kt)("h2",{id:"user-facing-behaviour"},"User-facing behaviour"),(0,r.kt)("ul",null,(0,r.kt)("li",{parentName:"ul"},"A search box on the home screen searches every stored value as you type (debounced)."),(0,r.kt)("li",{parentName:"ul"},"Results are the ",(0,r.kt)("strong",{parentName:"li"},"atoms")," (entities) whose data contains the term, grouped by concept."),(0,r.kt)("li",{parentName:"ul"},"For each result the user can open the atom in any interface that can display it; the available\ninterfaces are presented as buttons.")),(0,r.kt)("h2",{id:"design"},"Design"),(0,r.kt)("h3",{id:"searching-is-column-based-and-ttype-aware"},"Searching is column-based and TType-aware"),(0,r.kt)("p",null,"Every relation knows the table and the two columns (",(0,r.kt)("inlineCode",{parentName:"p"},"srcCol"),", ",(0,r.kt)("inlineCode",{parentName:"p"},"tgtCol"),") in which it is\nadministrated, together with the concept \u2014 and thus the ",(0,r.kt)("a",{parentName:"p",href:"/prototype/reference-material/prototype-framework"},"TType")," \u2014 of\neach side. Iterating over ",(0,r.kt)("strong",{parentName:"p"},"relations")," therefore yields every stored column uniformly, regardless\nof whether the Ampersand compiler stored a (univalent) relation in a concept's ",(0,r.kt)("em",{parentName:"p"},"broad")," table or in\nits own ",(0,r.kt)("em",{parentName:"p"},"binary")," table. This is why the module iterates relations rather than reverse-engineering\nthe SQL schema."),(0,r.kt)("p",null,"A column is only queried when the search term is a ",(0,r.kt)("strong",{parentName:"p"},"plausible value")," for that column's TType\n(requirement: search is TType-aware without telling or asking the user):"),(0,r.kt)("table",null,(0,r.kt)("thead",{parentName:"table"},(0,r.kt)("tr",{parentName:"thead"},(0,r.kt)("th",{parentName:"tr",align:null},"TType"),(0,r.kt)("th",{parentName:"tr",align:null},"Searched when the term\u2026"))),(0,r.kt)("tbody",{parentName:"table"},(0,r.kt)("tr",{parentName:"tbody"},(0,r.kt)("td",{parentName:"tr",align:null},(0,r.kt)("inlineCode",{parentName:"td"},"ALPHANUMERIC"),", ",(0,r.kt)("inlineCode",{parentName:"td"},"BIGALPHANUMERIC"),", ",(0,r.kt)("inlineCode",{parentName:"td"},"HUGEALPHANUMERIC")),(0,r.kt)("td",{parentName:"tr",align:null},"always")),(0,r.kt)("tr",{parentName:"tbody"},(0,r.kt)("td",{parentName:"tr",align:null},(0,r.kt)("inlineCode",{parentName:"td"},"INTEGER")),(0,r.kt)("td",{parentName:"tr",align:null},"consists of digits only")),(0,r.kt)("tr",{parentName:"tbody"},(0,r.kt)("td",{parentName:"tr",align:null},(0,r.kt)("inlineCode",{parentName:"td"},"FLOAT")),(0,r.kt)("td",{parentName:"tr",align:null},"is a number (digits, optional decimal separator)")),(0,r.kt)("tr",{parentName:"tbody"},(0,r.kt)("td",{parentName:"tr",align:null},(0,r.kt)("inlineCode",{parentName:"td"},"DATE"),", ",(0,r.kt)("inlineCode",{parentName:"td"},"DATETIME")),(0,r.kt)("td",{parentName:"tr",align:null},"looks like (part of) a date/time")),(0,r.kt)("tr",{parentName:"tbody"},(0,r.kt)("td",{parentName:"tr",align:null},(0,r.kt)("inlineCode",{parentName:"td"},"OBJECT"),", ",(0,r.kt)("inlineCode",{parentName:"td"},"PASSWORD"),", ",(0,r.kt)("inlineCode",{parentName:"td"},"BINARY*"),", ",(0,r.kt)("inlineCode",{parentName:"td"},"BOOLEAN"),", ",(0,r.kt)("inlineCode",{parentName:"td"},"TYPEOFONE")),(0,r.kt)("td",{parentName:"tr",align:null},"never")))),(0,r.kt)("p",null,"So the term ",(0,r.kt)("inlineCode",{parentName:"p"},"983")," is searched in ",(0,r.kt)("inlineCode",{parentName:"p"},"INTEGER")," columns ",(0,r.kt)("strong",{parentName:"p"},"and")," in alphanumeric columns; the term\n",(0,r.kt)("inlineCode",{parentName:"p"},"Solanum")," only in alphanumeric columns. ",(0,r.kt)("inlineCode",{parentName:"p"},"OBJECT")," atoms are never searched, because an object\nidentifier carries no meaningful content beyond its identity (only equality matters)."),(0,r.kt)("p",null,"Matching is a case-insensitive ",(0,r.kt)("inlineCode",{parentName:"p"},"LIKE '%term%'")," substring search. The term is escaped twice: LIKE\nmetacharacters (",(0,r.kt)("inlineCode",{parentName:"p"},"% _ \\"),") are neutralised so the user cannot inject wildcards, and the result is\nthen escaped as a SQL string literal."),(0,r.kt)("h3",{id:"a-match-belongs-to-the-entity-on-the-other
1-side"},"A match belongs to the entity on the other side"),(0,r.kt)("p",null,"A scalar value (a name, a code, a date) describes the ",(0,r.kt)("strong",{parentName:"p"},"entity")," it is attached to. When a value\ncolumn matches, the result is the ",(0,r.kt)("inlineCode",{parentName:"p"},"OBJECT")," atom on the other side of the relation (the ",(0,r.kt)("inlineCode",{parentName:"p"},"Land"),", the\n",(0,r.kt)("inlineCode",{parentName:"p"},"Product"),", the ",(0,r.kt)("inlineCode",{parentName:"p"},"Eis"),", \u2026), not the scalar value itself. Relations with two ",(0,r.kt)("inlineCode",{parentName:"p"},"OBJECT")," sides or two\nscalar sides have no single entity to navigate to and are skipped."),(0,r.kt)("h3",{id:"results-carry-their-interfaces"},"Results carry their interfaces"),(0,r.kt)("p",null,"Each result atom is enriched with the interfaces in which it can be opened, via\n",(0,r.kt)("inlineCode",{parentName:"p"},"AmpersandApp::getInterfacesToReadConcept()")," \u2014 the same mechanism that powers the ",(0,r.kt)("inlineCode",{parentName:"p"},"_ifcs_"),"\nnavigation elsewhere in the UI. Results are returned in the ",(0,r.kt)("inlineCode",{parentName:"p"},"ObjectBase")," shape (",(0,r.kt)("inlineCode",{parentName:"p"},"_id_"),", ",(0,r.kt)("inlineCode",{parentName:"p"},"_label_"),",\n",(0,r.kt)("inlineCode",{parentName:"p"},"_ifcs_"),") so the frontend reuses the application's interface route map for navigation."),(0,r.kt)("h3",{id:"limits"},"Limits"),(0,r.kt)("p",null,"To stay responsive on large datasets the search caps rows per column and the total number of\ndistinct result atoms; when the cap is hit the response sets ",(0,r.kt)("inlineCode",{parentName:"p"},"truncated: true")," and the UI asks the\nuser to refine the term. See the constants at the top of ",(0,r.kt)("inlineCode",{parentName:"p"},"SearchController"),"."),(0,r.kt)("h2",{id:"implementation"},"Implementation"),(0,r.kt)("table",null,(0,r.kt)("thead",{parentName:"table"},(0,r.kt)("tr",{parentName:"thead"},(0,r.kt)("th",{parentName:"tr",align:null},"Part"),(0,r.kt)("th",{parentName:"tr",align:null},"Location"))),(0,r.kt)("tbody",{parentName:"table"},(0,r.kt)("tr",{parentName:"tbody"},(0,r.kt)("td",{parentName:"tr",align:null},"Backend endpoint"),(0,r.kt)("td",{parentName:"tr",align:null},(0,r.kt)("inlineCode",{parentName:"td"}
1,"backend/src/Ampersand/Controller/SearchController.php"))),(0,r.kt)("tr",{parentName:"tbody"},(0,r.kt)("td",{parentName:"tr",align:null},"Route registration"),(0,r.kt)("td",{parentName:"tr",align:null},(0,r.kt)("inlineCode",{parentName:"td"},"backend/bootstrap/api/search.php")," (",(0,r.kt)("inlineCode",{parentName:"td"},"GET /api/v1/search?q=<term>"),")")),(0,r.kt)("tr",{parentName:"tbody"},(0,r.kt)("td",{parentName:"tr",align:null},"Frontend feature"),(0,r.kt)("td",{parentName:"tr",align:null},(0,r.kt)("inlineCode",{parentName:"td"},"frontend/src/app/search/")," (",(0,r.kt)("inlineCode",{parentName:"td"},"SearchComponent"),", ",(0,r.kt)("inlineCode",{parentName:"td"},"SearchService"),", ",(0,r.kt)("inlineCode",{parentName:"td"},"search.model.ts"),")")),(0,r.kt)("tr",{parentName:"tbody"},(0,r.kt)("td",{parentName:"tr",align:null},"Home-screen wiring"),(0,r.kt)("td",{parentName:"tr",align:null},(0,r.kt)("inlineCode",{parentName:"td"},"SearchComponent")," imported in ",(0,r.kt)("inlineCode",{parentName:"td"},"frontend/src/app/layout/app.layout.module.ts"),", used in ",(0,r.kt)("inlineCode",{parentName:"td"},"home.component.html"))))),(0,r.kt)("p",null,"The endpoint requires a normal session; the interfaces offered per result respect the session's\nactive roles (an atom with no accessible interface is shown but not navigable)."),(0,r.kt)("h2",{id:"extending-the-search"},"Extending the search"),(0,r.kt)("p",null,"Common changes and where to make them, all in ",(0,r.kt)("inlineCode",{parentName:"p"},"SearchController"),":"),(0,r.kt)("ul",null,(0,r.kt)("li",{parentName:"ul"},(0,r.kt)("strong",{parentName:"li"},"Make a TType (un)searchable")," \u2014 edit ",(0,r.kt)("inlineCode",{parentName:"li"},"ttypeMatchesTerm()"),'. It is the single place that maps a\nTType and the term to "search this column or not". Adding a case here is all that is needed; the\ncolumn enumeration picks it up automatically.'),(0,r.kt)("li",{parentName:"ul"},(0,r.kt)("strong",{parentName:"li"},"Change which entity a match belongs to")," \u2014 edit ",(0,r.kt)("inlineCode",{parentName:"li"},"scalarColumnToSearch()"),". It classifies a\nrelation's two sides into the searched (scalar) column and the owning (object) column. Today it\nskips relations with two object sides or two scalar sides."),(0,r.kt)("li",{parentName:"ul"},(0,r.kt)("strong",{parentName:"li"},"Tune responsiveness or result size")," \u2014 edit the ",(0,r.kt)("inlineCode",{parentName:"li"},"*_LIMIT")," / ",(0,r.kt)("inlineCode",{parentName:"li"},"MAX_*")," constants at the top of the\nclass. They cap rows per column, total result atoms, and matched-field hints."),(0,r.kt)("li",{parentName:"ul"},(0,r.kt)("strong",{parentName:"li"},"Change the matching")," (e.g. word boundaries, prefix-only) \u2014 edit ",(0,r.kt)("inlineCode",{parentName:"li"},"buildLikePattern()")," and\n",(0,r.kt)("inlineCode",{parentName:"li"},"buildColumnQuery()"),". Keep the two-layer escaping intact: LIKE metacharacters first, then the SQL\nstring literal."),(0,r.kt)("li",{parentName:"ul"},(0,r.kt)("strong",{parentName:"li"},"Change the result shape")," \u2014 edit ",(0,r.kt)("inlineCode",{parentName:"li"},"newResult()")," (one result atom) and the JSON assembled in\n",(0,r.kt)("inlineCode",{parentName:"li"},"search()"),". The frontend types live in ",(0,r.kt)("inlineCode",{parentName:"li"},"frontend/src/app/search/search.model.ts"),"; keep the\n",(0,r.kt)("inlineCode",{parentName:"li"},"ObjectBase")," fields (",(0,r.kt)("inlineCode",{parentName:"li"},"_id_"),", ",(0,r.kt)("inlineCode",{parentName:"li"},"_label_"),", ",(0,r.kt)("inlineCode",{parentName:"li"},"_ifcs_"),") so navigation keeps working."),(0,r.kt)("li",{parentName:"ul"},(0,r.kt)("strong",{parentName:"li"},"Change the presentation")," \u2014 the result list, grouping and interface buttons are in\n",(0,r.kt)("inlineCode",{parentName:"li"},"frontend/src/app/search/search.component.{ts,html,scss}"),". Navigation reuses the generated\ninterface route map (",(0,r.kt)("inlineCode",{parentName:"li"},"INTERFACE_ROUTE_MAPPING_TOKEN"),"); do not hard-code routes.")),(0,r.kt)("p",null,"A change to the backend ",(0,r.kt)("inlineCode",{parentName:"p"},"src/")," or ",(0,r.kt)("inlineCode",{parentName:"p"},"bootstrap/")," must be rebuilt into the base image before a\ngenerated prototype sees it; see ",(0,r.kt)("a",{parentName:"p",href:"/prototype/guides/updating-and-releasing"},"Updating and Releasing the Prototype Framework"),"."))}m.isMDXComponent=!0}}]);

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.