Skip to content

feat(bm25): configurable native language + opt-in pgroonga backend - #1538

Merged
nicoloboschi merged 4 commits into
mainfrom
multilingual-bm25
May 26, 2026
Merged

feat(bm25): configurable native language + opt-in pgroonga backend#1538
nicoloboschi merged 4 commits into
mainfrom
multilingual-bm25

Conversation

@nicoloboschi

@nicoloboschi nicoloboschi commented May 8, 2026

Copy link
Copy Markdown
Collaborator

Summary

Makes Hindsight's BM25 indexing language configurable, adds pgroonga as a fourth opt-in text-search backend, and adds a single knob to force the LLM output language across every LLM-driven pipeline. The goal is to serve non-English banks (especially CJK) out of the box.

Three new env vars

Variable Default Purpose
HINDSIGHT_API_TEXT_SEARCH_EXTENSION_NATIVE_LANGUAGE english PG text search dictionary used by the native backend in to_tsvector / to_tsquery. Validated as a PG identifier so it can be embedded safely in raw SQL. Users with zhparser etc. can point to custom dictionaries.
HINDSIGHT_API_TEXT_SEARCH_EXTENSION native Now accepts pgroonga (fourth backend). pgroonga's TokenBigram polyglot tokenizer + NormalizerNFKC150 Unicode normalization handles English, French, Japanese, Chinese, etc. in a single index. Docker recipe at docker/docker-compose/pgroonga/.
HINDSIGHT_API_LLM_OUTPUT_LANGUAGE unset When set, every LLM-generated artifact is forced into this language regardless of source content. Applies uniformly to retain (fact extraction), consolidation (observations / mental models), and reflect (response synthesis).

The two language knobs are deliberately independent — search tokenization and LLM output language are separate concerns. Common patterns are documented on the Multilingual Support page.

Schema change

New migration p4q5r6s7t8u9_configurable_bm25_language drops the GENERATED ALWAYS expression on memory_units.search_vector (and reflections.search_vector), turning them into regular tsvector columns. The application now populates them at INSERT time via to_tsvector(:lang, ...) using the configured native language. Existing rows retain their English-derived lexemes — switching languages only affects newly-written rows. A backfill SQL snippet is documented for users who need to re-index existing data.

The initial migration's _detect_text_search_extension was extended to accept pgroonga (returning native so the migration still creates valid columns); ensure_text_search_extension() at startup converts the schema to pgroonga's structure on first boot, installing the extension if necessary.

LLM output language wiring

A single shared helper engine.prompt_utils.output_language_directive(language) is consumed by all three pipelines so the directive is identical everywhere:

  • retain — appended to the fact-extraction system prompt in _build_extraction_prompt_and_schema.
  • consolidation — appended to the batch observation prompt via a new llm_output_language param on build_batch_consolidation_prompt.
  • reflect — appended to the final response prompt via a new llm_output_language param on build_final_system_prompt, threaded through run_reflect_agent and MemoryEngine.

Documentation

The multilingual story is consolidated on hindsight-docs/docs/developer/multilingual.md alongside the existing LLM / embedding / reranker guidance. configuration.md is a brief env-var table that links out. Backend-choice patterns ("CJK-out-of-the-box → pgroonga", "single-language bank → native + language", "cross-lingual unification → llm_output_language=English") are written up there.

Test plan

  • tests/test_config_validation.py — defaults, lowercasing, PG-identifier rejection, pgroonga accepted, llm_output_language empty-string handling.
  • tests/test_db_abstraction.py — native arm honours configured language, pgroonga branch uses &@~ + pgroonga_score, pgroonga ignores the native language knob.
  • tests/test_multilingual_bm25.pyoutput_language_directive helper, per-pipeline directive injection (retain, consolidation, reflect), str.format safety for the consolidation prompt, migration shape regression.
  • All existing tests pass (tests/test_migration_shape.py, tests/test_alembic_dag.py, tests/test_admin_backup_restore.py).
  • ./scripts/hooks/lint.sh clean.
  • End-to-end smoke against the new pgroonga docker-compose recipe (manual).

CI failures unrelated to this PR

The current CI run has 5 failures that all reproduce on a clean origin/main checkout:

  • test-apitest_hierarchical_fields_categorization count assertion (35 vs 36) — pre-existing breakage from feat(api): add targeted consolidation by observation scopes #1746 which added consolidation_max_memories_per_round to _CONFIGURABLE_FIELDS without updating the count.
  • test-rust-cli / test-embed-windows / test-doc-examples (cli) — all the same Rust compilation error (E0061: this method takes 3 arguments but 2 arguments were supplied) in hindsight-cli. I touched no Rust / API / OpenAPI files.
  • LLM acceptance (vertexai)test_llm_api_methods: known Vertex/Gemini flake (LLM occasionally doesn't emit the expected tool call).

@nicoloboschi
nicoloboschi marked this pull request as draft May 8, 2026 10:28
Adds two new env-level config knobs and a new opt-in BM25 backend so users
can serve non-English banks (especially CJK) out of the box.

- HINDSIGHT_API_BM25_LANGUAGE drives the PostgreSQL text search dictionary
  used by the native tsvector backend (default: english). Validated as a
  PG identifier so it can be safely embedded in to_tsvector('<lang>', ...).
- HINDSIGHT_API_RETAIN_OUTPUT_LANGUAGE forces the fact extractor to emit
  facts in the specified language regardless of source content's language.
  Independent from bm25_language so users can mix indexing/extraction
  languages deliberately.
- New 'pgroonga' option for HINDSIGHT_API_TEXT_SEARCH_EXTENSION. Uses
  TokenBigram + NormalizerNFKC150 — single polyglot index handles English,
  CJK, etc. simultaneously. Ships with a docker-compose recipe.

To support a per-deployment language, the GENERATED ALWAYS expression on
memory_units.search_vector (and reflections.search_vector) is dropped via
new alembic migration p4q5r6s7t8u9. The application now populates these
columns at INSERT time using the configured bm25_language.
…ontent to dedicated page

- Rename HINDSIGHT_API_BM25_LANGUAGE → HINDSIGHT_API_TEXT_SEARCH_EXTENSION_NATIVE_LANGUAGE.
  The setting only applies to the "native" backend (vchord/pg_textsearch/pgroonga
  use their own tokenizers), so the env var name now reflects that scope. Field
  renamed to text_search_extension_native_language.
- Trim configuration.md back to a brief env-var table + link. The expanded
  multilingual / CJK / pgroonga content moves to the dedicated multilingual.md
  page, alongside the existing LLM / embedding / reranker multilingual guidance.
…lidation + reflect

Renames HINDSIGHT_API_RETAIN_OUTPUT_LANGUAGE → HINDSIGHT_API_LLM_OUTPUT_LANGUAGE
(field llm_output_language) and applies the same "respond exclusively in {lang}"
directive across every LLM-generated artifact:

- retain (fact extraction) — already wired, just renamed.
- consolidation (observations / mental models) — appended to the batch
  consolidation prompt via a new llm_output_language parameter.
- reflect (response synthesis) — appended to the final-system prompt via a
  new parameter threaded through run_reflect_agent and memory_engine.

The shared directive lives in engine/prompt_utils.output_language_directive
so all three pipelines build the same instruction from a single source.
@nicoloboschi
nicoloboschi marked this pull request as ready for review May 26, 2026 12:23
@nicoloboschi
nicoloboschi merged commit cb04cb7 into main May 26, 2026
54 of 56 checks passed
nicoloboschi added a commit that referenced this pull request May 26, 2026
Rebasing onto main pulled in hindsight-docs/ changes from #1704
(Codex OAuth embeddings) and #1538 (pgroonga). Re-run the
generate-docs-skill.sh generator so the cached
skills/hindsight-docs/references/developer/configuration.md mirror
matches the current developer docs and verify-generated-files passes.
nicoloboschi added a commit that referenced this pull request May 26, 2026
…1755)

* feat(api): add ParadeDB pg_search as Citus-compatible BM25 backend

Adds a fourth value (`pg_search`) for `HINDSIGHT_API_TEXT_SEARCH_EXTENSION`
alongside the existing `native`, `vchord`, and `pg_textsearch`. ParadeDB
pg_search is the only true-BM25 backend that works on a Citus distributed
Postgres cluster, so this unblocks horizontally scaled deployments.

The retrieval arm builds the @@@ predicate via paradedb.boolean(should =>
ARRAY[paradedb.match('text', $4), ...]) since @@@ on the key_field requires
field-qualified terms; this preserves multi-field coverage (text + context
+ text_signals) without needing query string interpolation.

Includes a docker-compose example under docker/docker-compose/pg_search/
based on the official paradedb/paradedb:latest-pg17 image.

Closes #1754

* fix: accept pgroonga in n9i0 migration; clarify consolidator search_vector comment

- n9i0 (learnings + pinned_reflections) validation now permits 'pgroonga',
  treating it as native at this migration stage. ensure_text_search_extension()
  at startup converts the reflections table (renamed from pinned_reflections in
  p1k2l3m4n5o6) to pgroonga structures; the learnings table is dropped in the
  same later migration so its transient native column never reaches steady state.
  Without this, pgroonga users hit ValueError on a fresh install.

- consolidator.py single-observation INSERT: the previous comment claimed
  search_vector was GENERATED ALWAYS, but migration p4q5r6s7t8u9 dropped that
  expression. Updated to reflect current behavior and flag the resulting gap
  for native (observations land with NULL search_vector and are not BM25-
  searchable until reflected/re-ingested) so a follow-up can address it.

* chore: regenerate hindsight-docs skill after rebase

Rebasing onto main pulled in hindsight-docs/ changes from #1704
(Codex OAuth embeddings) and #1538 (pgroonga). Re-run the
generate-docs-skill.sh generator so the cached
skills/hindsight-docs/references/developer/configuration.md mirror
matches the current developer docs and verify-generated-files passes.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant