Skip to content

vector search - #388

Open
yutin1987 wants to merge 2 commits into
masterfrom
vector-search
Open

vector search#388
yutin1987 wants to merge 2 commits into
masterfrom
vector-search

Conversation

@yutin1987

@yutin1987 yutin1987 commented May 5, 2026

Copy link
Copy Markdown

混合搜尋(kNN 檢索 + BM25 排序)使用說明

如何啟用

ListArticles / ListRepliesfilter 加上 embedding 欄位:

  • 不傳 embedding → 純 BM25(預設,完全向後相容)。
  • 傳一個數字(最小 cosine 相似度,0~1)→ kNN 檢索 + BM25 排序。
    • 數字越大越嚴格;建議從 0.7 起調。

embedding 是一個 Float,直接給門檻值即可,不是物件。

範例

文字查詢

# 純 BM25(原本行為)
query {
  ListArticles(filter: { moreLikeThis: { like: "雞蛋 缺蛋 漲價" } }) {
    edges { node { id text } }
  }
}

# kNN 檢索 + BM25 排序,相似度門檻 0.7
query {
  ListArticles(filter: {
    moreLikeThis: { like: "雞蛋 缺蛋 漲價" }
    embedding: 0.7
  }) {
    edges { node { id text } }
  }
}

ListReplies 用法相同(文字查詢):

query {
  ListReplies(filter: {
    moreLikeThis: { like: "疫苗 副作用" }
    embedding: 0.7
  }) {
    edges { node { id text } }
  }
}

媒體查詢(僅 ListArticles)

傳入媒體 URL + embedding,就會對圖片/音訊/影片做語意 kNNquery {
  ListArticles(filter: {
    mediaUrl: "https://example.com/suspicious.jpg"
    embedding: 0.7
  }) {
    edges { node { id articleType attachmentHash } }
  }
}

- 若該媒體系統裡已有(相同內容雜湊),直接重用既有向量,不重算。
- 沒有的話,伺服器會抓該 URL 的內容即時產生向量(不落地 GCS)。

行為與注意事項

- Opt-in:沒傳 embedding 時一律純 BM25,行為不變。
- 永不弄壞搜尋:若向量產生失敗(例如外部服務異常),會自動退回純 BM25。
- 相似度門檻:embedding 的值是每個查詢片段對文件向量的最小 cosine 相似度;低於門檻的候選會被丟掉。
- 音訊/影片:文件向量只涵蓋前 80 秒(模型單次上限);超過的內容仍由逐字稿走 BM25 涵蓋。

伺服器端前提

- 文件要先有向量才搜得到:
  - 新文章/回覆建立時會自動 embed。
  - 舊資料需跑 backfillnode build/scripts/migrations/backfillVertexEmbeddings.js --index both
(建議先 --dry-run 或 --limit 小量試跑)。

@claude claude Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Claude Code Review

This repository is configured for manual code reviews. Comment @claude review to trigger a review and subscribe this PR to future pushes, or @claude review once for a one-time review.

Tip: disable this comment in your organization's Code Review settings.

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces AI embedding generation and caching capabilities. It adds a new EMBEDDING type to the GraphQL schema, implements a createEmbedding utility using Google Vertex AI with support for media chunking, and includes comprehensive tests. Feedback focuses on a breaking change in the createAIResponse utility that affects existing callers, the lack of handling for concurrent 'LOADING' states which could lead to redundant API calls, and opportunities to improve performance by parallelizing media chunk processing and reusing AI clients. Additionally, the AIEmbedding GraphQL type should include the text field to expose error messages.

Comment thread src/graphql/util.js
Comment thread src/util/embedding.ts
Comment on lines +86 to +93
if (
cached &&
cached.status === 'SUCCESS' &&
Array.isArray(cached.embeddings) &&
cached.embeddings.length > 0
) {
return cached.embeddings as EmbeddingChunk[];
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

The current logic only returns cached embeddings if the status is SUCCESS. If an embedding is currently being generated (status: 'LOADING'), this code will proceed to call createAIResponse, which likely creates a duplicate loading record and starts a redundant generation process. It should instead wait for the existing process to complete.

Comment thread src/util/embedding.ts Outdated
Comment thread src/util/embedding.ts Outdated
Comment on lines +105 to +107
fields: {
...commonAiResponseFields,
},

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The AIEmbedding type is missing the text field. Since createEmbedding stores error messages in the text field when the status is ERROR, this field should be added to the GraphQL model to allow clients to retrieve error details.

  fields: {
    ...commonAiResponseFields,
    text: { type: GraphQLString },
  },

@yutin1987
yutin1987 force-pushed the vector-search branch 2 times, most recently from 770f044 to cbfc471 Compare May 5, 2026 03:43
@yutin1987
yutin1987 force-pushed the vector-search branch 2 times, most recently from 664a2db to bcf2737 Compare May 20, 2026 15:57
@yutin1987
yutin1987 force-pushed the vector-search branch 2 times, most recently from b88be5e to 0e01635 Compare June 27, 2026 16:26
@coveralls

coveralls commented Jun 27, 2026

Copy link
Copy Markdown

Coverage Status

coverage: 83.238% (+1.2%) from 81.989% — vector-search into master

@yutin1987
yutin1987 force-pushed the vector-search branch 2 times, most recently from 366b8d2 to 299be47 Compare June 28, 2026 05:52
@yutin1987 yutin1987 changed the title [draft] Vector search vector search Jul 12, 2026
@yutin1987
yutin1987 force-pushed the vector-search branch 4 times, most recently from a42a369 to d6ec108 Compare July 13, 2026 04:04
yutin1987 and others added 2 commits July 26, 2026 20:57
Add dense-vector (kNN) retrieval to ListArticles/ListReplies, opt-in via
the `embedding` similarity filter. kNN narrows the candidate set and the
existing BM25 / perceptual-hash scoring ranks the results.

- Embed articles and replies on create; backfill script for old docs.
- Audio/video embed as a single vector capped at 80s (no duration probe).
- Media queries reuse the doc-side embedding by content hash, else fetch
  bytes from the query URL and embed — search never persists to GCS, so
  no orphan files.
- All Gemini calls (embeddings + transcription) use the Developer API and
  feed media via the Files API; Vertex AI is removed.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

MrOrz commented Jul 30, 2026

Copy link
Copy Markdown
Member

感謝這個 PR,我把 41 個檔案完整看過一輪了,混合搜尋的整體方向沒問題,mapping 也跟 cofacts/rumors-db#78 對得上(那邊我已經留了 review)。

不過在進入細部 review 之前,想先請你把它拆成 stacked PR。GitHub 的 stacked pull requests說明)剛進 public preview,這個 PR 剛好是很適合的案子。

為什麼想拆

這個 PR 目前夾了三類彼此獨立的變更,其中一類會直接影響另外兩類的驗證基準:

.github/workflows/ci.yml 拿掉了所有雲端憑證(workload identity、GCS_CREDENTIALSOPENAI_API_KEYTEST_DATASET),改由新的 integration.yml 每天排程跑。這個方向我沒意見,但它代表現在這個 PR 的綠燈 CI 並不能證明 Vertex / GCS 相關的新程式碼可用 —— 而那正是這個 PR 最需要被驗證的部分。這件事值得單獨討論、單獨 merge,而不是跟功能一起進來。

另外 createTranscript 的重構(搜尋路徑不再把 media 上傳 GCS,改直接把 caller URL 當 fileData.fileUri 交給 Vertex)也是既有行為的變更,跟向量搜尋沒有依賴關係,但值得認真看一輪。

建議的三層

# 主題 內容
1 CI 分流 + 測試穩定性 ci.ymlintegration.yml、三個 else describe.skipmedia-integration.js / genAITranscript.js / fetchStatsFromGA.js)、ListReplyRequests.js + snapshot 的 orderBy fix、test/setup.js 的 langfuse mock、三個新的 unit test(fetchStatsFromGA.unit.js / replaceMedia.unit.js / genAITranscript.unit.js)、docker-compose.yml.gitignoreexperimentAVTranscript.ts
2 createTranscript 不再上傳 GCS + 抽出 createGenAI src/util/genai.tsutil-transcript.unit.jssrc/graphql/util.js 的下半部 hunk
3 vector search 其餘全部(util/embedding.ts、三支 mutation、ListArticles / ListReplies、backfill、src/rumors-db submodule bump…)

好消息:現在拆是零衝突

  • git merge-base master vector-search = 436535f = 現在的 master HEAD,PR 完全 up-to-date。

  • 上面第 1 類幾乎都是整檔獨立git checkout 搬檔就好。

  • 唯一需要 hunk-level 拆的只有 src/graphql/util.js,而且兩組 hunk 完全不重疊:

    組別 hunk 起始行(新檔行號)
    transcript / genai L1(移除 import)、L846、L860、L959、L1033
    vector L110(buildKnnQuery)、L240(containsKnnClause)、L248(defaultResolveTotalCount)、L285(highlight opt-out)、L565、L645(createAIResponse 的 user optional)

    vector 那組全部 ≤ L667,transcript 那組全部 ≥ L846,中間隔了近 180 行。唯一交集是最上面 import 區塊的兩行(createGenAI vs getTotalCount)。git checkout -p master -- src/graphql/util.js 反選幾個 hunk 就分得開。

實務上最省事的做法是反向操作:從現在這個分支開一份,把要拆走的檔案 git checkout master -- <files> 還原掉,就直接得到 PR-3;再從 master 開兩個分支 git checkout vector-search -- <files> 撈出 PR-1 / PR-2。

兩個歸屬要決定的東西

  • @google/genai ^1.6.0 → ^2.0.1:全 repo 只有 util/genai.ts import 它,transcribeAV 用的 models.generateContent v1 就有,所以這個 major bump 應該是為了 models.embedContentoutputDimensionality / taskType → 歸 PR-3。但要注意這是 major bump,而 CI 已經不跑真實 Gemini 了,逐字稿的回歸沒有防護網。
  • @google-cloud/storage 從 devDependencies 移到 dependencies:這個看起來是誤改?全 repo 只有 genAITranscript.js__tests__/util.js 兩個測試檔 import 它,production code 沒有用到。建議退回 devDependencies。

另外幾個小提醒

  • ES 版本ci.yml 和新增的 integration.yml 都是 elasticsearch:9.2.2,但 production 的 rumors-deploy-db-1 已經在跑 9.3.2。這個 PR 把 docker-compose.yml 拉到 9.3.2 是對的(在修漂移),但 CI 也該一起跟上 —— 尤其 9.1 → 9.3 之間動到的正好是 BBQ 預設 index_options、exclude_source_vectors、ACORN filtered vector search,全都跟這個 PR 直接相關。這條建議放進 PR-1。
  • restacksrc/graphql/util.js 三層都會碰到(PR-2 動下半部、PR-3 動上半部),底層改動後上層要記得 restack。不過因為兩組 hunk 隔了 180 行,實際衝突機率很低。
  • docker-compose.yml 的 api port 5000 → 5001 我看不出跟這個 PR 的關係,而且會影響既有的本地開發習慣與 rumors-site 預設 —— 這個是有意的嗎?

拆完之後我會針對每一層分別留 review。先講兩個已經確定的方向,讓你在整理 PR-3 時可以一起處理:

  1. kNN 的相似度應該要進排序分數。 目前 kNN 是放在 bool.filter 純做候選過濾,minimum_should_match 降到 0 之後由 BM25 should 排序 —— 這代表「換句話說、字面零重疊」的變形訊息(也就是這個功能最想撈到的東西)BM25 是 0 分,會排在所有字面命中之後,而 0 分之間的順序由 _shard_doc 決定,等於隨機。
  2. 長影音分段先不做沒問題(前 80 秒單一 vector 的取捨合理),但 filter.mediaDuration 這個 GraphQL 參數全 repo 沒有任何地方讀它,建議直接刪掉 —— 公開 schema 加了之後很難拿掉。

辛苦了 🙏


Generated by Claude Code

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.

3 participants