Configuration¶
Everything about a FoodScholar instance — which stores it talks to, which LLM it calls, how each layer is built — comes from one validated config object. This page explains the config API, walks through each section with examples, and ends with copy-paste recipes for the common setups.
The mental model¶
One object, validated up front. Your YAML (or dict) is parsed into a
FoodScholarConfigPydantic model. Every field has a type and a default; unknown keys are rejected (extra="forbid"), so a typo likestroage:fails loudly at load time instead of being silently ignored.Only
corpusis required. Every other section has sensible defaults — omit a section to take them.Secrets come from the environment, never the file. Use
${VAR}anywhere and it’s substituted from the environment at load time.
The config API¶
FoodScholar.from_config(...) accepts three forms of the same thing:
from foodscholar import FoodScholar
from foodscholar.config import FoodScholarConfig
# 1. A YAML file path
fs = FoodScholar.from_config("config.yaml")
# 2. A plain dict (great for notebooks and tests)
fs = FoodScholar.from_config({
"corpus": {"chunks_path": "data/corpus"},
"storage": {"chunk_store": {"backend": "memory"},
"graph_store": {"backend": "memory"}},
})
# 3. An already-validated config object (build it in code, reuse it)
cfg = FoodScholarConfig.model_validate({"corpus": {"chunks_path": "data/corpus"}})
fs = FoodScholar.from_config(cfg)
${ENV} substitution runs over all three forms (strings, nested dicts, lists), so
in-code configs can carry placeholders exactly like YAML. Under the hood
resolve_config() normalizes any of them to a FoodScholarConfig; load_config(path)
is the YAML-only helper.
Tip
Tune at runtime. The config object is plain Pydantic and mutable, so you can poke a single knob between runs without rewriting the file:
fs.config.layer_b.pass1_mode = "per_shelf"
fs.build_layer_b(facet="foods")
This is exactly how the notebook sweeps Layer B settings.
Sections at a glance¶
Section |
Required |
Controls |
|---|---|---|
|
yes |
where chunks live; the offline snapshot path |
|
no |
the chunk store (Elasticsearch/memory) and graph store (Neo4j/memory) |
|
no |
provider + fallback chain (omit → built-in mock) |
|
no* |
the FoodOn OWL path + cache (*required for real annotation/Layer A) |
|
no |
NER (GLiNER) + embedder + the entity linker |
|
no |
shelf projection method + prune/aliasing knobs |
|
no |
theme discovery passes, merge, labeling |
|
no |
card LLM model + grounding/safety |
storage — where data lives¶
Each store independently selects a backend. memory needs nothing; elastic and
neo4j point at running services.
storage:
chunk_store:
backend: elastic # or: memory
url: http://localhost:9200
index: foodscholar_chunks
# auth (optional): basic creds win over api_key; both fall back to env
username: elastic
password: ${ELASTIC_PASSWORD}
bulk_size: 1000 # docs per _bulk request (1000–5000 is the sweet spot)
graph_store:
backend: neo4j # or: memory
url: bolt://localhost:7687
user: neo4j
password: ${NEO4J_PASSWORD}
Note
Mix freely — e.g. chunk_store: elastic with graph_store: memory. The all-memory
combination is what FoodScholar.in_memory() selects and needs zero services.
ES credentials, if unset here, fall back to ELASTICSEARCH_API_KEY; Neo4j’s password
falls back to NEO4J_PASSWORD.
llm — the language model¶
The LLM powers the (optional) linker LLM step and Layer C cards. Declare a primary provider and an ordered fallback chain; the chain is tried in order, each entry only if the earlier ones errored (timeout, rate limit, auth, outage).
llm:
primary: { provider: groq, model: llama-3.3-70b-versatile }
fallbacks:
- { provider: ollama, model: llama3.1, host: http://localhost:11434 }
timeout_s: 30
max_retries: 2
Providers: anthropic, openai, groq, gemini, ollama. API keys come from the
environment (GROQ_API_KEY, ANTHROPIC_API_KEY, OPENAI_API_KEY, GEMINI_API_KEY),
or you may set api_key: inline (the inline value wins). Ollama needs only a running
daemon. Install providers with the [llm] extra.
Important
Omit the llm: section entirely (or use in_memory()) and FoodScholar uses a
built-in mock client that returns a fixed string. That keeps tests and the
quickstart deterministic and offline — but if you forget to configure a real provider,
your Layer C cards and theme labels will be mock text. Wire the provider in at
construction, not after.
corpus & ontology¶
corpus:
chunks_path: data/corpus # dir of chunk CSVs, or a file
annotated_snapshot_path: data/annotated.parquet # offline snapshot (idempotent reruns)
ignore_source_types: [abstract] # drop these source types at ingest
ontology:
foodon_path: data/foodon.owl
cache_path: data/foodon_cache.parquet # Parquet cache beside the OWL
prefix_filter: ["FOODON:"] # keep only FOODON ids; null = keep all
prefix_filter matters: real FoodOn OWL files embed CHEBI/NCBITaxon/BFO terms inline,
and the default keeps the linker from matching food queries against unrelated
ontologies. See Corpus input format for the chunk/NEL input format.
annotate — NER, embeddings, linking¶
annotate:
ner: gliner # GLiNER-bio NER (the only strategy)
gliner:
model_id: urchade/gliner_large_bio-v0.1
threshold: 0.4
embedder: BAAI/bge-base-en-v1.5 # 768-d chunk embeddings
linker:
nel_backend: hnsw # local hnswlib index (or: elastic)
nel_encoder: biolord # sapbert | biolord | minilm | mpnet
nel_top_k: 1
nel_min_sim: 0.70 # reject links below this cosine
See Annotation — NER & linking for how these pieces fit together. You can skip live annotation entirely by supplying pre-computed NEL CSVs at ingest time.
layer_a, layer_b, layer_c — building the graph¶
These expose every construction knob. The most useful ones:
layer_a:
projection: backbone # "backbone" (1a+, default) or "prune" (fallback)
alias_shelves: true # LLM display labels for jargon shelves (additive)
layer_b:
min_chunks_per_shelf: 50 # skip shelves smaller than this
pass1_mode: per_shelf # "per_shelf" (default) or "global"
leiden: { min_community_size: 15 } # the main Layer B coverage lever
labeling: { strategy: llm } # "keyword" (free) or "llm"
layer_c:
llm_model: claude-sonnet-4-6
grounding_check: strict # reject card claims not grounded in cited chunks
safety_sensitive_facets: [allergies]
Each is explained in depth in the Concepts pages —
Layer A, Layer B,
Layer C — and the repo’s config.example.yaml lists
every field with its default and rationale.
Recipes¶
Zero-config, in memory¶
fs = FoodScholar.in_memory() # all-memory stores + mock embedder + mock LLM
Offline, from a snapshot (no services)¶
corpus:
chunks_path: data/corpus
annotated_snapshot_path: data/annotated.parquet
ontology: { foodon_path: data/foodon.owl, cache_path: data/foodon_cache.parquet }
storage:
chunk_store: { backend: memory }
graph_store: { backend: memory }
Full local stack (real ES + Neo4j + Groq)¶
corpus: { chunks_path: data/corpus }
ontology: { foodon_path: data/foodon.owl, cache_path: data/foodon_cache.parquet }
llm:
primary: { provider: groq, model: llama-3.3-70b-versatile }
storage:
chunk_store: { backend: elastic, url: http://localhost:9200, index: foodscholar_chunks }
graph_store: { backend: neo4j, url: bolt://localhost:7687, user: neo4j, password: ${NEO4J_PASSWORD} }
Tip
Because the config is validated, a misnamed key is caught immediately:
pydantic_core.ValidationError: ... Extra inputs are not permitted. Treat that as a
helpful spell-checker for your config.