FoodScholar — the facade¶
FoodScholar is the single object you work through. It owns the configured stores, the
ontology, the LLM client, and the embedder, and exposes every pipeline phase as a
method. Construct it with from_config for real
work, or in_memory for a zero-setup instance.
The sub-surfaces hang off it as attributes:
fs.graph→ GraphView (read/write the graph)fs.ontology→ FoodOnAPI (FoodOn lookup)fs.viz→ VizView (renderable views)fs.config→ FoodScholarConfig (the live, mutable config)
.. py:class:: FoodScholar(config, *, chunk_store, graph_store, embedder=None, llm=None, entity_store=None, card_store=None)
- module:
foodscholar.facade
Bases: :py:class:
objectUser-facing facade for the library.
.. py:property:: FoodScholar.embedder
- module:
foodscholar.facade
- type:
~foodscholar.storage.protocols.Embedder
Lazily-built chunk embedder.
For
memorybackends or when the[annotate]extra is missing, this is_MockEmbedder(). For production it isHFEmbedder("BAAI/bge-base-en-v1.5")(BRIEF §2 — single embedder across source types). First access pays the model-load cost; subsequent accesses are free.
.. py:method:: FoodScholar.in_memory(*, config=None, embedder=None, llm=None)
- module:
foodscholar.facade
- classmethod:
Zero-config facade backed by `InMemoryChunkStore` + `InMemoryGraphStore`. Intended for notebooks, tests, and quick experiments. Pass a `config` (dict, YAML path, or `FoodScholarConfig`) to override defaults.
.. py:method:: FoodScholar.from_config(config, *, embedder=None, llm=None) :module: foodscholar.facade :classmethod:
Construct from a YAML path, a Python dict, or a validated config. Builds whichever stores the config declares. `memory` works today; `elastic` and `neo4j` adapters lift the same constructor.
.. py:method:: FoodScholar.info() :module: foodscholar.facade
.. py:method:: FoodScholar.load_chunks(path) :module: foodscholar.facade
Read chunks from a parquet/jsonl/csv path and upsert into the chunk store.
.. py:method:: FoodScholar.upsert_chunks(chunks) :module: foodscholar.facade
Upsert an explicit list of chunks (useful in tests and notebooks).
.. py:method:: FoodScholar.load_and_annotate(path, *, snapshot_path=None, ignore_source_types=None) :module: foodscholar.facade
Single-pass: load chunks → run GLiNER+HNSW annotate → optional snapshot. This is the release-ready entry point that mirrors the validated prototype's `main()` — one call per corpus file. If a parquet snapshot is configured (via `snapshot_path` here or `cfg.corpus.annotated_snapshot_path`) and already exists with non-zero size, the call short-circuits and returns None — matching the prototype's skip-if-output-exists idempotency. ``ignore_source_types`` (defaults to ``cfg.corpus.ignore_source_types``) drops chunks whose ``source_type`` is in the set before annotation — useful to skip e.g. all ``abstract`` chunks when ingesting a guideline-only knowledge base.
.. py:method:: FoodScholar.ingest(corpus_dir, *, nel_dir=None, snapshot_path=None, ignore_source_types=None) :module: foodscholar.facade
Ingest corpus + annotations into `fs.chunk_store`. Two modes: - ``nel_dir`` **supplied** → annotations come from pre-computed `(chunk_id, chunk_entities_ner, chunk_uri_nel)` CSVs (the prototype's output shape). Chunks are loaded from ``corpus_dir``, annotations attached by `chunk_id`, and everything upserted to `fs.chunk_store`. **No GLiNER, no HNSW, no chunk embedding** — fast, deterministic, works without the `[annotate]` extra installed. Call `fs.embed()` afterwards to fill in chunk vectors for kNN search. - ``nel_dir`` **omitted** → falls back to `load_and_annotate(corpus_dir)` which runs GLiNER + HNSW from scratch (this path does embed, since the runner has the BGE-base embedder already loaded). ``snapshot_path`` (or `cfg.corpus.annotated_snapshot_path`) writes a parquet snapshot of the annotated chunks after ingest. If the snapshot already exists and is non-empty the whole call short-circuits — the same idempotency guarantee as `load_and_annotate`. ``ignore_source_types`` (defaults to ``cfg.corpus.ignore_source_types``) drops chunks whose ``source_type`` is in the set before upsert. Their NEL rows are skipped too — nothing about them reaches the chunk store or shows up in ``fs.entities`` later. Pass e.g. ``{"abstract"}`` to ingest only textbook + guide chunks... py:property:: FoodScholar.ontology :module: foodscholar.facade :type: FoodOnAPI
.. py:method:: FoodScholar.load_ontology(*, refresh=False) :module: foodscholar.facade
.. py:method:: FoodScholar.attach_ontology(api) :module: foodscholar.facade
.. py:property:: FoodScholar.ner :module: foodscholar.facade :type: ~foodscholar.storage.protocols.NER
Lazily-built NER. `cfg.annotate.ner = 'gliner'` (the only choice in v0.1). Override with `fs.attach_ner(...)` before first access to install a custom NER.
.. py:property:: FoodScholar.linker :module: foodscholar.facade :type: ~foodscholar.storage.protocols.Linker
`HNSWLinker` over `HNSWNELIndex`. First access builds the FoodOn term index (or loads it from the cache path) — that's the expensive call; subsequent accesses are free. :type: Lazily-built linker. Default
.. py:method:: FoodScholar.attach_ner(ner) :module: foodscholar.facade
.. py:method:: FoodScholar.attach_linker(linker) :module: foodscholar.facade
.. py:method:: FoodScholar.embed(*, only_missing=True, batch_size=64) :module: foodscholar.facade
Fill in chunk-text embeddings for chunks already in `fs.chunk_store`. Walks the store, encodes each chunk with the configured embedder (BGE-base for production — BRIEF §2/§7), and writes back only the `embedding` + `embedding_model` fields via `chunk_store.update_embeddings_bulk`. Mentions, links, and other annotations are untouched. - `only_missing=True` (default): skip chunks whose `embedding_model` is already a real model id (anything that isn't the deterministic `mock-embedder-v0`). Re-runs are cheap. - `only_missing=False`: re-encode every chunk regardless. Useful after swapping the configured embedder. Builds the production embedder lazily on first call — that is the ~440 MB BGE-base load, paid once per process.
.. py:method:: FoodScholar.build_entities(*, cap_chunk_sample=None) :module: foodscholar.facade
Derive first-class `Entity` records from the chunks already in the store and write them to (a) `fs.entity_store`, (b) `fs.graph_store` as `(:Entity)` nodes with `(:Chunk)-[:MENTIONS]->(:Entity)` edges. Walks `fs.chunk_store.iter_chunks(...)`, dedupes `EntityLink`s by `ontology_id`, aggregates `(mention_count, chunk_count, chunk_ids, facet_hint, last_seen)`, and enriches with `(label, synonyms, ancestor_ids)` from `fs.ontology` when an ontology is configured AND the entity id is a FOODON id (other OBO prefixes ship with the most-frequent surface form as the label and no ancestors). Idempotent — re-running over an unchanged corpus produces the same Entity records; re-running after `fs.ingest` of new chunks updates counts and the chunk_ids sample.
.. py:method:: FoodScholar.annotate() :module: foodscholar.facade
Run NER + linking + embedding over every chunk in `chunk_store`.
.. py:method:: FoodScholar.init() :module: foodscholar.facade
Provision the backing stores declared by the config. Calls `chunk_store.init()`, `entity_store.init()`, `graph_store.init()`, and `card_store.init()` — all are in the storage protocols and are no-ops for the in-memory backends, so this works uniformly regardless of where the stores live.
.. py:method:: FoodScholar.build_layer_a() :module: foodscholar.facade
.. py:method:: FoodScholar.attach() :module: foodscholar.facade
.. py:method:: FoodScholar.semantic_consolidate(*, facet=’foods’, dry_run=True) :module: foodscholar.facade
Embed shelves, find near-duplicate pairs, and merge via an LLM judge. Runs *after* `fs.attach()` so the judge can ground each decision on real sample chunks (it reads `chunk.shelf_ids`, written by attach). - `dry_run=True` (default): read-only. Returns the `ConsolidationArtifact` — candidates, decisions, and what the pre-LLM filters dropped — for inspection. Nothing is persisted. - `dry_run=False`: applies confirmed merges (above `auto_merge_confidence`), re-persists the shelf set, and re-runs `fs.attach()` so the merged-away shelves' chunks re-home onto the surviving canonical shelf. Set `layer_a.semantic_consolidation.judge_enabled=False` for a zero-cost candidate preview (no LLM calls).
.. py:method:: FoodScholar.audit() :module: foodscholar.facade
Run cross-store invariant checks and return a structured report. Read-only — never writes. Returns an `AuditReport` with five sections: inventory, coverage, cross-store consistency, attach integrity, and structural sanity. `report.passed` is True iff zero critical checks failed; `report.critical_failures` lists the broken invariants. Print `report` directly for a human-readable summary.
.. py:method:: FoodScholar.quality_report(*, facet=’foods’, top_n=20, sample_size=20, canonical_terms=None, seed=0) :module: foodscholar.facade
Produce a domain-expert quality report for one facet of Layer A. Pairs with `fs.audit()` but answers a different question: not "is the graph correctly built" (invariants) but "is the graph good" (semantic / coverage). Read-only; the output is a Pydantic `QualityReport` whose `__str__` is Markdown for direct notebook viewing. Sections: 1. Top shelves at a glance — table sorted by chunk_count with direct/lifted split + 3 example chunk snippets per shelf. 2. Hierarchy walkthrough — parent chains + sample descendants for the top 5 shelves, so the expert can sanity-check navigation. 3. Suspicious shelves — conservative heuristic flags (EFSA-style code prefixes, "datum" labels, collapse-misses, zero-chunk survivors). 4. Canonical vocabulary check — checklist of foods/nutrients/ conditions an expert would expect, with status per term. Override the default list via `canonical_terms=`. 5. Random chunk sample — `sample_size` randomly-chosen attached chunks with their text + attached shelf labels, formatted for hand audit per BRIEF §17. `seed` for reproducibility.
.. py:method:: FoodScholar.build_layer_b(*, facet=’foods’, dry_run=False) :module: foodscholar.facade
Build Layer B themes for `facet`. For each shelf with `≥ cfg.layer_b.min_chunks_per_shelf` attached chunks whose embedded-fraction clears `cfg.layer_b.min_embedded_fraction`, runs the dual-pass pipeline (similarity + relatedness), merges candidates greedily, labels themes (c-TF-IDF + LLM polish when `cfg.layer_b.labeling.strategy == "llm"`), picks a per-pass-aware primary chunk, and persists: - `(:Theme)` nodes + `(:Shelf)-[:IN_SHELF...]->(:Theme)` edges - `(:Chunk)-[:THEME_OF {primary, weight}]->(:Theme)` edges - ES `theme_ids` denorm via `bulk_set_theme_ids` (preserves `shelf_ids`) Skips the synthetic facet root (`facet:foods` etc.) — that's the iteration-8 unclassified bucket. `dry_run=True` runs the full pipeline but skips all writes. Useful for `n_themes` estimates and audit-decision inspection without modifying the stores. Returns a `LayerBArtifact` summarizing the run (themed/skipped shelf counts, total themes, per-pass distribution, leiden seed, timestamps). See `layer_b_construction_brief.md` for the full architecture... py:method:: FoodScholar.build_quality_report(*, facet=’foods’) :module: foodscholar.facade
Read-only WARN-level quality report for Layer B of `facet`. Pairs with `fs.audit()` (CRITICAL invariants) but answers "is the build *good* / well-tuned" rather than "is it correct". Reads shelves, themes, and attachments; mutates nothing. Returns a `LayerBQualityReport` whose `__str__` is Markdown for notebook viewing — structural stats (shelves, depth, fanout, support ratios), theme stats (coverage, source mix, duplicate/tiny/leakage counts), and a list of `LayerBWarning`s. Warning thresholds come from `cfg.layer_b.audit`. See `layer_b/quality.py` for the full metric + warning list.
.. py:method:: FoodScholar.sweep_layer_b(*, facet=’foods’, grid=None) :module: foodscholar.facade
Non-mutating tuning sweep over a grid of Layer B configs. Runs each config combination as a `dry_run` build with cheap keyword labels and `per_shelf` Pass 1, scores the resulting quality metrics, and returns a ranked `SweepResult` (best first). Nothing is persisted — apply the winning config (`result.best`) yourself and rebuild. `grid` maps dotted `layer_b` config paths (e.g. `"leiden.min_community_size"`) to candidate values; defaults to the full 160-config Cartesian product (`sweep.DEFAULT_GRID`). Scoring weights are fixed and documented in `layer_b/sweep.py`.
.. py:method:: FoodScholar.build_layer_c(*, facet=’foods’, dry_run=False) :module: foodscholar.facade
Build Layer C — one summary Card per Layer B theme of `facet`. Each theme's member chunks are compressed by a cheap extractive method (Stage 1, map-reduce when large), then refined by the LLM into a Card (Stage 2). `dry_run=True` runs both stages but skips persistence. Returns a `LayerCReport`.
.. py:method:: FoodScholar.benchmark_layer_c(*, facet=’foods’, themes=5, out=None) :module: foodscholar.facade
Read-only benchmark of all extractive methods over the largest `themes` themes of `facet`. Writes per-method JSON metrics; returns the results keyed by theme id. No LLM, no persistence.
.. py:method:: FoodScholar.export_graphml(output, *, facet=’foods’) :module: foodscholar.facade
Export the Layer A/B/C graph (shelves + themes + cards) to GraphML. Typed nodes (`shelf` / `theme` / `card`) with their attributes and edges (`parent_of` / `has_theme` / `has_card`), readable by Gephi, Cytoscape, yEd, etc. `facet=None` exports every facet. Returns the output `Path`.
.. py:method:: FoodScholar.search_cards(text, *, k=10) :module: foodscholar.facade
Vector-search Layer C cards by `text`. Embeds the query with the chunk embedder, runs kNN over the card store, and returns the matching `Card`s nearest-first. (A thin retrieval helper; full `query()` with answer synthesis is still deferred.)
.. py:method:: FoodScholar.build() :module: foodscholar.facade
.. py:method:: FoodScholar.query(text) :module: foodscholar.facade