DEV Community

Kenji
Kenji

Posted on

Hidden wiki pages never reached our chat until we rebuilt the index on a timer

Chat answers from a local Milvus Lite file, not from MySQL on the request path. Editors can change a page or drop notes onto a hidden category, and the model still sees whatever was embedded last time.

We use hidden pages on purpose. Some answers should not sit on the public wiki, so they go into categories that the site listing skips. Retrieval was also skipping them. One support question had a dedicated hidden FAQ in the database; the model cited an unrelated shop page and said the corpus had nothing on the topic.

The fix is a small sidecar next to the API, same venv, no extra services. On process start, and then every 12 hours, it reads the wiki tables (page id, category, title, update_time, body). Hidden rows are included; we do not filter on visible. Body prefers one locale and falls back to a second. The article key keeps the original category name, hidden marker and all, so the client can still open that page.

A fingerprint of id + update_time + body hash is compared with the last run. No change: exit, leave the API alone. Change: strip HTML, split chunks, embed with the same model the retriever already uses, write milvus_lite.next.db, then replace the live .db. Milvus Lite needs a path that ends in .db. After a real swap we restart the chat process, because it opens the file at boot and has no reload. Unchanged fingerprints must not restart anything; that would kill in-flight replies.

First production run pulled every page with content, including the hidden FAQ. Hitting the existing chat SSE endpoint with the old support question now points at that FAQ.

If someone publishes a hidden page and needs it in chat before the next 12-hour tick, restart the sidecar. It indexes once on startup.

Top comments (9)

Collapse
 
ahmetozel profile image
Ahmet Özel •

The unchanged-fingerprint path is a useful safeguard against unnecessary restarts. I would make sure that fingerprint is computed from exactly the same captured rows used to build milvus_lite.next.db, rather than from a second database read.

An edit arriving between those reads could otherwise label an index with a fingerprint for content it never embedded. A small replay test can pause extraction, update the hidden FAQ, then resume the build and check which version becomes searchable. Either a consistent snapshot or an explicitly detected concurrent change would keep the index and its completion marker describing the same source state.

Collapse
 
kenjiginjo profile image
Kenji •

The hash and the index both come from one fetch. If a page changes while we're embedding, the next 12-hour run picks it up.

Collapse
 
ahmetozel profile image
Ahmet Özel •

Using one fetch for both artifacts resolves the consistency issue I was asking about. I would label the published index with the source-fetch time separately from the index-publication time, since a long embedding run can make those noticeably different.

The 12-hour schedule then describes the next opportunity to observe a change, rather than a strict freshness guarantee. Recording failed runs and the age of the last successfully fetched snapshot would make that distinction visible when a rebuild fails or takes longer than expected.

Thread Thread
 
kenjiginjo profile image
Kenji •

Yeah. Source-fetch time and when the index actually lands can drift apart if embedding runs long. We don't stamp those separately yet. Failed runs just leave the old index in place. Logging last-success age would make that visible without treating the 12h tick as a freshness SLA.

Thread Thread
 
ahmetozel profile image
Ahmet Özel •

Leaving the old index in place on failure is a useful availability property, provided the status continues to identify that old successful revision. A failed scheduled attempt should not reset the apparent freshness clock.

I would show source-fetch age and last-publish age separately, with the latest attempt's outcome beside them. A long embedding run then looks different from repeated failed builds, and both remain distinguishable from a healthy index whose source simply has not changed. That avoids turning the 12-hour schedule into an unsupported freshness promise.

Thread Thread
 
kenjiginjo profile image
Kenji •

😀yeah

Thread Thread
 
ahmetozel profile image
Ahmet Özel •

Then I would leave the 12-hour interval described as a scheduling policy, with no stronger guarantee attached. That keeps the operational claim precise.

One lightweight regression is whether two consecutive failed rebuilds leave the published revision and its original source-fetch timestamp unchanged. It would catch a dashboard accidentally treating the latest attempt as fresh data, while preserving the existing behavior of continuing to serve the last successful index.

Thread Thread
 
kenjiginjo profile image
Kenji •

Yeah. 12 hours is only when we try again. We don't have a check yet that two failed rebuilds leave the published revision and its original fetch timestamp alone. That'd catch a dashboard treating the latest attempt as fresh.😜

Thread Thread
 
ahmetozel profile image
Ahmet Özel •

I would put a valid empty rebuild beside the two failed rebuilds in that fixture. An empty source collection can be a legitimate new publication, whereas a failed fetch returning no content must leave the old publication intact. Both can otherwise look like zero documents to the dashboard.

Giving those outcomes distinct statuses would let the test assert that only the valid empty result advances the published revision and source timestamp. It would catch an availability fallback accidentally preserving documents that were actually removed from the source.

⌂ 换网址