Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 0 additions & 3 deletions .dockerignore

This file was deleted.

43 changes: 32 additions & 11 deletions .env.example
Original file line number Diff line number Diff line change
@@ -1,21 +1,42 @@
# Backend runtime config.
# Local: copy to .env. Railway: set these as service variables.
# Runtime config. Local: copy to .env.local and run `vercel dev`.
# Production: set these as Vercel project environment variables.
#
# There is exactly one service to configure. The frontend, the API functions and
# the embedding models all live behind these two variables.

# Qdrant instance URL. Point at Qdrant Cloud for production.
QDRANT_URL=https://your-cluster-id.aws.cloud.qdrant.io:6333
# Qdrant Cloud cluster URL, with the port.
QDRANT_URL=https://your-cluster-id.region.aws.cloud.qdrant.io:6333

# Qdrant Cloud API key. Empty for self-hosted local instances.
# Qdrant Cloud API key.
QDRANT_API_KEY=

# Comma-separated allowlist of frontend origins that may call this API.
# Example: https://code-search.vercel.app,https://code-search-git-main.vercel.app
CORS_ORIGINS=https://code-search.vercel.app
# Collections. Defaults match what indexer/build.py creates.
QDRANT_CODE_COLLECTION=code-snippets-cloud
QDRANT_NLU_COLLECTION=code-signatures-cloud
QDRANT_FILE_COLLECTION=code-files-cloud

# Uvicorn worker count. 1 is fine unless you need concurrent requests.
WORKERS=1
# Models, both served from inside the cluster by Qdrant Cloud Inference.
# Changing these means rebuilding the collections: the dense dimension is part
# of the collection config, and the sparse leg's statistics are per-collection.
QDRANT_DENSE_MODEL=mixedbread-ai/mxbai-embed-large-v1
QDRANT_SPARSE_MODEL=Qdrant/bm25

# Candidates each leg of the hybrid query contributes before fusion.
PREFETCH_LIMIT=100

# Indexing only. Cut every document to this many characters before sending it to
# be embedded; 0 disables it.
#
# Qdrant Cloud Inference truncates at 512 tokens for every model, but all-MiniLM-L6-v2
# has a published max_seq_length of 256 and degrades when fed past it. On this
# corpus that is worth 0.863 vs 0.937 docstring recall@10 - see
# bench/truncation.py. 700 characters is roughly 256 tokens of Rust.
#
# Set to 0 for a model whose own limit is 512, such as mxbai-embed-large-v1.
INDEX_CHAR_BUDGET=0

# Commit of qdrant/qdrant the collections were built from. Result links carry
# line numbers, so they only land on the right code when resolved against this.
# The indexing run prints the SHA at the end of its log. Defaults to `master`,
# which is what the links used before and drifts as the source moves.
# which drifts as the source moves.
INDEXED_COMMIT=
14 changes: 7 additions & 7 deletions .github/dependabot.yml
Original file line number Diff line number Diff line change
@@ -1,7 +1,12 @@
# No pip or docker ecosystems here: the indexer and the benchmarks are stdlib,
# and there is no container to keep patched. What is left is npm in two places
# and the actions.
version: 2
updates:
- package-ecosystem: "pip"
directory: "/"
- package-ecosystem: "npm"
directories:
- "/"
- "/frontend"
schedule:
interval: "weekly"
open-pull-requests-limit: 5
Expand All @@ -17,11 +22,6 @@ updates:
- "patch"
patterns:
- "*"
- package-ecosystem: "docker"
directory: "/"
schedule:
interval: "weekly"
open-pull-requests-limit: 5
- package-ecosystem: "github-actions"
directory: "/"
schedule:
Expand Down
41 changes: 41 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
name: CI

on:
push:
branches: [ main, master ]
pull_request:

jobs:
check:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: '22'
# The API functions and the frontend are typechecked separately: they have
# different tsconfigs, and the API's has no DOM-heavy React types in it.
- name: Install API dev deps
run: npm install
- name: Typecheck API
run: npm run typecheck
- name: Test API
run: npm test
- name: Install frontend
run: npm --prefix frontend ci
- name: Typecheck and build frontend
run: npm --prefix frontend run build
- name: Lint frontend
run: npm --prefix frontend run lint

# The bench scripts are stdlib-only on purpose; this catches a syntax error
# before someone discovers it four hours into an indexing run.
python:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: '3.12'
- name: Compile
run: python -m compileall -q bench indexer
49 changes: 0 additions & 49 deletions .github/workflows/deploy.yml

This file was deleted.

110 changes: 61 additions & 49 deletions .github/workflows/index-latest.yml
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
name: Index Qdrant source

# Trigger manually. No schedule here: GitHub disables scheduled workflows in
# forks, which left this one stuck in `disabled_fork` and unrunnable. A demo
# doesn't need nightly reindexing anyway, and the run takes about six hours.
# forks, which left the old one stuck in `disabled_fork` and unrunnable. A demo
# doesn't need nightly reindexing anyway.
on:
workflow_dispatch:

Expand All @@ -11,53 +11,65 @@ env:

jobs:
build:

runs-on: ubuntu-latest

steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: Install minimal stable
uses: actions-rs/toolchain@v1
with:
profile: minimal
toolchain: stable
- name: Set up Python 3.10
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
with:
python-version: '3.10'
- name: Install dependencies
run: |
pip install -r requirements.txt
- name: Add rust analyzer
run: rustup component add rust-analyzer
# Shape-check the secrets before spending six hours on the indexing run.
# Reports length and word count rather than the values themselves, which is
# enough to spot the usual breakage: prompt text or a stray newline captured
# along with the value when it was pasted in.
- name: Check secrets
env:
QDRANT_URL: ${{ secrets.QDRANT_URL }}
QDRANT_API_KEY: ${{ secrets.QDRANT_API_KEY }}
run: |
fail=0
for name in QDRANT_URL QDRANT_API_KEY; do
value="${!name}"
words=$(printf '%s' "$value" | wc -w | tr -d ' ')
printf '%s: %s chars, %s word(s)\n' "$name" "${#value}" "$words"
if [ -z "$value" ]; then echo " -> not set"; fail=1; fi
if [ "$words" -gt 1 ]; then echo " -> contains whitespace, so it was pasted with extra text"; fail=1; fi
done
case "$QDRANT_URL" in https://*) ;; *) echo 'QDRANT_URL does not start with https://'; fail=1;; esac
# Qdrant Cloud keys are JWTs, but self-hosted keys are arbitrary strings,
# so a non-JWT shape is worth a note in the log rather than a failed run.
case "$QDRANT_API_KEY" in eyJ*) ;; *) echo 'note: QDRANT_API_KEY is not JWT-shaped, fine for self-hosted Qdrant';; esac
[ "$fail" -eq 0 ] || { echo 'Re-set the offending secret with: gh secret set <NAME> --repo qdrant-labs/demo-code-search'; exit 1; }
echo 'Both secrets look well-formed.'
# Passed through env rather than interpolated into the command line: a value
# containing a space used to split the assignment and abort the run at
# `secret: command not found` before any indexing happened.
- name: Run indexing
env:
QDRANT_URL: ${{ secrets.QDRANT_URL }}
QDRANT_API_KEY: ${{ secrets.QDRANT_API_KEY }}
run: bash -x tools/download_and_index.sh
- uses: actions/checkout@v4
- name: Install minimal stable
uses: actions-rs/toolchain@v1
with:
profile: minimal
toolchain: stable
- uses: actions/setup-python@v5
with:
python-version: '3.12'
- name: Add rust analyzer
run: rustup component add rust-analyzer

# Shape-check the secrets before spending hours on the indexing run.
# Reports length and word count rather than the values themselves, which
# is enough to spot the usual breakage: prompt text or a stray newline
# captured along with the value when it was pasted in.
- name: Check secrets
env:
QDRANT_URL: ${{ secrets.QDRANT_URL }}
QDRANT_API_KEY: ${{ secrets.QDRANT_API_KEY }}
run: |
fail=0
for name in QDRANT_URL QDRANT_API_KEY; do
value="${!name}"
words=$(printf '%s' "$value" | wc -w | tr -d ' ')
printf '%s: %s chars, %s word(s)\n' "$name" "${#value}" "$words"
if [ -z "$value" ]; then echo " -> not set"; fail=1; fi
if [ "$words" -gt 1 ]; then echo " -> contains whitespace, so it was pasted with extra text"; fail=1; fi
done
case "$QDRANT_URL" in https://*) ;; *) echo 'QDRANT_URL does not start with https://'; fail=1;; esac
case "$QDRANT_API_KEY" in eyJ*) ;; *) echo 'note: QDRANT_API_KEY is not JWT-shaped, fine for self-hosted Qdrant';; esac
[ "$fail" -eq 0 ] || { echo 'Re-set the offending secret.'; exit 1; }
echo 'Both secrets look well-formed.'

# Qdrant Cloud Inference has to be on for the cluster, or every upsert fails with
# "Unsupported model" after the corpus has already been built. Two seconds
# here against hours of wasted run.
- name: Check Qdrant Cloud Inference is enabled
env:
QDRANT_URL: ${{ secrets.QDRANT_URL }}
QDRANT_API_KEY: ${{ secrets.QDRANT_API_KEY }}
run: |
body='{"query":{"text":"probe","model":"sentence-transformers/all-MiniLM-L6-v2"},"limit":1}'
out=$(curl -sS -X POST -H "api-key: $QDRANT_API_KEY" -H 'Content-Type: application/json' \
"$QDRANT_URL/collections/does-not-exist/points/query" -d "$body")
echo "$out"
case "$out" in
*"Unsupported model"*|*"Expected some form of vector"*)
echo 'Qdrant Cloud Inference is not serving this model on this cluster.'
echo 'Enable it on the Inference tab of the cluster page in the Cloud Console.'
exit 1;;
esac
echo 'Inference reachable.'

- name: Run indexing
env:
QDRANT_URL: ${{ secrets.QDRANT_URL }}
QDRANT_API_KEY: ${{ secrets.QDRANT_API_KEY }}
run: bash -x tools/download_and_index.sh
Loading
Loading