Wiki › Component

Question engine (ask.chaipat.cc)

Component

The question engine is the part of ask.chaipat.cc that decides how a typed question is answered. It lives in src/engine.js and the modules around it, is plain JavaScript, and runs in the visitor's browser; the same code runs in the evaluation scripts, so the questions in tests/search-eval.json are answered exactly as the page answers them. This article describes its design the way a short paper would: the problem, the method with its algorithms in pseudocode, how it was measured, and what it does not do.

Abstract

A personal portfolio is a small corpus with a strong prior: visitors want to see what one person built, and every claim should be checkable. This article describes the question engine behind this site: a layered, deterministic pipeline over a typed knowledge graph (7,289 triples, 447 typed entities) and a wiki of 169 articles. Its layers are entity linking, ordered intent rules, graph and BM25 retrieval fused by weighted reciprocal rank fusion, an answer composer that cites only what it read, a route-based confidence, a zero-result pivot for technologies the graph lacks, and follow-up questions that are validated before they are offered. A guarded fallback to a hosted language model exists in the code but is switched off. The engine passes a 156-case regression suite; on 59 held-out free-form questions it does the right kind of thing for 48 (81%). It is also honest about where it does not: four unrelated questions still receive an answer taken from an unrelated passage, and the fallback that would address them has never been evaluated against a live model.

Status in one line: everything in sections 4 to 7 is on and tested; section 8 is implemented, tested only against a simulated model, and disabled.

1 Introduction

Visitors type free-form questions, from a bare skill name with the word "projects" to a how or why question about one piece of work. The site is small, so the answer to most of them exists, but in different forms: as a typed fact in the graph, as a paragraph in a wiki article, or nowhere. The engine has to choose, and say so when it cannot.

Five principles shaped it, in the order they were applied when two of them disagreed.

P1 The graph is the source of truth
facts (what he built, with what, when) come from the graph; the wiki explains; Wikipedia only defines general things and never answers a question about him.
P2 Deterministic before generative
an answer that can be computed from the graph and the wiki is computed, not generated, so the same question gives the same answer and every sentence has a source.
P3 Abstain rather than invent
a wrong answer on a portfolio is worse than none. Where the engine is unsure it must be able to say so.
P4 Key work first
results are ordered so that his best work is seen first, from an editorial ranking, not from popularity.
P5 Nothing private
no phone number, no birthday, no stored IP address, and no link the graph does not hold.

This follows how large search systems are built, but not how they are sized. Google describes Search as running on "hundreds of algorithms and machine learning models", each with "a specialized role" that triggers "at different times and in distinct combinations" [1], and its ranking guide lists separate systems for separate jobs [2]. The engine copies the shape (several small layers, each testable alone, none a single algorithm) and nothing else: there is no click data to learn from at this traffic, and every layer here can be explained in a paragraph.

Lexical ranking and fusion. BM25 [3] ranks wiki passages. Ranked lists from different sources (graph neighbours, passages) are merged with reciprocal rank fusion [4], which needs no score calibration between sources.

Knowledge graphs and the semantic web. The data is RDF in Turtle, with an OWL 2 ontology [8] and RDFS and OWL reasoning, queried with SPARQL [7]; [5] and [6] give the background. The graph's value here is that its facts are typed: "uses Python" is an edge to one entity, not a mention of a word.

Retrieval-augmented generation. Retrieval-augmented generation [9] grounds a language model's answer in retrieved text; graph-based variants build a graph from the text first [10]. A systematic comparison finds that graph structure is not universally beneficial and depends on the task [11]. The engine uses graph matches where the question is exact (a count, a list of the works that use a skill) and passage retrieval where it is prose, which is the same split, made by hand because the graph here is curated and not extracted. Language models also state falsehoods fluently [12], which is why section 8 is built around checking an answer, not trusting it.

Spelling. A word the site does not know is corrected by edit distance with transpositions [13], [14].

Evaluation. Held-out test sets and ranked-retrieval measures follow standard practice [15].

3 Data

SourceSize
Knowledge graph7,289 triples: 6,126 stated, 1,163 inferred by the reasoner; 95 predicates; ontology version 1.14.0
Typed entities447, among them 146 projects, 116 skills, 51 techniques, 19 components of this site, 3 papers
Wiki169 articles, split into passages: the lead, and one per paragraph under each heading
Rankingevery work has a tier: 1 key work, 2 project, 3 side work; key work has an explicit order

The reasoner adds what the ontology implies before the engine runs, so every step below sees stated and inferred facts together. Code blocks and figures are not indexed, so the pseudocode in this article cannot be retrieved as an answer.

4 Architecture

Figure 1. The pipeline. Blue boxes are the main path, green the authored and graph routes, the dashed box the switched-off model fallback.
Figure 1. The pipeline. Blue boxes are the main path, green the authored and graph routes, the dashed box the switched-off model fallback.

The engine is one function, plan(question), that returns a plan: what the question was understood to be, the ranked results, the answer source, the resources to offer, a composed answer with its sources, and a confidence. The page, the server function and the evaluation scripts all call it, so they cannot drift apart. It is split into modules, each with one job.

ModuleJob
src/engine.jsentity linking, retrieval, fusion, the planning order, spelling correction
src/intents.jswhat each kind of question implies: task, operation, whether it is about one thing
src/lexicon.jsthe site's vocabulary: aliases, ambiguous names, absent technologies
src/dates.js, src/compare.jsdates in a question; comparisons
src/search.jsBM25 over wiki passages
src/answer.js, src/resources.jsthe answer composer; the entity catalog and resource selection
src/confidence.js, src/pivot.js, src/next.jsconfidence, the zero-result pivot, follow-up questions
api/ask.js, lib/llm.js, lib/verify.jsthe switched-off model fallback and its verifier

5 Method

5.1 Entity linking

The question is matched against the names of every typed entity: labels, short labels, alternative labels, a small alias table, and the addresses of verified resources. Longest names win, so "knowledge graph explorer" links one project rather than two skills. Names that are also ordinary words ("Go", "Make", "Rust") link only when written with an initial upper-case letter or after a cue such as "in" or "with". A name shared by two entities is reported as ambiguous and never silently resolved.

Algorithm 1. Entity linking.

pseudocode
LINK(q):
    taken ← a flag per character of q;  linked ← [];  ambiguous ← [];  unknown_urls ← []
    for each URL-like token u in q:                       # protect addresses first
        if a longer known name containing u also occurs in q: continue
        mark u taken
        owners ← entities whose resource address is u
        if |owners| = 1: linked.add(owners[0])
        else if |owners| > 1: ambiguous.add(u, owners)
        else: unknown_urls.add(u)
    for each name n in NAMES, longest first:
        for each word-bounded occurrence m of n in q:
            if m overlaps a taken character: continue
            if n is an ordinary word (Go, Make, Rust…) and m is not capitalised as n
               and the text before m has no cue word and q ≠ n: continue
            mark m taken
            owners ← entities called n          # graph names win over alias-table names
            if |owners| > 1: ambiguous.add(m, owners)
            else if owners[0] not in linked: linked.add(owners[0])
    rest ← q without its taken characters
    return linked in order of position, rest, ambiguous, unknown_urls

Linking is memoised per question, because planning, intent, interpretation and spelling all ask for it.

5.2 Intent and interpretation

The kind of question is decided by ordered rules; the first that matches wins, and the order is the design: an explicit request to open something outranks a topic, a question about him outranks a keyword.

Algorithm 2. Intent, then interpretation.

pseudocode
INTENT(q, linked):
    cue ← the part of q that names no entity
    if cue starts with open / go to / visit / launch / try / play / use:     return navigate
    if cue mentions source / repository / github (and not how, why, explain): return source
    if the only other words are filler (tell, about, show, me…) and a work is named: return entity
    if q matches an authored question about him or the site:                  return about
    if cue mentions available / contact / hire / phone:                       return contact
    if q asks what is new, or recently:                                       return recent
    if q mentions Japan and names nothing:                                    return japan
    if q asks to rebuild from scratch:                                        return scratch
    if a work is named and the cue asks for its stack or technology:          return stack
    if cue has "how many":                                                    return fact
    if cue has when, how long, what year, since:                              return fact
    if q starts with what is / who is / define and an entity (not a work):    return define
    if q starts with which / list / show / name, or ends in "projects":       return list
    if cue has how / why / explain / architecture / algorithm:                return explain
    if something is named and nothing else is asked:                          return entity
    return search

INTERPRET(kind, q):
    operation ← count if "how many" ∈ q
                else stack if kind = stack, or kind = about with the key "stack"
                else explain / list / fact / compare as the kind says, else overview
    task      ← navigate if kind = navigate, or q is the bare name of one work
                source   if kind = source;  compare if kind = compare
                discover if operation = list, or kind = search and nothing is named
                else learn
    return (task, operation)

Separating the task (what the visitor wants to do) from the operation (what shape the answer takes) from the entity (what it is about) is what lets "open Wordplay", "how does Wordplay work" and "Wordplay source" share one entity and differ in the action offered. The mapping from kind to operation, and which kinds are authored, lives in one table (src/intents.js) that planning, interpretation and the answer composer all read.

5.3 Retrieval and fusion

Two sources propose candidates. The graph proposes works related to what was linked: for a skill, the works that use it; for a theme, the works under it; for a technique or dataset, the works the wiki's typed facts attach to it; for a project, the works related to or succeeded by it. The wiki proposes articles, ranked by BM25 over passages.

For a passage p and the set Q of stemmed query terms, with k1 = 1.2 and b = 0.75 [3]:

score(p, Q) = Σ_w idf(w) · f(w,p) · (k1 + 1) / ( f(w,p) + k1 · (1 − b + b · |p| / avg) )

idf(w) = ln( 1 + (N − n_w + 0.5) / (n_w + 0.5) )

where f(w,p) is the term's frequency in the passage, N the number of passages and n_w those containing the term. An article scores by its best passage, plus a quarter of the rest of its passage mass, plus 2.5 per query term in its title, plus 1.5 times the share of query terms it contains anywhere. Terms are lower-cased, stop words are dropped and plural and tense endings are stripped.

The candidate lists are merged with reciprocal rank fusion [4], with its usual constant of 60, weighted by the kind of question, and then scaled by the work's tier so that key work is nudged ahead of side work without overriding relevance.

Algorithm 3. Fusion.

pseudocode
FUSE(lists, weights):                       # lists: linked L, graph G, wiki W
    for each candidate d:
        score[d] ← Σ over lists s containing d:  weights[s] / (60 + rank_s(d))
        score[d] ← score[d] × { tier 1: 1.12,  tier 2: 1.00,  tier 3: 0.92 }
    return candidates sorted by score, keeping only known entities
Kind of questionWeight of linkedWeight of graphWeight of wiki
list0.41.60.8
explain2.00.61.6
search1.51.01.4
entity3.01.00.8
define3.01.20.6
fact2.01.21.0

A list question trusts the graph; an explanation trusts the wiki; a bare name trusts the link. These weights were set by reading failures, not fitted, and they are the most arbitrary numbers in the engine.

5.4 Planning

Algorithm 4. The planning order. The first branch that applies returns the plan.

pseudocode
PLAN(q):
    q ← SHORT(q) if q is one or two words with a known meaning ("skills", "education")
    (linked, rest) ← LINK(q);  kind ← INTENT(q, linked)
    W ← BM25 hits for q with coverage ≥ 0.34 and score ≥ 0.3 × best
    G ← graph candidates of linked
    results ← FUSE([linked, G, W], weights[kind])

    if linked = ∅ and kind ∈ {search, list, explain, fact} and DETECT_ABSENT(q):   return PIVOT(q)     # section 7
    if two works, skills or places are compared:                                   return COMPARISON
    if "how many" and everything linked is a skill, theme or concept:              return COUNT
    if q names a period and kind ∉ {define, explain, contact, navigate, source, stack}:
                                                                                   return WORK_IN_PERIOD
    if q asks about a class ("which games…") and nothing is linked:                return CLASS_LIST
    if kind = about:                                                               return the authored answer
    if kind ∈ {contact, recent, japan, scratch}:                                   return the canned answer
    if kind = define and something is linked:                                      return DEFINITION
    if kind = explain:                                                             return the best passage
    if two or more skills or themes are linked:                                    return WORKS_WITH_ALL
    if the main entity is a concept (technique, dataset…) with works:              return GRAPH_ANSWER + passage
    if the main entity has a prewritten graph question:                            return it
    if W ≠ ∅:                                                                      return the top wiki passage
    if Wikipedia knows a term of q:                                                return DEFINITION
    return an empty plan

    # then, for every plan:
    interpretation ← INTERPRET(kind, q);  primary entity ← the one thing it is about, if any
    items ← results with their resources;  answer ← COMPOSE(plan);  confidence ← CONFIDENCE(plan)

5.5 Entities, tasks and resources

The shared catalog (src/resources.js) describes every typed entity, including those without a wiki article: names, aliases, type, description, relations and resources. A resource has a role. About is the local wiki address and exists for every entity. Live exists only for an explicit cj:liveUrl and source only for a cj:repository; a general cj:url may be a paper or a store listing and is never treated as a live product. External actions must be HTTP(S) addresses without credentials. The task chooses the primary action: navigation prefers live, source prefers the repository, learning and discovery prefer about; a missing role falls back to about and the others stay available beside it.

The composer (src/answer.js) writes one answer for a focused question from a single authority: the opening of the article if it has one, else the graph description, never both. It cites only what it read: graph facts, one passage of the named article, or one Wikipedia extract. Related entities are returned separately and are not citations.

5.6 Time

A question about time is turned into a range of dates (src/dates.js): relative ("the past six months", "since last year"), named ("March 2025", "Q3 2025", "early 2024") and written dates at the precision given. A work counts when it had commits on a day inside the range, from the months of work kept for each repository. During a role, work attached to the role counts whatever its dates, and the SPARQL shown to the visitor includes that branch, so the rows displayed are exactly the works counted.

6 Confidence

An answer should come with a measure of how sure the engine is, because that decides whether anything else may be asked and whether the page should hedge. The first attempt used a statistic. It failed (section 9.3). What does separate the cases is the route the answer took.

Figure 2. How a plan's confidence is decided. Only weak and none may reach a model, and none is ever asked unless enabled.
Figure 2. How a plan's confidence is decided. Only weak and none may reach a model, and none is ever asked unless enabled.

Algorithm 5. Confidence.

pseudocode
CONFIDENCE(P, support):
    if P is ambiguous:                       return exact      # the visitor chooses
    if P has an authored or graph answer:    return exact
    if P found nothing:                      return none
    if P names an entity the site knows:     return strong
    if support ≥ 0.85:                       return strong     # one article carries nearly the whole question
    return weak                                                # word overlap alone: a guess

support(q, article) = Σ idf(w) over query terms w found in the article  /  Σ idf(w) over all query terms
model_allowed(P)    = LEVEL(P) ∈ {weak, none}  and  P is not ambiguous  and  the fallback is enabled

Exact means the engine knows; strong that it was told what to look at; weak that it found words, not meaning. The threshold 0.85 is the one numeric choice here, and it is deliberately high. On the held-out set the coverage of unrelated questions and of right answers overlaps everywhere except at the very top of its range (section 9.3), and only a handful of cases sit there, so the bar is set where a wrong strong, which gets no hedge and no second look, costs more than a needless weak.

7 Zero-result pivot and follow-up questions

The pivot. A question about a technology the graph lacks used to return nothing, or a passage that merely contained the word. A small lexicon names well-known technologies the graph does not have, with the skill category each belongs to; the graph supplies everything shown. The answer says only what is true: the graph lists no such work.

Algorithm 6. Pivot.

pseudocode
DETECT_ABSENT(q):  q is not a definition ("what is X", "define X", "explain X")  and  q contains a name from ABSENT_TECH

PIVOT(q, t):                                   # t has a label and a category, e.g. (Scala, programming language)
    S ← skills with cj:category = t.category, ordered by the number of works that use them
    A ← works that use any skill of S, ordered by tier, key rank and date
    K ← the tier-1 works in A;   R ← K if K ≠ ∅ else A
    evidence_1 ← SPARQL: works using a skill labelled t.label        # returns no rows
    evidence_2 ← SPARQL: the works R, with the skill that links each
    say ← "The graph lists no <t.label> work. The <category noun> he does use most are
           S[1], S[2] and S[3] (n works each). Key work in them: R[1..4] and more."
    return plan(intent = pivot, answerFrom = graph, results = R, confidence = exact)

It never says he lacks the skill, only that the graph lists no work, and libraries he may have used without recording it are kept out of the lexicon on purpose. The pivot recognises only technologies in the lexicon: a name it has not been told about gets no pivot.

Follow-up questions. After an answer the page offers up to three questions. They come from one hop around what the answer was about, and a candidate is kept only if the engine itself answers it confidently.

Algorithm 7. Follow-ups.

pseudocode
NEXT(P):
    if CONFIDENCE(P) ∉ {exact, strong}: return []            # never build on a guess
    C ← candidates:
        pivot:               "<skill> projects" for the top skills of the category
        a work:              "How does X work?"  (if it has an article and was not just asked)
                             "<tool> projects"   (tools used by 2 to 20 works, not file names)
                             "Compare X and Y"   (related work of the same kind)  or  "Tell me about Z"
        a skill:             "Tell me about <its top work>",  "Compare X and <same-category skill with most works>"
        a theme or concept:  "Tell me about <a work under it>"
        an organisation:     "What did he do during his time at X?"
    keep the first 3 c ∈ C such that  c ≠ the question asked
        and  PLAN(c).confidence ∈ {exact, strong}  and  PLAN(c) has an answer

This is a separate call, not part of plan(), so planning stays fast and a chip's own plan never builds chips of its own.

8 The guarded model fallback (switched off)

The model fallback is a component of its own. The engine can ask a hosted language model, but only for the plans it rates weak or none, and only if LLM_ENABLED=true and a provider key are both set. It is off by default, and a key alone enables nothing, so a key left in an environment file cannot send a question anywhere. It has been run against a simulated model in tests; it has not been evaluated against a live model, and for that reason alone it is disabled. While it is off, no question leaves the server.

Figure 3. The guarded fallback. Every box is dashed because the whole path is disabled.
Figure 3. The guarded fallback. Every box is dashed because the whole path is disabled.

Algorithm 8. The fallback and its verifier.

pseudocode
ASK(P, q):                                       # only if model_allowed(P)
    E ← evidence pack: the 5 best BM25 passages for q, P's own passage, a definition if any
    repeat at most 3 rounds:
        reply ← MODEL(instructions, schema without the entity list, E, tools)
        tools: sparql(query) runs on the graph;  search_wiki(query) returns 5 passages
        every text a tool returns is added to E
    a ← reply.answer with every link the server did not supply removed
    if a is empty or says it found nothing:     return NOT_FOUND
    problems ← VERIFY(a, q, E)
    if problems = ∅ and a cites no source:       problems ← ["no source"]
    return NOT_FOUND if problems ≠ ∅ else (a, the sources it actually used)

VERIFY(a, q, E):
    problems ← ∅
    for each catalog entity name n (≥ 5 letters, not an ordinary word) in a:  if n ∉ E ∪ q: add problem
    for each number in a:  if it is a year or has two digits or more, or counts years or months, and ∉ E ∪ q: add problem
    for each proper noun, acronym or technical token t in a (not just a capitalised sentence start):
        if t ∉ E ∪ q and t is not a common word of the wiki: add problem
    if a contains a phone number or a birth date: add problem
    return problems

The checks are conservative on purpose, in the direction P3 asks for. Their limit is stated plainly: a false claim made only of ordinary lowercase words, or an invented name that opens a sentence and is not in the catalog, would pass the verifier; the source requirement and the rule that the model sees only retrieved evidence are the other defences. The verifier proves nothing about truth.

Two practical findings from the first live attempt are recorded because they shaped the design. The prompt inherited from an earlier agent was about 8,000 input tokens, almost all of it a list of every entity, which exceeded a free tier's per-minute input limit on a single request; the fallback now has its own prompt of about 3,000 tokens. And a question where the model calls a tool sends two requests, which together can exceed the same limit, so tool use costs real capacity on a small plan.

Privacy, if it is ever enabled: a question the engine cannot answer would be sent as typed to the model's provider, and nothing else: no address, identifier or history. With the default setting nothing is sent.

9 Evaluation

9.1 Setup

The search evaluation runs two suites with different jobs.

SuiteSizePurpose
Regression (tests/search-eval.json)156 casesA case is added whenever a real question goes wrong. It asserts intent, answer source, interpretation, resources, the entities expected in the first results, and the confidence level. It guards behaviour; it cannot show generalisation, because it is built from past failures.
Held-out (tests/search-holdout.json)59 free-form questionsWritten without tuning the engine against them, each with the kind of outcome expected: 41 should be answered, 10 are unrelated to him and should not be, 8 concern a technology the graph lacks and should be pivoted. It reports; it never fails the build.

The metrics are the share of cases with the right outcome, mean reciprocal rank (the average, over questions, of one divided by the rank of the first expected result) among the answered cases, and the time to plan a question.

9.2 Results

MeasureResult
Regression suite156 of 156 cases; retrieval MRR 0.75
Held-out, right outcome overall48 of 59 (81%); baseline before the confidence and pivot work was 40 of 59
Held-out, should be answered34 of 41 (83%); answer MRR 0.64
Held-out, unrelated to him6 of 10 correctly not answered
Held-out, absent technology8 of 8 pivoted
Questions that never need a model34 of the 41 answerable. All 10 unrelated questions are eligible for one; the 8 about absent technology are answered by the pivot and never reach it
Planning timemedian about 3 ms, 95th percentile 14 to 20 ms, on a laptop

Read these with their limits. The pivot's 8 of 8 reflects a lexicon written with those names in mind, not an ability to recognise a technology it was never told about. The 6 of 10 is the engine alone: the four it gets wrong receive a confident answer from a passage that shares words with the question, and the model fallback that exists to catch them is the part that is switched off. The held-out set was written by the author of the engine, so it is held out from the tuning but not blind.

9.3 A negative result: statistics do not separate guesses from answers

The natural way to decide "is this answer reliable" is a threshold on a score. Three were tried on the held-out questions: the BM25 score of the best article, the share of query terms it contains, and the share of query information it contains (each term weighted by its idf). The table shows the last, for the thirteen held-out questions that name nothing the site knows and were answered from a wiki passage.

GroupQuestionsIdf-weighted coverage
Unrelated to him30.48 to 0.53
Answerable, and no miss recorded70.41 to 1.00
Answerable, but the expected result was missed30.55 to 0.77

Between 0.43 and about 0.77 the three groups are interleaved: an unrelated question and a real one can score the same, because both are questions with no entity named whose words half-match an article. The raw BM25 scores overlap in the same way. No threshold separates them, and what does is whether the question names something the site knows, or the answer is an authored or graph answer. This is why section 6 decides by route and uses the coverage figure only at the top of its range. "No miss recorded" means the expected entity was among the first results, or the case names none; the sample is small, and the figures describe it, not a population.

9.4 What was not evaluated

The model fallback has no live-model result. Spelling correction and the follow-up chips are covered by tests of their rules, not by a measure of whether visitors find them useful. No user study was done.

10 Limitations and future work

  • Four unrelated questions are still answered. The engine alone cannot tell them from a paraphrase of a real one. A live evaluation of section 8, or a rule that a weak answer is shown as "closest match" and not as an answer, would address it.
  • The lexicon is data written by hand. Aliases, absent technologies and the weights in section 5.3 are curated, not learned. A script (npm run misses) lists what visitors asked that the engine answered badly, so the lexicon can grow from real questions, with a person deciding each change.
  • Retrieval is lexical. A paraphrase that shares no words with the article is missed. Embeddings were not added: the corpus is 558 passages, there is no model in the default path, and the structure that matters most (typed facts) is not what embeddings improve.
  • The intent rules are ordered patterns. They are easy to explain and test, and brittle at the edges; the regression suite exists because of that.

11 Reproducing

bash
npm run test:search                     # the regression suite and every unit test
node scripts/eval-search.mjs --holdout  # the held-out suite and the confidence gate
node scripts/plan-snapshot.mjs save f   # record what plan() returns for 1,400 questions, then
node scripts/plan-snapshot.mjs check f  # prove a refactor changed nothing
node scripts/build-figures.mjs          # redraw the figures in this article
npm run misses                          # what visitors asked that was answered badly (needs the database)

npm run build:wiki regenerates the wiki graph, evidence, passage index and data/knowledge-state.json. The manifest covers the engine's sources, schema, graph and wiki, and validation recomputes it without writing, so a source change without a rebuild, or an index that is out of date, fails. The component's cj:implementationDigest tracks its declared source files.

References

[1] Nayak, P. "How AI powers great search results". Google, The Keyword, 3 February 2022. https://blog.google/products/search/how-ai-powers-great-search-results/

[2] Google Search Central. "A guide to Google Search ranking systems". https://developers.google.com/search/docs/appearance/ranking-systems-guide (read 7 November 2026).

[3] Robertson, S. and Zaragoza, H. "The Probabilistic Relevance Framework: BM25 and Beyond". Foundations and Trends in Information Retrieval 3(4), 333–389, 2009.

[4] Cormack, G. V., Clarke, C. L. A. and Büttcher, S. "Reciprocal rank fusion outperforms Condorcet and individual rank learning methods". Proceedings of SIGIR 2009, 758–759. doi:10.1145/1571941.1572114

[5] Hogan, A. et al. "Knowledge Graphs". ACM Computing Surveys 54(4), 2021. doi:10.1145/3447772

[6] Berners-Lee, T., Hendler, J. and Lassila, O. "The Semantic Web". Scientific American 284(5), 34–43, 2001.

[7] Harris, S. and Seaborne, A. (eds). "SPARQL 1.1 Query Language". W3C Recommendation, 21 March 2013. https://www.w3.org/TR/sparql11-query/

[8] W3C OWL Working Group (ed.). "OWL 2 Web Ontology Language Document Overview (Second Edition)". W3C Recommendation, 11 December 2012. https://www.w3.org/TR/owl2-overview/

[9] Lewis, P. et al. "Retrieval-Augmented Generation for Knowledge-Intensive NLP Tasks". Advances in Neural Information Processing Systems 33, 2020. arXiv:2005.11401

[10] Edge, D., Trinh, H. et al. "From Local to Global: A GraphRAG Approach to Query-Focused Summarization". arXiv:2404.16130, 2024.

[11] Xiang, Z., Wu, C., Zhang, Q., Chen, S., Hong, Z., Huang, X. and Su, J. "When to use Graphs in RAG: A Comprehensive Analysis for Graph Retrieval-Augmented Generation". arXiv:2506.05690, 2025.

[12] Ji, Z. et al. "Survey of Hallucination in Natural Language Generation". ACM Computing Surveys 55(12), 2023.

[13] Damerau, F. J. "A technique for computer detection and correction of spelling errors". Communications of the ACM 7(3), 171–176, 1964.

[14] Levenshtein, V. I. "Binary codes capable of correcting deletions, insertions and reversals". Soviet Physics Doklady 10(8), 707–710, 1966.

[15] Manning, C. D., Raghavan, P. and Schütze, H. Introduction to Information Retrieval. Cambridge University Press, 2008.

In the knowledge graph

Question engine and the 6 things it is linked to. Open any dot for its page, or explore it in the full graph.

As data

Everything on this page is read from triples in the knowledge graph. These are the 22 it states about Question engine, and 5 more its ontology implies; the console runs SPARQL over the whole graph, here in your browser.

Download
@prefix id: <https://ask.chaipat.cc/id/> .
@prefix rdf: <http://www.w3.org/1999/02/22-rdf-syntax-ns#> .
@prefix cj: <https://ask.chaipat.cc/ns#> .
@prefix rdfs: <http://www.w3.org/2000/01/rdf-schema#> .
@prefix xsd: <http://www.w3.org/2001/XMLSchema#> .

id:SiteEngine
    a cj:Component ;
    rdfs:label "Question engine (ask.chaipat.cc)" ;
    cj:shortLabel "Question engine" ;
    cj:componentOf id:ThisSite ;
    cj:sourceFile "src/engine.js" ,
                  "src/answer.js" ,
                  "src/resources.js" ,
                  "src/intents.js" ,
                  "src/lexicon.js" ,
                  "src/dates.js" ,
                  "src/compare.js" ,
                  "src/confidence.js" ,
                  "src/pivot.js" ,
                  "src/next.js" ;
    cj:implements id:EntityLinking ,
                  id:ReciprocalRankFusion ,
                  id:DamerauLevenshtein ,
                  id:Abstention ;
    cj:uses id:JavaScript ;
    cj:description "Reads a typed question: links the things it names, decides what kind of question it is, gathers candidates from the graph and the wiki, fuses them into one ranking and picks how to answer. It says how sure it is, answers a technology the graph lacks by saying so and showing what he uses instead, suggests follow-up questions and corrects misspelled words. Its design is written up as a short paper, with pseudocode and figures; a verified fallback to a hosted model is a separate component and is switched off." ;
    cj:article "https://ask.chaipat.cc/wiki/SiteEngine"^^xsd:anyURI ;
    cj:implementationDigest "sha256:76bee82e8d40f7961194436faca70aaf39825de30989b600a133667210f73d19" .