Skip to content

Vector search performance enhancements - #3754

Open
shanbady wants to merge 5 commits into
mainfrom
shanbady/vector-results-from-qdrant-payload
Open

Vector search performance enhancements#3754
shanbady wants to merge 5 commits into
mainfrom
shanbady/vector-results-from-qdrant-payload

Conversation

@shanbady

@shanbady shanbady commented Aug 11, 2026

Copy link
Copy Markdown
Contributor

What are the relevant tickets?

Closes https://github.com/mitodl/hq/issues/12687

Description (What does it do?)

This PR contains the following performance enhancements for vector search:

  1. in the vector learning resources api, render the payload directly from qdrant (skip serializing with the db altogether) - there is also a setting to make this easy to toggle on/off
  2. A fix for an issue where on the vector search frontend (https://learn.mit.edu/search?q=test&vector_search=true) the interface performs an SSR for regular search before fetching vector search results (user is waiting on 2 requests for every search).

How can this be tested?

  1. checkout main
  2. make sure you have lots of resources embedded
  3. go to the search interface and open up developer tools and paste the following script which runs a bunch of queries against the vector endpoint and give you some numbers on how long the results took to render end to end:
(async () => {
  const QUERIES = [
    "test",
    "machine learning",
    "calculus",
    "climate",
    "python",
    "economics",
    "robotics",
    "statistics",
  ]
  const REPEATS = 3
  const WARMUPS = 1
  const PAUSE_MS = 150

  const AGGS = [
    "resource_type",
    "certification_type",
    "delivery",
    "department",
    "topic",
    "offered_by",
    "free",
    "resource_category",
    "resource_type_group",
  ]

  const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms))
  const ms = (n) => `${Math.round(n)}ms`
  const avg = (xs) => xs.reduce((sum, x) => sum + x, 0) / xs.length

  const getEnv = () => {
    if (window.__ENV) return window.__ENV
    const meta = document.querySelector('meta[name="x-public-env"]')
    if (!meta) return {}
    try {
      window.__ENV = JSON.parse(meta.getAttribute("content") || "{}")
      return window.__ENV
    } catch {
      return {}
    }
  }

  const apiBase =
    getEnv().NEXT_PUBLIC_MITOL_API_BASE_URL ||
    (location.hostname === "learn.mit.edu"
      ? "https://api.learn.mit.edu"
      : location.origin)

  const pageUrl = (q) => {
    const url = new URL("/search", location.origin)
    url.searchParams.set("q", q)
    url.searchParams.set("vector_search", "true")
    return url
  }

  const vectorApiUrl = (q) => {
    const url = new URL("/api/v0/vector_learning_resources_search/", apiBase)
    AGGS.forEach((agg) => url.searchParams.append("aggregations", agg))
    url.searchParams.set("q", q)
    url.searchParams.set("hybrid_search", "true")
    return url
  }

  const timedTextFetch = async (url, accept) => {
    const start = performance.now()
    const res = await fetch(url, {
      cache: "no-store",
      credentials: "include",
      headers: {
        Accept: accept,
        "Cache-Control": "no-cache",
      },
    })
    const text = await res.text()
    const elapsed = performance.now() - start
    if (!res.ok) throw new Error(`${res.status} ${res.statusText}: ${url}`)
    return { ms: elapsed, text }
  }

  const detectHydratedQuery = (html) => {
    const normalized = html.replaceAll("\\", "")
    return {
      vectorSSR: normalized.includes(
        '"queryKey":["learning_resources","vectorSearch"',
      ),
      openSearchSSR: normalized.includes(
        '"queryKey":["learning_resources","search"',
      ),
    }
  }

  const rows = []

  console.log(`Benchmarking vector search page on ${location.origin}`)
  console.log(`API base: ${apiBase}`)
  console.log("")

  for (const q of QUERIES) {
    for (let i = 0; i < WARMUPS; i++) {
      await timedTextFetch(pageUrl(q), "text/html")
      await timedTextFetch(vectorApiUrl(q), "application/json")
      await sleep(PAUSE_MS)
    }

    const pageTimes = []
    const apiTimes = []
    let vectorSSR = false
    let openSearchSSR = false

    for (let i = 0; i < REPEATS; i++) {
      const page = await timedTextFetch(pageUrl(q), "text/html")
      pageTimes.push(page.ms)

      const detected = detectHydratedQuery(page.text)
      vectorSSR ||= detected.vectorSSR
      openSearchSSR ||= detected.openSearchSSR

      const api = await timedTextFetch(vectorApiUrl(q), "application/json")
      apiTimes.push(api.ms)

      await sleep(PAUSE_MS)
    }

    const ssrAvg = avg(pageTimes)
    const rawVectorApiAvg = avg(apiTimes)

    const ssrLabel = vectorSSR
      ? "SSR vector"
      : openSearchSSR
        ? "SSR OpenSearch"
        : "SSR unknown"

    const clientVectorFetchAvg = vectorSSR ? 0 : rawVectorApiAvg
    const renderAvg = ssrAvg + clientVectorFetchAvg

    const equation = `${ms(ssrAvg)} ${ssrLabel} + ${ms(
      clientVectorFetchAvg,
    )} client vector fetch = ${ms(renderAvg)} to render vector results`

    rows.push({
      query: q,
      path: vectorSSR ? "branch/fixed" : "main/current",
      equation,
      "SSR ms": Math.round(ssrAvg),
      "raw vector API ms": Math.round(rawVectorApiAvg),
      "client vector fetch ms": Math.round(clientVectorFetchAvg),
      "render ms": Math.round(renderAvg),
    })

    console.log(`${q}: ${equation}`)
  }

  console.log("")
  console.table(rows)

  const avgSSR = avg(rows.map((r) => r["SSR ms"]))
  const avgRawVectorApi = avg(rows.map((r) => r["raw vector API ms"]))
  const avgClientVectorFetch = avg(rows.map((r) => r["client vector fetch ms"]))
  const avgRender = avg(rows.map((r) => r["render ms"]))

  const detectedPath = rows.some((r) => r.path === "branch/fixed")
    ? "branch/fixed"
    : "main/current"

  const summaryLabel =
    detectedPath === "branch/fixed" ? "SSR vector" : "SSR OpenSearch"

  console.log("")
  console.log("Summary")
  console.log(
    `${ms(avgSSR)} ${summaryLabel} + ${ms(
      avgClientVectorFetch,
    )} client vector fetch = ${ms(avgRender)} average to render vector results`,
  )
  console.log(`Raw vector API baseline: ${ms(avgRawVectorApi)}`)
})()
  1. checkout this branch - reload the search interface and re-run the above script and note the difference in average response time

@github-actions

github-actions Bot commented Aug 11, 2026

Copy link
Copy Markdown

OpenAPI Changes

2 changes: 0 error, 0 warning, 2 info

View full changelog

Unexpected changes? Ensure your branch is up-to-date with main (consider rebasing).

@shanbady
shanbady marked this pull request as ready for review August 11, 2026 15:58
Copilot AI balanced review requested due to automatic review settings August 11, 2026 15:58
@shanbady shanbady added the Needs Review An open Pull Request that is ready for review label Aug 11, 2026

Copilot AI 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.

Pull request overview

Improves vector-search performance by serving Qdrant payloads directly and prefetching vector results during SSR.

Changes:

  • Adds configurable payload-backed resource responses.
  • Trims indexing-only payload fields.
  • Aligns SSR prefetching with the selected search mode.

Reviewed changes

Copilot reviewed 10 out of 10 changed files in this pull request and generated 1 comment.

Show a summary per file
File Description
vector_search/views.py Uses selected Qdrant payloads directly.
vector_search/views_test.py Tests payload selection and fallback behavior.
vector_search/utils.py Adds payload selection and transformation helpers.
vector_search/utils_test.py Tests payload parity, trimming, and deduplication.
vector_search/constants.py Defines excluded payload fields.
main/settings.py Adds the payload-response feature toggle.
vectorSearchParams.ts Extracts shared vector request mapping.
HybridSearchDisplay.tsx Uses shared vector parameter helpers.
search/page.tsx Prefetches vector results during SSR.
search/page.test.tsx Tests endpoint-specific SSR prefetching.

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread vector_search/constants.py Outdated
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Needs Review An open Pull Request that is ready for review

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants